diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 8a520769..d5125011 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,5 +1,8 @@ ## Tips: +* please read the style guide before submitting your patch: +[docs/style-guide.md](docs/style-guide.md) + * commit message titles must be in the form: ```topic: Capitalized message with no trailing period``` or: diff --git a/.github/settings.yml b/.github/settings.yml index 4039b46a..eea18ac9 100644 --- a/.github/settings.yml +++ b/.github/settings.yml @@ -10,10 +10,10 @@ repository: description: Next generation distributed, event-driven, parallel config management! # A URL with more information about the repository - homepage: https://ttboj.wordpress.com/?s=mgmtconfig + homepage: https://purpleidea.com/tags/mgmtconfig/ # A comma-separated list of topics to set on the repository - topics: golang, go, configuration-management, config-management, devops, etcd, distributed-systems, graph-theory + topics: golang, go, configuration-management, config-management, devops, etcd, distributed-systems, graph-theory, choreography # Either `true` to make the repository private, or `false` to make it public. private: false diff --git a/Makefile b/Makefile index 707c8e56..6fb86a37 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,7 @@ # along with this program. If not, see . SHELL = /usr/bin/env bash -.PHONY: all art cleanart version program path deps run race bindata generate build crossbuild clean test gofmt yamlfmt format docs rpmbuild mkdirs rpm srpm spec tar upload upload-sources upload-srpms upload-rpms copr +.PHONY: all art cleanart version program lang path deps run race bindata generate build crossbuild clean test gofmt yamlfmt format docs rpmbuild mkdirs rpm srpm spec tar upload upload-sources upload-srpms upload-rpms copr .SILENT: clean bindata GO_FILES := $(shell find . -name '*.go') @@ -104,12 +104,16 @@ race: # generate go files from non-go source bindata: + @echo "Generating: bindata..." $(MAKE) --quiet -C bindata generate: go generate -build: bindata $(PROGRAM) +lang: + @# recursively run make in child dir named lang + @echo "Generating: lang..." + $(MAKE) --quiet -C lang $(PROGRAM): $(GO_FILES) @echo "Building: $(PROGRAM), version: $(SVERSION)..." @@ -128,7 +132,11 @@ $(PROGRAM).static: $(GO_FILES) go generate go build -a -installsuffix cgo -tags netgo -ldflags '-extldflags "-static" -X main.program=$(PROGRAM) -X main.version=$(SVERSION) -s -w' -o $(PROGRAM).static $(BUILD_FLAGS); +build: bindata lang $(PROGRAM) + clean: + $(MAKE) --quiet -C bindata clean + $(MAKE) --quiet -C lang clean [ ! -e $(PROGRAM) ] || rm $(PROGRAM) rm -f *_stringer.go # generated by `go generate` rm -f *_mock.go # generated by `go generate` @@ -138,8 +146,8 @@ test: bindata gofmt: # TODO: remove gofmt once goimports has a -s option - find . -maxdepth 3 -type f -name '*.go' -not -path './old/*' -not -path './tmp/*' -exec gofmt -s -w {} \; - find . -maxdepth 3 -type f -name '*.go' -not -path './old/*' -not -path './tmp/*' -exec goimports -w {} \; + find . -maxdepth 6 -type f -name '*.go' -not -path './old/*' -not -path './tmp/*' -not -path './vendor/*' -exec gofmt -s -w {} \; + find . -maxdepth 6 -type f -name '*.go' -not -path './old/*' -not -path './tmp/*' -not -path './vendor/*' -exec goimports -w {} \; yamlfmt: find . -maxdepth 3 -type f -name '*.yaml' -not -path './old/*' -not -path './tmp/*' -not -path './omv.yaml' -exec ruby -e "require 'yaml'; x=YAML.load_file('{}').to_yaml.each_line.map(&:rstrip).join(10.chr)+10.chr; File.open('{}', 'w').write x" \; diff --git a/README.md b/README.md index 415e52ce..b43ed5a7 100644 --- a/README.md +++ b/README.md @@ -18,10 +18,13 @@ Come join us in the `mgmt` community! | Mailing list | [mgmtconfig-list@redhat.com](https://www.redhat.com/mailman/listinfo/mgmtconfig-list) | ## Status: -Mgmt is a fairly new project. -We're working towards being minimally useful for production environments. -We aren't feature complete for what we'd consider a 1.x release yet. -With your help you'll be able to influence our design and get us there sooner! +Mgmt is a next generation automation tool. It has similarities to other tools in +the configuration management space, but has a fast, modern, distributed systems +approach. The project contains an engine and a language. +[Please have a look at an introductory video or blog post.](docs/on-the-web.md) + +Mgmt is a fairly new project. It is usable today, but not yet feature complete. +With your help you'll be able to influence our design and get us to 1.0 sooner! Interested developers should read the [quick start guide](docs/quick-start-guide.md). ## Documentation: @@ -33,6 +36,8 @@ Please read, enjoy and help improve our documentation! | [quick start guide](docs/quick-start-guide.md) | for mgmt developers | | [frequently asked questions](docs/faq.md) | for everyone | | [resource guide](docs/resource-guide.md) | for mgmt developers | +| [language guide](docs/language-guide.md) | for everyone | +| [style guide](docs/style-guide.md) | for mgmt developers | | [godoc API reference](https://godoc.org/github.com/purpleidea/mgmt) | for mgmt developers | | [prometheus guide](docs/prometheus.md) | for everyone | | [puppet guide](docs/puppet-guide.md) | for puppet sysadmins | @@ -49,7 +54,7 @@ Please get involved by working on one of these items or by suggesting something ## Bugs: Please set the `DEBUG` constant in [main.go](https://github.com/purpleidea/mgmt/blob/master/main.go) to `true`, and post the logs when you report the [issue](https://github.com/purpleidea/mgmt/issues). Bonus points if you provide a [shell](https://github.com/purpleidea/mgmt/tree/master/test/shell) or [OMV](https://github.com/purpleidea/mgmt/tree/master/test/omv) reproducible test case. -Feel free to read my article on [debugging golang programs](https://ttboj.wordpress.com/2016/02/15/debugging-golang-programs/). +Feel free to read my article on [debugging golang programs](https://purpleidea.com/blog/2016/02/15/debugging-golang-programs/). ## Patches: We'd love to have your patches! Please send them by email, or as a pull request. diff --git a/TODO.md b/TODO.md index 1bc4179e..0a7e66a4 100644 --- a/TODO.md +++ b/TODO.md @@ -24,7 +24,6 @@ level and how many hours you'd like to spend on the patch. - [ ] increment algorithm (linear, exponential, etc...) [:heart:](https://github.com/purpleidea/mgmt/labels/mgmtlove) ## User/Group resource -- [ ] base resource [:heart:](https://github.com/purpleidea/mgmt/labels/mgmtlove) - [ ] automatic edges to file resource [:heart:](https://github.com/purpleidea/mgmt/labels/mgmtlove) ## Virt (libvirt) resource @@ -55,8 +54,7 @@ level and how many hours you'd like to spend on the patch. - [ ] base plumbing ## Language improvements -- [ ] language design -- [ ] lexer/parser +- [ ] more core functions - [ ] automatic language formatter, ala `gofmt` - [ ] gedit/gnome-builder/gtksourceview syntax highlighting - [ ] vim syntax highlighting diff --git a/bindata/Makefile b/bindata/Makefile index 3d013fe7..fd3e6c7f 100644 --- a/bindata/Makefile +++ b/bindata/Makefile @@ -20,14 +20,19 @@ # `bytes, err := bindata.Asset("FILEPATH")` # where FILEPATH is the path of the original input file relative to `bindata/`. -.PHONY: build +.PHONY: build clean default: build build: bindata.go # add more input files as dependencies at the end here... bindata.go: ../COPYING - # go-bindata --pkg bindata -o {OUTPUT} {INPUT} + # go-bindata --pkg bindata -o go-bindata --pkg bindata -o ./$@ $^ # gofmt the output file gofmt -s -w $@ + @ROOT=$$(dirname "$${BASH_SOURCE}")/.. && $$ROOT/misc/header.sh '$@' + +clean: + # remove generated bindata/*.go + @ROOT=$$(dirname "$${BASH_SOURCE}")/.. && rm *.go diff --git a/docs/documentation.md b/docs/documentation.md index 120b996d..5a2696b7 100644 --- a/docs/documentation.md +++ b/docs/documentation.md @@ -13,13 +13,13 @@ foundation in and for, new and existing software. For more information, you may like to read some blog posts from the author: -* [Next generation config mgmt](https://ttboj.wordpress.com/2016/01/18/next-generation-configuration-mgmt/) -* [Automatic edges in mgmt](https://ttboj.wordpress.com/2016/03/14/automatic-edges-in-mgmt/) -* [Automatic grouping in mgmt](https://ttboj.wordpress.com/2016/03/30/automatic-grouping-in-mgmt/) -* [Automatic clustering in mgmt](https://ttboj.wordpress.com/2016/06/20/automatic-clustering-in-mgmt/) -* [Remote execution in mgmt](https://ttboj.wordpress.com/2016/10/07/remote-execution-in-mgmt/) -* [Send/Recv in mgmt](https://ttboj.wordpress.com/2016/12/07/sendrecv-in-mgmt/) -* [Metaparameters in mgmt](https://ttboj.wordpress.com/2017/03/01/metaparameters-in-mgmt/) +* [Next generation config mgmt](https://purpleidea.com/blog/2016/01/18/next-generation-configuration-mgmt/) +* [Automatic edges in mgmt](https://purpleidea.com/blog/2016/03/14/automatic-edges-in-mgmt/) +* [Automatic grouping in mgmt](https://purpleidea.com/blog/2016/03/30/automatic-grouping-in-mgmt/) +* [Automatic clustering in mgmt](https://purpleidea.com/blog/2016/06/20/automatic-clustering-in-mgmt/) +* [Remote execution in mgmt](https://purpleidea.com/blog/2016/10/07/remote-execution-in-mgmt/) +* [Send/Recv in mgmt](https://purpleidea.com/blog/2016/12/07/sendrecv-in-mgmt/) +* [Metaparameters in mgmt](https://purpleidea.com/blog/2017/03/01/metaparameters-in-mgmt/) There is also an [introductory video](https://www.youtube.com/watch?v=LkEtBVLfygE&html5=1) available. Older videos and other material [is available](on-the-web.md). @@ -62,7 +62,7 @@ the meta attributes of that resource to `false`. #### Blog post You can read the introductory blog post about this topic here: -[https://ttboj.wordpress.com/2016/03/14/automatic-edges-in-mgmt/](https://ttboj.wordpress.com/2016/03/14/automatic-edges-in-mgmt/) +[https://purpleidea.com/blog/2016/03/14/automatic-edges-in-mgmt/](https://purpleidea.com/blog/2016/03/14/automatic-edges-in-mgmt/) ### Autogrouping @@ -81,7 +81,7 @@ the meta attributes of that resource to `false`. #### Blog post You can read the introductory blog post about this topic here: -[https://ttboj.wordpress.com/2016/03/30/automatic-grouping-in-mgmt/](https://ttboj.wordpress.com/2016/03/30/automatic-grouping-in-mgmt/) +[https://purpleidea.com/blog/2016/03/30/automatic-grouping-in-mgmt/](https://purpleidea.com/blog/2016/03/30/automatic-grouping-in-mgmt/) ### Automatic clustering @@ -97,7 +97,7 @@ with the `--seeds` variable. #### Blog post You can read the introductory blog post about this topic here: -[https://ttboj.wordpress.com/2016/06/20/automatic-clustering-in-mgmt/](https://ttboj.wordpress.com/2016/06/20/automatic-clustering-in-mgmt/) +[https://purpleidea.com/blog/2016/06/20/automatic-clustering-in-mgmt/](https://purpleidea.com/blog/2016/06/20/automatic-clustering-in-mgmt/) ### Remote ("agent-less") mode @@ -124,7 +124,7 @@ which need to exchange information that is only available at run time. #### Blog post You can read the introductory blog post about this topic here: -[https://ttboj.wordpress.com/2016/10/07/remote-execution-in-mgmt/](https://ttboj.wordpress.com/2016/10/07/remote-execution-in-mgmt/) +[https://purpleidea.com/blog/2016/10/07/remote-execution-in-mgmt/](https://purpleidea.com/blog/2016/10/07/remote-execution-in-mgmt/) ### Puppet support @@ -371,7 +371,7 @@ This is a project that I started in my free time in 2013. Development is driven by all of our collective patches! Dive right in, and start hacking! Please contact me if you'd like to invite me to speak about this at your event. -You can follow along [on my technical blog](https://ttboj.wordpress.com/). +You can follow along [on my technical blog](https://purpleidea.com/blog/). To report any bugs, please file a ticket at: [https://github.com/purpleidea/mgmt/issues](https://github.com/purpleidea/mgmt/issues). @@ -385,4 +385,4 @@ for more information. * [github](https://github.com/purpleidea/) * [@purpleidea](https://twitter.com/#!/purpleidea) -* [https://ttboj.wordpress.com/](https://ttboj.wordpress.com/) +* [https://purpleidea.com/](https://purpleidea.com/) diff --git a/docs/faq.md b/docs/faq.md index 248f54fe..83c2f073 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -76,12 +76,16 @@ anguishing, I chose the name because it was short and I thought it was appropriately descriptive. If you need a less ambiguous search term or phrase, you can try using `mgmtconfig` or `mgmt config`. +It also doesn't stand for +[Methyl Guanine Methyl Transferase](https://en.wikipedia.org/wiki/O-6-methylguanine-DNA_methyltransferase) +which definitely existed before the band did. + ### You didn't answer my question, or I have a question! It's best to ask on [IRC](https://webchat.freenode.net/?channels=#mgmtconfig) to see if someone can help you. Once we get a big enough community going, we'll add a mailing list. If you don't get any response from the above, you can -contact me through my [technical blog](https://ttboj.wordpress.com/contact/) +contact me through my [technical blog](https://purpleidea.com/contact/) and I'll do my best to help. If you have a good question, please add it as a patch to this documentation. I'll merge your question, and add a patch with the answer! diff --git a/docs/language-guide.md b/docs/language-guide.md new file mode 100644 index 00000000..4656f640 --- /dev/null +++ b/docs/language-guide.md @@ -0,0 +1,432 @@ +# Language guide + +## Overview +The `mgmt` tool has various frontends, each of which may produce a stream of +between zero or more graphs that are passed to the engine for desired state +application. In almost all scenarios, you're going to want to use the language +frontend. This guide describes some of the internals of the language. + +## Theory +The mgmt language is a declarative (immutable) functional, reactive programming +language. It is implemented in `golang`. A longer introduction to the language +is coming soon! + +### Types +All expressions must have a type. A composite type such as a list of strings +(`[]str`) is different from a list of integers (`[]int`). + +There _is_ a _variant_ type in the language's type system, but it is only used +internally and only appears briefly when needed for type unification hints +during static polymorphic function generation. This is an advanced topic which +is not required for normal usage of the software. + +The implementation of the internal types can be found in +[lang/types/](https://github.com/purpleidea/mgmt/tree/master/lang/types/). + +#### bool +A `true` or `false` value. + +#### str +Any `"string!"` enclosed in quotes. + +#### int +A number like `42` or `-13`. Integers are represented internally as golang's +`int64`. + +#### float +A floating point number like: `3.1415926`. Float's are represented internally as +golang's `float64`. + +#### list +An ordered collection of values of the same type, eg: `[6, 7, 8, 9,]`. It is +worth mentioning that empty lists have a type, although without type hints it +can be impossible to infer the item's type. + +#### map +An unordered set of unique keys of the same type and corresponding value pairs +of another type, eg: `{"boiling" => 100, "freezing" => 0, "room" => "25", "house" => 22, "canada" => -30,}`. +That is to say, all of the keys must have the same type, and all of the values +must have the same type. You can use any type for either, although it is +probably advisable to avoid using very complex types as map keys. + +#### struct +An ordered set of field names and corresponding values, each of their own type, +eg: `struct{answer => "42", james => "awesome", is_mgmt_awesome => true,}`. +These are useful for combining more than one type into the same value. Note the +syntactical difference between these and map's: the key's in map's have types, +and as a result, string keys are enclosed in quotes, whereas struct _fields_ are +not string values, and as such are bare and specified without quotes. + +#### func +An ordered set of optionally named, differently typed input arguments, and a +return type, eg: `func(s str) int` or: +`func(bool, []str, {str: float}) struct{foo str; bar int}`. + +### Expressions +Expressions, and the `Expr` interface need to be better documented. For now +please consume +[lang/interfaces/ast.go](https://github.com/purpleidea/mgmt/tree/master/lang/interfaces/ast.go). +These docs will be expanded on when things are more certain to be stable. + +### Statements +Statements, and the `Stmt` interface need to be better documented. For now +please consume +[lang/interfaces/ast.go](https://github.com/purpleidea/mgmt/tree/master/lang/interfaces/ast.go). +These docs will be expanded on when things are more certain to be stable. + +### Stages +The mgmt compiler runs in a number of stages. In order of execution they are: +* [Lexing](#lexing) +* [Parsing](#parsing) +* [Interpolation](#interpolation) +* [Scope propagation](#scope-propagation) +* [Type unification](#type-unification) +* [Function graph generation](#function-graph-generation) +* [Function engine creation and validation](#function-engine-creation-and-validation) + +All of the above needs to be done every time the source code changes. After this +point, the [function engine runs](#function-engine-running-and-interpret) and +produces events. On every event, we "[interpret](#function-engine-running-and-interpret)" +which produces a resource graph. This series of resource graphs are passed +to the engine as they are produced. + +What follows are some notes about each step. + +#### Lexing +Lexing is done using [nex](https://github.com/blynn/nex). It is a pure-golang +implementation which is similar to _Lex_ or _Flex_, but which produces golang +code instead of C. It integrates reasonably well with golang's _yacc_ which is +used for parsing. The token definitions are in: +[lang/lexer.nex](https://github.com/purpleidea/mgmt/tree/master/lang/lexer.nex). +Lexing and parsing run together by calling the `LexParse` method. + +#### Parsing +The parser used is golang's implementation of +[yacc](https://godoc.org/golang.org/x/tools/cmd/goyacc). The documentation is +quite abysmal, so it's helpful to rely on the documentation from standard yacc +and trial and error. One small advantage yacc has over standard yacc is that it +can produce error messages from examples. The best documentation is to examine +the source. There is a short write up available [here](https://research.swtch.com/yyerror). +The yacc file exists at: +[lang/parser.y](https://github.com/purpleidea/mgmt/tree/master/lang/parser.y). +Lexing and parsing run together by calling the `LexParse` method. + +#### Interpolation +Interpolation is used to transform the AST (which was produced from lexing and +parsing) into one which is either identical or different. It expands strings +which might contain expressions to be interpolated (eg: `"the answer is: ${foo}"`) +and can be used for other scenarios in which one statement or expression would +be better represented by a larger AST. Most nodes in the AST simply return their +own node address, and do not modify the AST. + +#### Scope propagation +Scope propagation passes the parent scope (starting with the top-level, built-in +scope) down through the AST. This is necessary so that children nodes can access +variables in the scope if needed. Most AST node's simply pass on the scope +without making any changes. The `ExprVar` node naturally consumes scope's and +the `StmtProg` node cleverly passes the scope through in the order expected for +the out-of-order bind logic to work. + +#### Type unification +Each expression must have a known type. The unpleasant option is to force the +programmer to specify by annotation every type throughout their whole program +so that each `Expr` node in the AST knows what to expect. Type annotation is +allowed in situations when you want to explicitly specify a type, or when the +compiler cannot deduce it, however, most of it can usually be inferred. + +For type inferrence to work, each node in the AST implements a `Unify` method +which is able to return a list of invariants that must hold true. This starts at +the top most AST node, and gets called through to it's children to assemble a +giant list of invariants. The invariants can take different forms. They can +specify that a particular expression must have a particular type, or they can +specify that two expressions must have the same types. More complex invariants +allow you to specify relationships between different types and expressions. +Furthermore, invariants can allow you to specify that only one invariant out of +a set must hold true. + +Once the list of invariants has been collected, they are run through an +invariant solver. The solver can return either return successfully or with an +error. If the solver returns successfully, it means that it has found a trivial +mapping between every expression and it's corresponding type. At this point it +is a simple task to run `SetType` on every expression so that the types are +known. If the solver returns in error, it is usually due to one of two +possibilities: + +1. Ambiguity + + The solver does not have enough information to make a definitive or + unique determination about the expression to type mappings. The set of + invariants is ambiguous, and we cannot continue. An error will be + returned to the programmer. In this scenario the user will probably need + to add a type annotation, possibly because of a design bug in the user's + program. + +2. Conflict + + The solver has conflicting information that cannot be reconciled. In + this situation an explicit conflict has been found. If two invariants + are found which both expect a particular expression to have different + types, then it is not possible to find a valid solution. This almost + always happens if the user has made a type error in their program. + +Only one solver currently exists, but it is possible to easily plug in an +alternate implementation if someone more skilled in the art of solver design +would like to propose a more logical or performant variant. + +#### Function graph generation +At this point we have a fully type AST. The AST must now be transformed into a +directed, acyclic graph (DAG) data structure that represents the flow of data as +necessary for everything to be reactive. Note that this graph is *different* +from the resource graph which is produced and sent to the engine. It is just a +coincidence that both happen to be DAG's. (You don't freak out when you see a +list data structure show up in more than one place, do you?) + +To produce this graph, each node has a `Graph` method which it can call. This +starts at the top most node, and is called down through the AST. The edges in +the graphs must represent the individual expression values which are passed +from node to node. The names of the edges must match the function type argument +names which are used in the definition of the corresponding function. These +corresponding functions must exist for each expression node and are produced by +calling that expression's `Func` method. These are usually called by the +function engine during function creation and validation. + +#### Function engine creation and validation +Finally we have a graph of the data flows. The function engine must first +initialize which creates references to each of the necessary function +implementations, and gets information about each one. It then needs to be type +checked to ensure that the data flows all correctly match what is expected. If +you were to pass an `int` to a function expecting a `bool`, this would be a +problem. If all goes well, the program should get run shortly. + +#### Function engine running and interpret +At this point the function engine runs. It produces a stream of events which +cause the `Output()` method of the top-level program to run, which produces the +list of resources and edges. These are then transformed into the resource graph +which is passed to the engine. + +### Function API +If you'd like to create a built-in, core function, you'll need to implement the +function API interface named `Func`. It can be found in +[lang/interfaces/func.go](https://github.com/purpleidea/mgmt/tree/master/lang/interfaces/func.go). +Your function must have a specific type. For example, a simple math function +might have a signature of `func(x int, x int) int`. As you can see, all the +types are known _before_ compile time. + +What follows are each of the method signatures and a description of each. +Failure to implement the API correctly can cause the function graph engine to +block, or the program to panic. + +### Info +```golang +Info() *Info +``` + +The Info method must return a struct containing some information about your +function. The struct has the following type: + +```golang +type Info struct { + Sig *types.Type // the signature of the function, must be KindFunc +} +``` + +You must implement this correctly. Other fields in the `Info` struct may be +added in the future. This method is usually called before any other, and should +not depend on any other method being called first. Other methods must not depend +on this method being called first. + +#### Example +```golang +func (obj *FooFunc) Info() *interfaces.Info { + return &interfaces.Info{ + Sig: types.NewType("func(a str, b int) float"), + } +} +``` + +### Init +```golang +Init(*Init) error +``` + +Init is called by the function graph engine to create an implementation of this +function. It is passed in a struct of the following form: + +```golang +type Init struct { + Hostname string // uuid for the host + Input chan types.Value // Engine will close `input` chan + Output chan types.Value // Stream must close `output` chan + World resources.World + Debug bool + Logf func(format string, v ...interface{}) +} +``` + +These values and references may be used (wisely) inside your function. `Input` +will contain a channel of input structs matching the expected input signature +for your function. `Output` will be the channel which you must send values to +whenever a new value should be produced. This must be done in the `Stream()` +function. You may carefully use `World` to access functionality provided by the +engine. You may use `Logf` to log informational messages, however there is no +guarantee that they will be displayed to the user. `Debug` specifies whether the +function is running in a user-requested debug mode. This might cause you to want +to print more log messages for example. You will need to save references to any +or all of these info fields that you wish to use in the struct implementing this +`Func` interface. At a minimum you will need to save `Output` as a minimum of +one value must be produced. + +#### Example +```golang +Please see the example functions in +[lang/funcs/public/](https://github.com/purpleidea/mgmt/tree/master/lang/funcs/public/). +``` + +### Stream +```golang +Stream() error +``` + +Stream is called by the function engine when it is ready for your function to +start accepting input and producing output. You must always produce at least one +value. Failure to produce at least one value will probably cause the function +engine to hang waiting for your output. This function must close the `Output` +channel when it has no more values to send. The engine will close the `Input` +channel when it has no more values to send. This may or may not influence +whether or not you close the `Output` channel. + +#### Example +```golang +Please see the example functions in +[lang/funcs/public/](https://github.com/purpleidea/mgmt/tree/master/lang/funcs/public/). +``` + +### Close +```golang +Close() error +``` + +Close asks the particular function to shutdown its `Stream()` function and +return. + +#### Example +```golang +Please see the example functions in +[lang/funcs/public/](https://github.com/purpleidea/mgmt/tree/master/lang/funcs/public/). +``` + +### Polymorphic Function API +For some functions, it might be helpful to be able to implement a function once, +but to have multiple polymorphic variants that can be chosen at compile time. +For this more advanced topic, you will need to use the +[Polymorphic Function API](#polymorphic-function-api). This will help with code +reuse when you have a small, finite number of possible type signatures, and also +for more complicated cases where you might have an infinite number of possible +type signatures. (eg: `[]str`, or `[][]str`, or `[][][]str`, etc...) + +Suppose you want to implement a function which can assume different type +signatures. The mgmt language does not support polymorphic types-- you must use +static types throughout the language, however, it is legal to implement a +function which can take different specific type signatures based on how it is +used. For example, you might wish to add a math function which could take the +form of `func(x int, x int) int` or `func(x float, x float) float` depending on +the input values. You might also want to implement a function which takes an +arbitrary number of input arguments (the number must be statically fixed at the +compile time of your program though) and which returns a string. + +The `PolyFunc` interface adds additional methods which you must implement to +satisfy such a function implementation. If you'd like to implement such a +function, then please notify the project authors, and they will expand this +section with a longer description of the process. + +#### Examples + +What follows are a few examples that might help you understand some of the +language details. + +##### Example Foo +TODO: please add an example here! + +##### Example Bar +TODO: please add an example here! + +## Frequently asked questions +(Send your questions as a patch to this FAQ! I'll review it, merge it, and +respond by commit with the answer.) + +### What is the difference between `ExprIf` and `StmtIf`? + +The language contains both an `if` expression, and and `if` statement. An `if` +expression takes a boolean conditional *and* it must contain exactly _two_ +branches (a `then` and an `else` branch) which each contain one expression. The +`if` expression _will_ return the value of one of the two branches based on the +conditional. + +#### Example: +``` +# this is an if expression, and both branches must exist +$b = true +$x = if $b { + 42 +} else { + -13 +} +``` + +The `if` statement also takes a boolean conditional, but it may have either one +or two branches. Branches must only directly contain statements. The `if` +statement does not return any value, but it does produce output when it is +evaluated. The output consists primarily of resources (vertices) and edges. + +#### Example: +``` +# this is an if statement, and in this scenario the else branch was omitted +$b = true +if $b { + file "/tmp/hello" { + content => "world", + } +} +``` + +### I don't like the mgmt language, is there an alternative? + +Yes, the language is just one of the available "frontends" that passes a stream +of graphs to the engine "backend". While it _is_ the recommended way of using +mgmt, you're welcome to either use an alternate frontend, or write your own. To +write your own frontend, you must implement the +[GAPI](https://github.com/purpleidea/mgmt/blob/master/gapi/gapi.go) interface. + +### I'm an expert in FRP, and you got it all wrong; even the names of things! + +I am certainly no expert in FRP, and I've certainly got lots more to learn. One +thing FRP experts might notice is that some of the concepts from FRP are either +named differently, or are notably absent. + +In mgmt, we don't talk about behaviours, events, or signals in the strict FRP +definitons of the words. Firstly, because we only support discretized, streams +of values with no plan to add continuous semantics. Secondly, because we prefer +to use terms which are more natural and relatable to what our target audience is +expecting. Our users are more likely to have a background in Physiology, or +systems administration than a background in FRP. + +Having said that, we hope that the FRP community will engage with us and help +improve the parts that we got wrong. Even if that means adding continuous +behaviours! + +### This is brilliant, may I give you a high-five? + +Thank you, and yes, probably. "Props" may also be accepted, although patches are +preferred. If you can't do either, [donations](https://purpleidea.com/misc/donate/) +to support the project are welcome too! + +### Where can I find more information about mgmt? + +Additional blog posts, videos and other material +[is available!](https://github.com/purpleidea/mgmt/blob/master/docs/on-the-web.md). + +## Suggestions + +If you have any ideas for changes or other improvements to the language, please +let us know! We're still pre 1.0 and pre 0.1 and happy to change it in order to +get it right! diff --git a/docs/on-the-web.md b/docs/on-the-web.md index 1c3a9cbf..4224ee31 100644 --- a/docs/on-the-web.md +++ b/docs/on-the-web.md @@ -6,30 +6,30 @@ if we missed something that you think is relevant! ## Links | Author | Format | Subject | |---|---|---| -| James Shubin | blog | [Next generation configuration mgmt](https://ttboj.wordpress.com/2016/01/18/next-generation-configuration-mgmt/) | +| James Shubin | blog | [Next generation configuration mgmt](https://purpleidea.com/blog/2016/01/18/next-generation-configuration-mgmt/) | | James Shubin | video | [Introductory recording from DevConf.cz 2016](https://www.youtube.com/watch?v=GVhpPF0j-iE&html5=1) | | James Shubin | video | [Introductory recording from CfgMgmtCamp.eu 2016](https://www.youtube.com/watch?v=fNeooSiIRnA&html5=1) | | Julian Dunn | video | [On mgmt at CfgMgmtCamp.eu 2016](https://www.youtube.com/watch?v=kfF9IATUask&t=1949&html5=1) | | Walter Heck | slides | [On mgmt at CfgMgmtCamp.eu 2016](http://www.slideshare.net/olindata/configuration-management-time-for-a-4th-generation/3) | | Marco Marongiu | blog | [On mgmt](http://syslog.me/2016/02/15/leap-or-die/) | | Felix Frank | blog | [From Catalog To Mgmt (on puppet to mgmt "transpiling")](https://ffrank.github.io/features/2016/02/18/from-catalog-to-mgmt/) | -| James Shubin | blog | [Automatic edges in mgmt (...and the pkg resource)](https://ttboj.wordpress.com/2016/03/14/automatic-edges-in-mgmt/) | -| James Shubin | blog | [Automatic grouping in mgmt](https://ttboj.wordpress.com/2016/03/30/automatic-grouping-in-mgmt/) | +| James Shubin | blog | [Automatic edges in mgmt (...and the pkg resource)](https://purpleidea.com/blog/2016/03/14/automatic-edges-in-mgmt/) | +| James Shubin | blog | [Automatic grouping in mgmt](https://purpleidea.com/blog/2016/03/30/automatic-grouping-in-mgmt/) | | John Arundel | tweet | [“Puppet’s days are numbered.”](https://twitter.com/bitfield/status/732157519142002688) | | Felix Frank | blog | [Puppet, Meet Mgmt (on puppet to mgmt internals)](https://ffrank.github.io/features/2016/06/12/puppet,-meet-mgmt/) | | Felix Frank | blog | [Puppet Powered Mgmt (puppet to mgmt tl;dr)](https://ffrank.github.io/features/2016/06/19/puppet-powered-mgmt/) | -| James Shubin | blog | [Automatic clustering in mgmt](https://ttboj.wordpress.com/2016/06/20/automatic-clustering-in-mgmt/) | +| James Shubin | blog | [Automatic clustering in mgmt](https://purpleidea.com/blog/2016/06/20/automatic-clustering-in-mgmt/) | | James Shubin | video | [Recording from CoreOSFest 2016](https://www.youtube.com/watch?v=KVmDCUA42wc&html5=1) | | James Shubin | video | [Recording from DebConf16](http://meetings-archive.debian.net/pub/debian-meetings/2016/debconf16/Next_Generation_Config_Mgmt.webm) ([Slides](https://annex.debconf.org//debconf-share/debconf16/slides/15-next-generation-config-mgmt.pdf)) | | Felix Frank | blog | [Edging It All In (puppet and mgmt edges)](https://ffrank.github.io/features/2016/07/12/edging-it-all-in/) | | Felix Frank | blog | [Translating All The Things (puppet to mgmt translation warnings)](https://ffrank.github.io/features/2016/08/19/translating-all-the-things/) | | James Shubin | video | [Recording from systemd.conf 2016](https://www.youtube.com/watch?v=jB992Zb3nH0&html5=1) | -| James Shubin | blog | [Remote execution in mgmt](https://ttboj.wordpress.com/2016/10/07/remote-execution-in-mgmt/) | +| James Shubin | blog | [Remote execution in mgmt](https://purpleidea.com/blog/2016/10/07/remote-execution-in-mgmt/) | | James Shubin | video | [Recording from High Load Strategy 2016](https://vimeo.com/191493409) | | James Shubin | video | [Recording from NLUUG 2016](https://www.youtube.com/watch?v=MmpwOQAb_SE&html5=1) | -| James Shubin | blog | [Send/Recv in mgmt](https://ttboj.wordpress.com/2016/12/07/sendrecv-in-mgmt/) | +| James Shubin | blog | [Send/Recv in mgmt](https://purpleidea.com/blog/2016/12/07/sendrecv-in-mgmt/) | | Julien Pivotto | blog | [Augeas resource for mgmt](https://roidelapluie.be/blog/2017/02/14/mgmt-augeas/) | -| James Shubin | blog | [Metaparameters in mgmt](https://ttboj.wordpress.com/2017/03/01/metaparameters-in-mgmt/) | +| James Shubin | blog | [Metaparameters in mgmt](https://purpleidea.com/blog/2017/03/01/metaparameters-in-mgmt/) | | James Shubin | video | [Recording from Incontro DevOps 2017](https://vimeo.com/212241877) | | Yves Brissaud | blog | [mgmt aux HumanTalks Grenoble (french)](http://log.winsos.net/2017/04/12/mgmt-aux-human-talks-grenoble.html) | | James Shubin | video | [Recording from OSDC Berlin 2017](https://www.youtube.com/watch?v=LkEtBVLfygE&html5=1) | diff --git a/docs/quick-start-guide.md b/docs/quick-start-guide.md index 48a9087a..0ec9f311 100644 --- a/docs/quick-start-guide.md +++ b/docs/quick-start-guide.md @@ -4,7 +4,7 @@ This guide is intended for developers. Once `mgmt` is minimally viable, we'll publish a quick start guide for users too. If you're brand new to `mgmt`, it's probably a good idea to start by reading the -[introductory article](https://ttboj.wordpress.com/2016/01/18/next-generation-configuration-mgmt/) +[introductory article](https://purpleidea.com/blog/2016/01/18/next-generation-configuration-mgmt/) or to watch an [introductory video](https://www.youtube.com/watch?v=LkEtBVLfygE&html5=1). Once you're familiar with the general idea, please start hacking... @@ -38,14 +38,12 @@ cd $GOPATH/src/github.com/purpleidea/mgmt * Run `make build` to get a freshly built `mgmt` binary. ### Running mgmt -* Run `time ./mgmt run --yaml examples/graph0.yaml --converged-timeout=5 --tmp-prefix` to try out a very simple example! -* To run continuously in the default mode of operation, omit the `--converged-timeout` option. +* Run `time ./mgmt run --lang examples/lang/hello0.mcl --tmp-prefix` to try out a very simple example! * Look in that example file that you ran to see if you can figure out what it did! -* The yaml frontend is provided as a developer tool to test the engine until the language is ready. * Have fun hacking on our future technology and get involved to shape the project! ## Examples -Please look in the [examples/](../examples/) folder for some more examples! +Please look in the [examples/lang/](../examples/lang/) folder for some more examples! ## Vagrant If you would like to avoid doing the above steps manually, we have prepared a diff --git a/docs/resource-guide.md b/docs/resource-guide.md index d90d3800..a34a16b0 100644 --- a/docs/resource-guide.md +++ b/docs/resource-guide.md @@ -16,7 +16,7 @@ Resources in `mgmt` are similar to resources in other systems in that they are uniquely different in that they can detect when their state has changed, and as a result can run to revert or repair this change instantly. For some background on this design, please read the -[original article](https://ttboj.wordpress.com/2016/01/18/next-generation-configuration-mgmt/) +[original article](https://purpleidea.com/blog/2016/01/18/next-generation-configuration-mgmt/) on the subject. ## Resource API @@ -465,14 +465,14 @@ func init() { // special golang method that runs once ``` ## Automatic edges -Automatic edges in `mgmt` are well described in [this article](https://ttboj.wordpress.com/2016/03/14/automatic-edges-in-mgmt/). +Automatic edges in `mgmt` are well described in [this article](https://purpleidea.com/blog/2016/03/14/automatic-edges-in-mgmt/). The best example of this technique can be seen in the `svc` resource. Unfortunately no further documentation about this subject has been written. To expand this section, please send a patch! Please contact us if you'd like to work on a resource that uses this feature, or to add it to an existing one! ## Automatic grouping -Automatic grouping in `mgmt` is well described in [this article](https://ttboj.wordpress.com/2016/03/30/automatic-grouping-in-mgmt/). +Automatic grouping in `mgmt` is well described in [this article](https://purpleidea.com/blog/2016/03/30/automatic-grouping-in-mgmt/). The best example of this technique can be seen in the `pkg` resource. Unfortunately no further documentation about this subject has been written. To expand this section, please send a patch! Please contact us if you'd like to @@ -481,7 +481,7 @@ work on a resource that uses this feature, or to add it to an existing one! ## Send/Recv In `mgmt` there is a novel concept called _Send/Recv_. For some background, -please [read the introductory article](https://ttboj.wordpress.com/2016/12/07/sendrecv-in-mgmt/). +please [read the introductory article](https://purpleidea.com/blog/2016/12/07/sendrecv-in-mgmt/). When using this feature, the engine will automatically send the user specified value to the intended destination without requiring any resource specific code. Any time that one of the destination values is changed, the engine automatically @@ -547,7 +547,7 @@ There are still many ideas for new resources that haven't been written yet. If you'd like to contribute one, please contact us and tell us about your idea! ### Where can I find more information about mgmt? -Additional blog posts, videos and other material [is available!](https://github.com/purpleidea/mgmt/#on-the-web). +Additional blog posts, videos and other material [is available!](https://github.com/purpleidea/mgmt/blob/master/docs/on-the-web.md). ## Suggestions If you have any ideas for API changes or other improvements to resource writing, diff --git a/docs/style-guide.md b/docs/style-guide.md new file mode 100644 index 00000000..2a25d6af --- /dev/null +++ b/docs/style-guide.md @@ -0,0 +1,95 @@ +# Style guide + +## Overview + +This document aims to be a reference for the desired style for patches to mgmt. +In particular it describes conventions which we use which are not officially +enforced by the `gofmt` tool, and which might not be clearly defined elsewhere. +Most of these are common sense to seasoned programmers, and we hope this will be +a useful reference for new programmers. + +There are a lot of useful code review comments described +[here](https://github.com/golang/go/wiki/CodeReviewComments). We don't +necessarily follow everything strictly, but it is in general a very good guide. + +## Basics + +* All of our golang code is formatted with `gofmt`. + +## Comments + +All of our code is commented with the minimums required for `godoc` to function, +and so that our comments pass `golint`. Code comments should either be full +sentences (which end with a period, use proper punctuation, and capitalize the +first word when it is not a lower cased identifier), or are short one-line +comments in the source which are not full sentences and don't end with a period. + +They should explain algorithms, describe non-obvious behaviour, or situations +which would otherwise need explanation or additional research during a code +review. Notes about use of unfamiliar API's is a good idea for a code comment. + +### Example + +Here you can see a function with the correct `godoc` string. The first word must +match the name of the function. It is _not_ capitalized because the function is +private. + +```golang +// square multiplies the input integer by itself and returns this product. +func square(x int) int { + return x * x // we don't care about overflow errors +} +``` + +## Line length + +In general we try to stick to 80 character lines when it is appropriate. It is +almost *always* appropriate for function `godoc` comments and most longer +paragraphs. Exceptions are always allowed based on the will of the maintainer. + +It is usually better to exceed 80 characters than to break code unnecessarily. +If your code often exceeds 80 characters, it might be an indication that it +needs refactoring. + +Occasionally inline, two line source code comments are used within a function. +These should usually be balanced so that you don't have one line with 78 +characters and the second with only four. Split the comment between the two. + +## Method receiver naming + +[Contrary](https://github.com/golang/go/wiki/CodeReviewComments#receiver-names) +to the specialized naming of the method receiver variable, we usually name all +of these `obj` for ease of code copying throughout the project, and for faster +identification when reviewing code. Some anecdotal studies have shown that it +makes the code easier to read since you don't need to remember the name of the +method receiver variable in each different method. This is very similar to what +is done in `python`. + +### Example + +```golang +// Bar does a thing, and returns the number of baz results found in our +database. +func (obj *Foo) Bar(baz string) int { + if len(obj.s) > 0 { + return strings.Count(obj.s, baz) + } + return -1 +} +``` + +## Consistent ordering + +In general we try to preserve a logical ordering in source files which usually +matches the common order of execution that a _lazy evaluator_ would follow. + +This is also the order which is recommended when creating interface types. When +implementing an interface, arrange your methods in the same order that they are +declared in the interface. + +When implementing code for the various types in the language, please follow this +order: `bool`, `str`, `int`, `float`, `list`, `map`, `struct`, `func`. + +## Suggestions +If you have any ideas for suggestions or other improvements to this guide, +please let us know! diff --git a/etcd/client.go b/etcd/client.go new file mode 100644 index 00000000..bbd9b353 --- /dev/null +++ b/etcd/client.go @@ -0,0 +1,94 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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 etcd + +import ( + "time" + + etcd "github.com/coreos/etcd/clientv3" // "clientv3" + errwrap "github.com/pkg/errors" + context "golang.org/x/net/context" +) + +// ClientEtcd provides a simple etcd client for deploy and status operations. +type ClientEtcd struct { + Seeds []string // list of endpoints to try to connect + + client *etcd.Client +} + +// GetClient returns a handle to the raw etcd client object. +func (obj *ClientEtcd) GetClient() *etcd.Client { + return obj.client +} + +// GetConfig returns the config struct to be used for the etcd client connect. +func (obj *ClientEtcd) GetConfig() etcd.Config { + cfg := etcd.Config{ + Endpoints: obj.Seeds, + // RetryDialer chooses the next endpoint to use + // it comes with a default dialer if unspecified + DialTimeout: 5 * time.Second, + } + return cfg +} + +// Connect connects the client to a server, and then builds the *API structs. +// If reconnect is true, it will force a reconnect with new config endpoints. +func (obj *ClientEtcd) Connect() error { + if obj.client != nil { // memoize + return nil + } + + var err error + cfg := obj.GetConfig() + obj.client, err = etcd.New(cfg) // connect! + if err != nil { + return errwrap.Wrapf(err, "client connect error") + } + return nil +} + +// Destroy cleans up the entire etcd client connection. +func (obj *ClientEtcd) Destroy() error { + err := obj.client.Close() + //obj.wg.Wait() + return err +} + +// Get runs a get on the client connection. This has the same signature as our +// EmbdEtcd Get function. +func (obj *ClientEtcd) Get(path string, opts ...etcd.OpOption) (map[string]string, error) { + resp, err := obj.client.Get(context.TODO(), path, opts...) + if err != nil || resp == nil { + return nil, err + } + + // TODO: write a resp.ToMap() function on https://godoc.org/github.com/coreos/etcd/etcdserver/etcdserverpb#RangeResponse + result := make(map[string]string) + for _, x := range resp.Kvs { + result[string(x.Key)] = string(x.Value) + } + return result, nil +} + +// Txn runs a transaction on the client connection. This has the same signature +// as our EmbdEtcd Txn function. +func (obj *ClientEtcd) Txn(ifcmps []etcd.Cmp, thenops, elseops []etcd.Op) (*etcd.TxnResponse, error) { + return obj.client.KV.Txn(context.TODO()).If(ifcmps...).Then(thenops...).Else(elseops...).Commit() +} diff --git a/etcd/deploy.go b/etcd/deploy.go new file mode 100644 index 00000000..b44bcf28 --- /dev/null +++ b/etcd/deploy.go @@ -0,0 +1,171 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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 etcd + +import ( + "fmt" + "strconv" + "strings" + + etcd "github.com/coreos/etcd/clientv3" + errwrap "github.com/pkg/errors" +) + +const ( + deployPath = "deploy" + payloadPath = "payload" + hashPath = "hash" +) + +// WatchDeploy returns a channel which spits out events on new deploy activity. +// FIXME: It should close the channel when it's done, and spit out errors when +// something goes wrong. +func WatchDeploy(obj *EmbdEtcd) chan error { + // key structure is $NS/deploy/$id/payload = $data + path := fmt.Sprintf("%s/%s/", NS, deployPath) + ch := make(chan error, 1) + // FIXME: fix our API so that we get a close event on shutdown. + callback := func(re *RE) error { + // TODO: is this even needed? it used to happen on conn errors + //log.Printf("Etcd: Watch: Path: %v", path) // event + if re == nil || re.response.Canceled { + return fmt.Errorf("watch is empty") // will cause a CtxError+retry + } + if len(ch) == 0 { // send event only if one isn't pending + ch <- nil // event + } + return nil + } + _, _ = obj.AddWatcher(path, callback, true, false, etcd.WithPrefix()) // no need to check errors + return ch +} + +// GetDeploys gets all the available deploys. +func GetDeploys(obj Client) (map[uint64]string, error) { + // key structure is $NS/deploy/$id/payload = $data + path := fmt.Sprintf("%s/%s/", NS, deployPath) + keyMap, err := obj.Get(path, etcd.WithPrefix(), etcd.WithSort(etcd.SortByKey, etcd.SortAscend)) + if err != nil { + return nil, errwrap.Wrapf(err, "could not get deploy") + } + result := make(map[uint64]string) + for key, val := range keyMap { + if !strings.HasPrefix(key, path) { // sanity check + continue + } + + str := strings.Split(key[len(path):], "/") + if len(str) != 2 { + return nil, fmt.Errorf("unexpected chunk count of %d", len(str)) + } + if s := str[1]; s != payloadPath { + continue // skip, maybe there are other future additions + } + + var id uint64 + var err error + x := str[0] + if id, err = strconv.ParseUint(x, 10, 64); err != nil { + return nil, fmt.Errorf("invalid id of `%s`", x) + } + + // TODO: do some sort of filtering here? + //log.Printf("Etcd: GetDeploys(%s): Id => Data: %d => %s", key, id, val) + result[id] = val + } + return result, nil +} + +// GetDeploy gets the latest deploy if id == 0, otherwise it returns the deploy +// with the specified id if it exists. +// FIXME: implement this more efficiently so that it doesn't have to download *all* the old deploys from etcd! +func GetDeploy(obj Client, id uint64) (string, error) { + result, err := GetDeploys(obj) + if err != nil { + return "", err + } + if id != 0 { + str, exists := result[id] + if !exists { + return "", fmt.Errorf("can't find id `%d`", id) + } + return str, nil + } + // find the latest id + var max uint64 + for i := range result { + if i > max { + max = i + } + } + if max == 0 { + return "", nil // no results yet + } + return result[max], nil +} + +// AddDeploy adds a new deploy. It takes an id and ensures it's sequential. If +// hash is not empty, then it will check that the pHash matches what the +// previous hash was, and also adds this new hash along side the id. This is +// useful to make sure you get a linear chain of git patches, and to avoid two +// contributors pushing conflicting deploys. This isn't git specific, and so any +// arbitrary string hash can be used. +// FIXME: prune old deploys from the store when they aren't needed anymore... +func AddDeploy(obj Client, id uint64, hash, pHash string, data *string) error { + // key structure is $NS/deploy/$id/payload = $data + // key structure is $NS/deploy/$id/hash = $hash + path := fmt.Sprintf("%s/%s/%d/%s", NS, deployPath, id, payloadPath) + tPath := fmt.Sprintf("%s/%s/%d/%s", NS, deployPath, id, hashPath) + ifs := []etcd.Cmp{} // list matching the desired state + ops := []etcd.Op{} // list of ops in this transaction (then) + + // TODO: use https://github.com/coreos/etcd/pull/7417 if merged + // we're append only, so ensure this unique deploy id doesn't exist + ifs = append(ifs, etcd.Compare(etcd.Version(path), "=", 0)) // KeyMissing + //ifs = append(ifs, etcd.KeyMissing(path)) + + // don't look for previous deploy if this is the first deploy ever + if id > 1 { + // we append sequentially, so ensure previous key *does* exist + prev := fmt.Sprintf("%s/%s/%d/%s", NS, deployPath, id-1, payloadPath) + ifs = append(ifs, etcd.Compare(etcd.Version(prev), ">", 0)) // KeyExists + //ifs = append(ifs, etcd.KeyExists(prev)) + + if hash != "" && pHash != "" { + // does the previously stored hash match what we expect? + prevHash := fmt.Sprintf("%s/%s/%d/%s", NS, deployPath, id-1, hashPath) + ifs = append(ifs, etcd.Compare(etcd.Value(prevHash), "=", pHash)) + } + } + + ops = append(ops, etcd.OpPut(path, *data)) + if hash != "" { + ops = append(ops, etcd.OpPut(tPath, hash)) // store new hash as well + } + + // it's important to do this in one transaction, and atomically, because + // this way, we only generate one watch event, and only when it's needed + result, err := obj.Txn(ifs, ops, nil) + if err != nil { + return errwrap.Wrapf(err, "error creating deploy id %d: %s", id) + } + if !result.Succeeded { + return fmt.Errorf("could not create deploy id %d", id) + } + return nil // success +} diff --git a/etcd/etcd.go b/etcd/etcd.go index 7ba027ae..bad66a1d 100644 --- a/etcd/etcd.go +++ b/etcd/etcd.go @@ -79,14 +79,14 @@ import ( // constant parameters which may need to be tweaked or customized const ( - NS = "_mgmt" // root namespace for mgmt operations - seedSentinel = "_seed" // you must not name your hostname this - MaxStartServerTimeout = 60 // max number of seconds to wait for server to start - MaxStartServerRetries = 3 // number of times to retry starting the etcd server - maxClientConnectRetries = 5 // number of times to retry consecutive connect failures - selfRemoveTimeout = 3 // give unnominated members a chance to self exit - exitDelay = 3 // number of sec of inactivity after exit to clean up - DefaultIdealClusterSize = 5 // default ideal cluster size target for initial seed + NS = "/_mgmt" // root namespace for mgmt operations + seedSentinel = "_seed" // you must not name your hostname this + MaxStartServerTimeout = 60 // max number of seconds to wait for server to start + MaxStartServerRetries = 3 // number of times to retry starting the etcd server + maxClientConnectRetries = 5 // number of times to retry consecutive connect failures + selfRemoveTimeout = 3 // give unnominated members a chance to self exit + exitDelay = 3 // number of sec of inactivity after exit to clean up + DefaultIdealClusterSize = 5 // default ideal cluster size target for initial seed DefaultClientURL = "127.0.0.1:2379" DefaultServerURL = "127.0.0.1:2380" ) @@ -170,11 +170,12 @@ type EmbdEtcd struct { // EMBeddeD etcd ctxErr error // permanent ctx error // exit and cleanup related - cancelLock sync.Mutex // lock for the cancels list - cancels []func() // array of every cancel function for watches - exiting bool - exitchan chan struct{} - exitTimeout <-chan time.Time + cancelLock sync.Mutex // lock for the cancels list + cancels []func() // array of every cancel function for watches + exiting bool + exitchan chan struct{} + exitchanCb chan struct{} + exitwg *sync.WaitGroup // wait for main loops to shutdown hostname string memberID uint64 // cluster membership id of server if running @@ -220,14 +221,15 @@ func NewEmbdEtcd(hostname string, seeds, clientURLs, serverURLs, advertiseClient idealClusterSize = 0 // unset, get from running cluster } obj := &EmbdEtcd{ - exitchan: make(chan struct{}), // exit signal for main loop - exitTimeout: nil, - awq: make(chan *AW), - wevents: make(chan *RE), - setq: make(chan *KV), - getq: make(chan *GQ), - delq: make(chan *DL), - txnq: make(chan *TN), + exitchan: make(chan struct{}), // exit signal for main loop + exitchanCb: make(chan struct{}), + exitwg: &sync.WaitGroup{}, + awq: make(chan *AW), + wevents: make(chan *RE), + setq: make(chan *KV), + getq: make(chan *GQ), + delq: make(chan *DL), + txnq: make(chan *TN), nominated: make(etcdtypes.URLsMap), @@ -265,6 +267,11 @@ func NewEmbdEtcd(hostname string, seeds, clientURLs, serverURLs, advertiseClient return obj } +// GetClient returns a handle to the raw etcd client object for those scenarios. +func (obj *EmbdEtcd) GetClient() *etcd.Client { + return obj.client +} + // GetConfig returns the config struct to be used for the etcd client connect. func (obj *EmbdEtcd) GetConfig() etcd.Config { endpoints := []string{} @@ -363,11 +370,11 @@ func (obj *EmbdEtcd) Startup() error { go obj.Loop() // start main loop // TODO: implement native etcd watcher method on member API changes - path := fmt.Sprintf("/%s/nominated/", NS) + path := fmt.Sprintf("%s/nominated/", NS) go obj.AddWatcher(path, obj.nominateCallback, true, false, etcd.WithPrefix()) // no block // setup ideal cluster size watcher - key := fmt.Sprintf("/%s/idealClusterSize", NS) + key := fmt.Sprintf("%s/idealClusterSize", NS) go obj.AddWatcher(key, obj.idealClusterSizeCallback, true, false) // no block // if we have no endpoints, it means we are bootstrapping... @@ -393,7 +400,7 @@ func (obj *EmbdEtcd) Startup() error { } if !obj.noServer { - path := fmt.Sprintf("/%s/volunteers/", NS) + path := fmt.Sprintf("%s/volunteers/", NS) go obj.AddWatcher(path, obj.volunteerCallback, true, false, etcd.WithPrefix()) // no block } @@ -431,7 +438,7 @@ func (obj *EmbdEtcd) Startup() error { } } - go obj.AddWatcher(fmt.Sprintf("/%s/endpoints/", NS), obj.endpointCallback, true, false, etcd.WithPrefix()) + go obj.AddWatcher(fmt.Sprintf("%s/endpoints/", NS), obj.endpointCallback, true, false, etcd.WithPrefix()) if err := obj.Connect(false); err != nil { // don't exit from this Startup function until connected! return err @@ -461,7 +468,8 @@ func (obj *EmbdEtcd) Destroy() error { } obj.cancelLock.Unlock() - obj.exitchan <- struct{}{} // cause main loop to exit + close(obj.exitchan) // cause main loop to exit + close(obj.exitchanCb) obj.rLock.Lock() if obj.client != nil { @@ -474,6 +482,7 @@ func (obj *EmbdEtcd) Destroy() error { //if obj.server != nil { // return obj.DestroyServer() //} + obj.exitwg.Wait() return nil } @@ -715,12 +724,15 @@ func (obj *EmbdEtcd) CtxError(ctx context.Context, err error) (context.Context, // CbLoop is the loop where callback execution is serialized. func (obj *EmbdEtcd) CbLoop() { + obj.exitwg.Add(1) + defer obj.exitwg.Done() cuid := obj.converger.Register() cuid.SetName("Etcd: CbLoop") defer cuid.Unregister() if e := obj.Connect(false); e != nil { return // fatal } + var exitTimeout <-chan time.Time // = nil is implied // we use this timer because when we ignore un-converge events and loop, // we reset the ConvergedTimer case statement, ruining the timeout math! cuid.StartTimer() @@ -760,8 +772,18 @@ func (obj *EmbdEtcd) CbLoop() { log.Printf("Trace: Etcd: CbLoop: Event: FinishLoop") } + // exit loop signal + case <-obj.exitchanCb: + obj.exitchanCb = nil + log.Println("Etcd: Exiting loop shortly...") + // activate exitTimeout switch which only opens after N + // seconds of inactivity in this select switch, which + // lets everything get bled dry to avoid blocking calls + // which would otherwise block us from exiting cleanly! + exitTimeout = util.TimeAfterOrBlock(exitDelay) + // exit loop commit - case <-obj.exitTimeout: + case <-exitTimeout: log.Println("Etcd: Exiting callback loop!") cuid.StopTimer() // clean up nicely return @@ -771,12 +793,15 @@ func (obj *EmbdEtcd) CbLoop() { // Loop is the main loop where everything is serialized. func (obj *EmbdEtcd) Loop() { + obj.exitwg.Add(1) // TODO: add these to other go routines? + defer obj.exitwg.Done() cuid := obj.converger.Register() cuid.SetName("Etcd: Loop") defer cuid.Unregister() if e := obj.Connect(false); e != nil { return // fatal } + var exitTimeout <-chan time.Time // = nil is implied cuid.StartTimer() for { ctx := context.Background() // TODO: inherit as input argument? @@ -911,15 +936,16 @@ func (obj *EmbdEtcd) Loop() { // exit loop signal case <-obj.exitchan: + obj.exitchan = nil log.Println("Etcd: Exiting loop shortly...") // activate exitTimeout switch which only opens after N // seconds of inactivity in this select switch, which // lets everything get bled dry to avoid blocking calls // which would otherwise block us from exiting cleanly! - obj.exitTimeout = util.TimeAfterOrBlock(exitDelay) + exitTimeout = util.TimeAfterOrBlock(exitDelay) // exit loop commit - case <-obj.exitTimeout: + case <-exitTimeout: log.Println("Etcd: Exiting loop!") cuid.StopTimer() // clean up nicely return @@ -1597,7 +1623,7 @@ func (obj *EmbdEtcd) idealClusterSizeCallback(re *RE) error { log.Printf("Trace: Etcd: idealClusterSizeCallback()") defer log.Printf("Trace: Etcd: idealClusterSizeCallback(): Finished!") } - path := fmt.Sprintf("/%s/idealClusterSize", NS) + path := fmt.Sprintf("%s/idealClusterSize", NS) for _, event := range re.response.Events { if key := bytes.NewBuffer(event.Kv.Key).String(); key != path { continue diff --git a/etcd/fs/file.go b/etcd/fs/file.go new file mode 100644 index 00000000..00c6490b --- /dev/null +++ b/etcd/fs/file.go @@ -0,0 +1,543 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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 fs + +import ( + "bytes" + "encoding/gob" + "fmt" + "io" + "log" + "os" + "path" + "strings" + "syscall" + "time" + + etcd "github.com/coreos/etcd/clientv3" // "clientv3" + errwrap "github.com/pkg/errors" +) + +func init() { + gob.Register(&File{}) +} + +// File represents a file node. This is the node of our tree structure. This is +// not thread safe, and you can have at most one open file handle at a time. +type File struct { + // FIXME: add a rwmutex to make this thread safe + fs *Fs // pointer to file system + + Path string // relative path to file, trailing slash if it's a directory + Mode os.FileMode + ModTime time.Time + //Size int64 // XXX: cache the size to avoid full file downloads for stat! + + Children []*File // dir's use this + Hash string // string not []byte so it's readable, matches data + + data []byte // cache of the data. private so it doesn't get encoded + cursor int64 + dirCursor int64 + + readOnly bool // is the file read-only? + closed bool // is file closed? +} + +// path returns the expected path to the actual file in etcd. +func (obj *File) path() string { + // keys are prefixed with the hash-type eg: {sha256} to allow different + // superblocks to share the same data prefix even with different hashes + return fmt.Sprintf("%s/{%s}%s", obj.fs.sb.DataPrefix, obj.fs.Hash, obj.Hash) +} + +// cache downloads the file contents from etcd and stores them in our cache. +func (obj *File) cache() error { + if obj.Mode.IsDir() { + return nil + } + + h, err := obj.fs.hash(obj.data) // update hash + if err != nil { + return err + } + if h == obj.Hash { // we already have the correct data cached + return nil + } + + p := obj.path() // get file data from this path in etcd + + result, err := obj.fs.get(p) // download the file... + if err != nil { + return err + } + if result == nil || len(result) == 0 { // nothing found + return err + } + data, exists := result[p] + if !exists { + return fmt.Errorf("could not find data") // programming error? + } + obj.data = data // save + return nil +} + +// findNode is the "in array" equivalent for searching through a dir's children. +// You must *not* specify an absolute path as the search string, but rather you +// should specify the name. To search for something name "bar" inside a dir +// named "/tmp/foo/", you just pass in "bar", not "/tmp/foo/bar". +func (obj *File) findNode(name string) (*File, bool) { + for _, node := range obj.Children { + if name == node.Path { + return node, true // found + } + } + return nil, false // not found +} + +func fileCreate(fs *Fs, name string) (*File, error) { + if name == "" { + return nil, fmt.Errorf("invalid input path") + } + if !strings.HasPrefix(name, "/") { + return nil, fmt.Errorf("invalid input path (not absolute)") + } + cleanPath := path.Clean(name) // remove possible trailing slashes + + // try to add node to tree by first finding the parent node + parentPath, filePath := path.Split(cleanPath) // looking for this + + node, err := fs.find(parentPath) + if err != nil { // might be ErrNotExist + return nil, err + } + + fi, err := node.Stat() + if err != nil { + return nil, err + } + if !fi.IsDir() { // is the parent a suitable home? + return nil, &os.PathError{Op: "create", Path: name, Err: syscall.ENOTDIR} + } + + f, exists := node.findNode(filePath) // does file already exist inside? + if exists { // already exists, overwrite! + if err := f.Truncate(0); err != nil { + return nil, err + } + return f, nil + } + + data := []byte("") // empty file contents + h, err := fs.hash(data) // TODO: use memoized value? + if err != nil { + return &File{}, err // TODO: nil instead? + } + + f = &File{ + fs: fs, + Path: filePath, // the relative path chunk (not incl. dir name) + Hash: h, + data: data, + } + + // add to parent + node.Children = append(node.Children, f) + + // push new file up if not on server, and then push up the metadata + if err := f.Sync(); err != nil { + return f, err // TODO: ok to return the file so user can run sync? + } + + return f, nil +} + +func fileOpen(fs *Fs, name string) (*File, error) { + if name == "" { + return nil, fmt.Errorf("invalid input path") + } + if !strings.HasPrefix(name, "/") { + return nil, fmt.Errorf("invalid input path (not absolute)") + } + cleanPath := path.Clean(name) // remove possible trailing slashes + + node, err := fs.find(cleanPath) + if err != nil { // might be ErrNotExist + return &File{}, err // TODO: nil instead? + } + + // download file contents into obj.data + if err := node.cache(); err != nil { + return &File{}, err // TODO: nil instead? + } + + //fi, err := node.Stat() + //if err != nil { + // return nil, err + //} + //if fi.IsDir() { // can we open a directory? - yes we can apparently + // return nil, fmt.Errorf("file is a directory") + //} + + node.readOnly = true // as per docs, fileOpen opens files as read-only + node.closed = false // as per docs, fileOpen opens files as read-only + + return node, nil +} + +// Close closes the file handle. This will try and run Sync automatically. +func (obj *File) Close() error { + if !obj.readOnly { + obj.ModTime = time.Now() + } + + if err := obj.Sync(); err != nil { + return err + } + + // FIXME: there is a big implementation mistake between the metadata + // node and the file handle, since they're currently sharing a struct! + + // invalidate all of the fields + //obj.fs = nil + + //obj.Path = "" + //obj.Mode = os.FileMode(0) + //obj.ModTime = time.Time{} + + //obj.Children = nil + //obj.Hash = "" + + //obj.data = nil + obj.cursor = 0 + obj.readOnly = false + + obj.closed = true + return nil +} + +// Name returns the path of the file. +func (obj *File) Name() string { + return obj.Path +} + +// Stat returns some information about the file. +func (obj *File) Stat() (os.FileInfo, error) { + // download file contents into obj.data + if err := obj.cache(); err != nil { // needed so Size() works correctly + return nil, err + } + return &FileInfo{ // everything is actually stored in the main file node + file: obj, + }, nil +} + +// Sync flushes the file contents to the server and calls the filesystem +// metadata sync as well. +// FIXME: instead of a txn, run a get and then a put in two separate stages. if +// the get already found the data up there, then we don't need to push it all in +// the put phase. with the txn it is always all sent up even if the put is never +// needed. the get should just be a "key exists" test, and not a download of the +// whole file. if we *do* do the download, we can byte-by-byte check for hash +// collisions and panic if we find one :) +func (obj *File) Sync() error { + if obj.closed { + return ErrFileClosed + } + + p := obj.path() // store file data at this path in etcd + + // TODO: use https://github.com/coreos/etcd/pull/7417 if merged + cmp := etcd.Compare(etcd.Version(p), "=", 0) // KeyMissing + //cmp := etcd.KeyMissing(p)) + + op := etcd.OpPut(p, string(obj.data)) // this pushes contents to server + + // it's important to do this in one transaction, and atomically, because + // this way, we only generate one watch event, and only when it's needed + result, err := obj.fs.txn([]etcd.Cmp{cmp}, []etcd.Op{op}, nil) + if err != nil { + return errwrap.Wrapf(err, "sync error with: %s (%s)", obj.Path, p) + } + if !result.Succeeded { + if obj.fs.Debug { + log.Printf("debug: data already exists in storage") + } + } + + if err := obj.fs.sync(); err != nil { // push metadata up to server + return err + } + return nil +} + +// Truncate trims the file to the requested size. Since our file system can only +// read and write data, but never edit existing data blocks, doing this will not +// cause more space to be available. +func (obj *File) Truncate(size int64) error { + if obj.closed { + return ErrFileClosed + } + if obj.readOnly { + return &os.PathError{Op: "truncate", Path: obj.Path, Err: ErrFileReadOnly} + } + if size < 0 { + return ErrOutOfRange + } + + if size > 0 { // if size == 0, we don't need to run cache! + // download file contents into obj.data + if err := obj.cache(); err != nil { + return err + } + } + + if size > int64(len(obj.data)) { + diff := size - int64(len(obj.data)) + obj.data = append(obj.data, bytes.Repeat([]byte{00}, int(diff))...) + } else { + obj.data = obj.data[0:size] + } + + h, err := obj.fs.hash(obj.data) // update hash + if err != nil { + return err + } + obj.Hash = h + obj.ModTime = time.Now() + + // this pushes the new data and metadata up to etcd + if err := obj.Sync(); err != nil { + return err + } + return nil +} + +// Read reads up to len(b) bytes from the File. It returns the number of bytes +// read and any error encountered. At end of file, Read returns 0, io.EOF. +// NOTE: This reads into the byte input. It's a side effect! +func (obj *File) Read(b []byte) (n int, err error) { + if obj.closed { + return 0, ErrFileClosed + } + if obj.Mode.IsDir() { + return 0, fmt.Errorf("file is a directory") + } + + // download file contents into obj.data + if err := obj.cache(); err != nil { + return 0, err // TODO: -1 ? + } + + // TODO: can we optimize by reading just the length from etcd, and also + // by only downloading the data range we're interested in? + if len(b) > 0 && int(obj.cursor) == len(obj.data) { + return 0, io.EOF + } + if len(obj.data)-int(obj.cursor) >= len(b) { + n = len(b) + } else { + n = len(obj.data) - int(obj.cursor) + } + copy(b, obj.data[obj.cursor:obj.cursor+int64(n)]) // store into input b + obj.cursor = obj.cursor + int64(n) // update cursor + + return +} + +// ReadAt reads len(b) bytes from the File starting at byte offset off. It +// returns the number of bytes read and the error, if any. ReadAt always returns +// a non-nil error when n < len(b). At end of file, that error is io.EOF. +func (obj *File) ReadAt(b []byte, off int64) (n int, err error) { + obj.cursor = off + return obj.Read(b) +} + +// Readdir lists the contents of the directory and returns a list of file info +// objects for each entry. +func (obj *File) Readdir(count int) ([]os.FileInfo, error) { + if !obj.Mode.IsDir() { + return nil, &os.PathError{Op: "readdir", Path: obj.Name(), Err: syscall.ENOTDIR} + } + + children := obj.Children[obj.dirCursor:] // available children to output + var l = int64(len(children)) // initially assume to return them all + var err error + + // for count > 0, if we return the last entry, also return io.EOF + if count > 0 { + l = int64(count) // initial assumption + if c := len(children); count >= c { + l = int64(c) + err = io.EOF // this result includes the last dir entry + } + } + obj.dirCursor += l // store our progress + + output := make([]os.FileInfo, l) + // TODO: should this be sorted by "directory order" what does that mean? + // from `man 3 readdir`: "unlikely that the names will be sorted" + for i := range output { + output[i] = &FileInfo{ + file: children[i], + } + } + + // we're seen the whole directory, so reset the cursor + if err == io.EOF || count <= 0 { + obj.dirCursor = 0 // TODO: is it okay to reset the cursor? + } + + return output, err +} + +// Readdirnames returns a list of name is the current file handle's directory. +// TODO: this implementation shares the dirCursor with Readdir, is this okay? +// TODO: should Readdirnames even use a dirCursor at all? +func (obj *File) Readdirnames(n int) (names []string, _ error) { + fis, err := obj.Readdir(n) + if fis != nil { + for i, x := range fis { + if x != nil { + names = append(names, fis[i].Name()) + } + } + } + return names, err +} + +// Seek sets the offset for the next Read or Write on file to offset, +// interpreted according to whence: 0 means relative to the origin of the file, +// 1 means relative to the current offset, and 2 means relative to the end. It +// returns the new offset and an error, if any. The behavior of Seek on a file +// opened with O_APPEND is not specified. +func (obj *File) Seek(offset int64, whence int) (int64, error) { + if obj.closed { + return 0, ErrFileClosed + } + + switch whence { + case io.SeekStart: // 0 + obj.cursor = offset + case io.SeekCurrent: // 1 + obj.cursor += offset + case io.SeekEnd: // 2 + // download file contents into obj.data + if err := obj.cache(); err != nil { + return 0, err // TODO: -1 ? + } + obj.cursor = int64(len(obj.data)) + offset + } + return obj.cursor, nil +} + +// Write writes to the given file. +func (obj *File) Write(b []byte) (n int, err error) { + if obj.closed { + return 0, ErrFileClosed + } + if obj.readOnly { + return 0, &os.PathError{Op: "write", Path: obj.Path, Err: ErrFileReadOnly} + } + + // download file contents into obj.data + if err := obj.cache(); err != nil { + return 0, err // TODO: -1 ? + } + + // calculate the write + n = len(b) + cur := obj.cursor + diff := cur - int64(len(obj.data)) + + var tail []byte + if n+int(cur) < len(obj.data) { + tail = obj.data[n+int(cur):] + } + + if diff > 0 { + obj.data = append(bytes.Repeat([]byte{00}, int(diff)), b...) + obj.data = append(obj.data, tail...) + } else { + obj.data = append(obj.data[:cur], b...) + obj.data = append(obj.data, tail...) + } + + h, err := obj.fs.hash(obj.data) // update hash + if err != nil { + return 0, err // TODO: -1 ? + } + obj.Hash = h + obj.ModTime = time.Now() + + // this pushes the new data and metadata up to etcd + if err := obj.Sync(); err != nil { + return 0, err // TODO: -1 ? + } + + obj.cursor = int64(len(obj.data)) + return +} + +// WriteAt writes into the given file at a certain offset. +func (obj *File) WriteAt(b []byte, off int64) (n int, err error) { + obj.cursor = off + return obj.Write(b) +} + +// WriteString writes a string to the file. +func (obj *File) WriteString(s string) (n int, err error) { + return obj.Write([]byte(s)) +} + +// FileInfo is a struct which provides some information about a file handle. +type FileInfo struct { + file *File // anonymous pointer to the actual file +} + +// Name returns the base name of the file. +func (obj *FileInfo) Name() string { + return obj.file.Name() +} + +// Size returns the length in bytes. +func (obj *FileInfo) Size() int64 { + return int64(len(obj.file.data)) +} + +// Mode returns the file mode bits. +func (obj *FileInfo) Mode() os.FileMode { + return obj.file.Mode +} + +// ModTime returns the modification time. +func (obj *FileInfo) ModTime() time.Time { + return obj.file.ModTime +} + +// IsDir is an abbreviation for Mode().IsDir(). +func (obj *FileInfo) IsDir() bool { + //return obj.file.Mode&os.ModeDir != 0 + return obj.file.Mode.IsDir() +} + +// Sys returns the underlying data source (can return nil). +func (obj *FileInfo) Sys() interface{} { + return nil // TODO: should we do something better? + //return obj.file.fs // TODO: would this work? +} diff --git a/etcd/fs/fs.go b/etcd/fs/fs.go new file mode 100644 index 00000000..fc90c0aa --- /dev/null +++ b/etcd/fs/fs.go @@ -0,0 +1,821 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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 fs implements a very simple and limited file system on top of etcd. +package fs + +import ( + "bytes" + "crypto/sha256" + "encoding/gob" + "encoding/hex" + "errors" + "fmt" + "hash" + "io" + "log" + "os" + "path" + "strings" + "syscall" + "time" + + etcd "github.com/coreos/etcd/clientv3" // "clientv3" + rpctypes "github.com/coreos/etcd/etcdserver/api/v3rpc/rpctypes" + errwrap "github.com/pkg/errors" + "github.com/spf13/afero" + context "golang.org/x/net/context" +) + +func init() { + gob.Register(&superBlock{}) +} + +const ( + // EtcdTimeout is the timeout to wait before erroring. + EtcdTimeout = 5 * time.Second // FIXME: chosen arbitrarily + // DefaultDataPrefix is the default path for data storage in etcd. + DefaultDataPrefix = "/_etcdfs/data" + // DefaultHash is the default hashing algorithm to use. + DefaultHash = "sha256" + // PathSeparator is the path separator to use on this filesystem. + PathSeparator = os.PathSeparator // usually the slash character +) + +// TODO: https://dave.cheney.net/2016/04/07/constant-errors +var ( + IsPathSeparator = os.IsPathSeparator + + // ErrNotImplemented is returned when something is not implemented by design. + ErrNotImplemented = errors.New("not implemented") + + // ErrExist is returned when requested path already exists. + ErrExist = os.ErrExist + + // ErrNotExist is returned when we can't find the requested path. + ErrNotExist = os.ErrNotExist + + ErrFileClosed = errors.New("File is closed") + ErrFileReadOnly = errors.New("File handle is read only") + ErrOutOfRange = errors.New("Out of range") +) + +// Fs is a specialized afero.Fs implementation for etcd. It implements a small +// subset of the features, and has some special properties. In particular, file +// data is stored with it's unique reference being a hash of the data. In this +// way, you cannot actually edit a file, but rather you create a new one, and +// update the metadata pointer to point to the new blob. This might seem slow, +// but it has the unique advantage of being relatively straight forward to +// implement, and repeated uploads of the same file cost almost nothing. Since +// etcd isn't meant for large file systems, this fits the desired use case. +// This implementation is designed to have a single writer for each superblock, +// but as many readers as you like. +// FIXME: this is not currently thread-safe, nor is it clear if it needs to be. +// XXX: we probably aren't updating the modification time everywhere we should! +// XXX: because we never delete data blocks, we need to occasionally "vacuum". +// XXX: this is harder because we need to list of *all* metadata paths, if we +// want them to be able to share storage backends. (we do) +type Fs struct { + Client *etcd.Client + + Metadata string // location of "superblock" for this filesystem + + DataPrefix string // prefix of data storage (no trailing slashes) + Hash string // eg: sha256 + + Debug bool + + sb *superBlock + mounted bool +} + +// superBlock is the metadata structure of everything stored outside of the data +// section in etcd. Its fields need to be exported or they won't get marshalled. +type superBlock struct { + DataPrefix string // prefix of data storage + Hash string // hashing algorithm used + + Tree *File // filesystem tree +} + +// NewEtcdFs creates a new filesystem handle on an etcd client connection. You +// must specify the metadata string that you wish to use. +func NewEtcdFs(client *etcd.Client, metadata string) afero.Fs { + return &Fs{ + Client: client, + Metadata: metadata, + } +} + +// get a number of values from etcd. +func (obj *Fs) get(path string, opts ...etcd.OpOption) (map[string][]byte, error) { + ctx, cancel := context.WithTimeout(context.Background(), EtcdTimeout) + resp, err := obj.Client.Get(ctx, path, opts...) + cancel() + if err != nil || resp == nil { + return nil, err + } + + // TODO: write a resp.ToMap() function on https://godoc.org/github.com/coreos/etcd/etcdserver/etcdserverpb#RangeResponse + result := make(map[string][]byte) // formerly: map[string][]byte + for _, x := range resp.Kvs { + result[string(x.Key)] = x.Value // formerly: bytes.NewBuffer(x.Value).String() + } + + return result, nil +} + +// put a value into etcd. +func (obj *Fs) put(path string, data []byte, opts ...etcd.OpOption) error { + ctx, cancel := context.WithTimeout(context.Background(), EtcdTimeout) + _, err := obj.Client.Put(ctx, path, string(data), opts...) // TODO: obj.Client.KV ? + cancel() + if err != nil { + switch err { + case context.Canceled: + return errwrap.Wrapf(err, "ctx canceled") + case context.DeadlineExceeded: + return errwrap.Wrapf(err, "ctx deadline exceeded") + case rpctypes.ErrEmptyKey: + return errwrap.Wrapf(err, "client-side error") + default: + return errwrap.Wrapf(err, "invalid endpoints") + } + } + return nil +} + +// txn runs a txn in etcd. +func (obj *Fs) txn(ifcmps []etcd.Cmp, thenops, elseops []etcd.Op) (*etcd.TxnResponse, error) { + ctx, cancel := context.WithTimeout(context.Background(), EtcdTimeout) + resp, err := obj.Client.Txn(ctx).If(ifcmps...).Then(thenops...).Else(elseops...).Commit() + cancel() + return resp, err +} + +// hash is a small helper that does the hashing for us. +func (obj *Fs) hash(input []byte) (string, error) { + var h hash.Hash + switch obj.Hash { + // TODO: add other hashes + case "sha256": + h = sha256.New() + default: + return "", fmt.Errorf("hash does not exist") + } + src := bytes.NewReader(input) + if _, err := io.Copy(h, src); err != nil { + return "", err + } + return hex.EncodeToString(h.Sum(nil)), nil +} + +// sync overwrites the superblock with whatever version we have stored. +func (obj *Fs) sync() error { + b := bytes.Buffer{} + e := gob.NewEncoder(&b) + err := e.Encode(&obj.sb) // pass with & + if err != nil { + return errwrap.Wrapf(err, "gob failed to encode") + } + //base64.StdEncoding.EncodeToString(b.Bytes()) + return obj.put(obj.Metadata, b.Bytes()) +} + +// mount downloads the initial cache of metadata, including the *file tree. +// Since there's no explicit mount API in the afero.Fs interface, we hide this +// method inside any operation that might do any real work, and make it +// idempotent so that it can be called as much as we want. If there's no +// metadata found (superblock) then we create one. +func (obj *Fs) mount() error { + if obj.mounted { + return nil + } + + result, err := obj.get(obj.Metadata) // download the metadata... + if err != nil { + return err + } + if result == nil || len(result) == 0 { // nothing found, create the fs + if obj.Debug { + log.Printf("debug: mount: creating new fs at: %s", obj.Metadata) + } + // trim any trailing slashes from DataPrefix + for strings.HasSuffix(obj.DataPrefix, "/") { + obj.DataPrefix = strings.TrimSuffix(obj.DataPrefix, "/") + } + if obj.DataPrefix == "" { + obj.DataPrefix = DefaultDataPrefix + } + if obj.Hash == "" { + obj.Hash = DefaultHash + } + // test run an empty string to see if our hash selection works! + if _, err := obj.hash([]byte("")); err != nil { + return fmt.Errorf("cannot hash with %s", obj.Hash) + } + + obj.sb = &superBlock{ + DataPrefix: obj.DataPrefix, + Hash: obj.Hash, + Tree: &File{ // include a root directory + fs: obj, + Path: "", // root dir is "" (empty string) + Mode: os.ModeDir, + }, + } + if err := obj.sync(); err != nil { + return err + } + + obj.mounted = true + return nil + } + + if obj.Debug { + log.Printf("debug: mount: opening old fs at: %s", obj.Metadata) + } + sb, exists := result[obj.Metadata] + if !exists { + return fmt.Errorf("could not find metadata") // programming error? + } + + // decode into obj.sb + //bb, err := base64.StdEncoding.DecodeString(str) + //if err != nil { + // return errwrap.Wrapf(err, "base64 failed to decode") + //} + //b := bytes.NewBuffer(bb) + b := bytes.NewBuffer(sb) + d := gob.NewDecoder(b) + if err := d.Decode(&obj.sb); err != nil { // pass with & + return errwrap.Wrapf(err, "gob failed to decode") + } + + if obj.DataPrefix != "" && obj.DataPrefix != obj.sb.DataPrefix { + return fmt.Errorf("the DataPrefix mount option `%s` does not match the remote value of `%s`", obj.DataPrefix, obj.sb.DataPrefix) + } + if obj.Hash != "" && obj.Hash != obj.sb.Hash { + return fmt.Errorf("the Hash mount option `%s` does not match the remote value of `%s`", obj.Hash, obj.sb.Hash) + } + // if all checks passed, copy these values down locally + obj.DataPrefix = obj.sb.DataPrefix + obj.Hash = obj.sb.Hash + + // hook up file system pointers to each element in the tree structure + obj.traverse(obj.sb.Tree) + + obj.mounted = true + return nil +} + +// traverse adds the file system pointer to each element in the tree structure. +func (obj *Fs) traverse(node *File) { + if node == nil { + return + } + node.fs = obj + for _, n := range node.Children { + obj.traverse(n) + } +} + +// find returns the file node corresponding to this absolute path if it exists. +func (obj *Fs) find(absPath string) (*File, error) { // TODO: function naming? + if absPath == "" { + return nil, fmt.Errorf("empty path specified") + } + if !strings.HasPrefix(absPath, "/") { + return nil, fmt.Errorf("invalid input path (not absolute)") + } + + node := obj.sb.Tree + if node == nil { + return nil, ErrNotExist // no nodes exist yet, not even root dir + } + + var x string // first value + sp := PathSplit(absPath) + if x, sp = sp[0], sp[1:]; x != node.Path { + return nil, fmt.Errorf("root values do not match") // TODO: panic? + } + + for _, p := range sp { + n, exists := node.findNode(p) + if !exists { + return nil, ErrNotExist + } + node = n // descend into this node + } + + return node, nil +} + +// Name returns the name of this filesystem. +func (obj *Fs) Name() string { return "etcdfs" } + +// URI returns a URI representing this particular filesystem. +func (obj *Fs) URI() string { + return fmt.Sprintf("%s://%s", obj.Name(), obj.Metadata) +} + +// Create creates a new file. +func (obj *Fs) Create(name string) (afero.File, error) { + if err := obj.mount(); err != nil { + return nil, err + } + return fileCreate(obj, name) +} + +// Mkdir makes a new directory. +func (obj *Fs) Mkdir(name string, perm os.FileMode) error { + if err := obj.mount(); err != nil { + return err + } + if name == "" { + return fmt.Errorf("invalid input path") + } + if !strings.HasPrefix(name, "/") { + return fmt.Errorf("invalid input path (not absolute)") + } + + // remove possible trailing slashes + cleanPath := path.Clean(name) + + for strings.HasSuffix(cleanPath, "/") { // bonus clean for "/" as input + cleanPath = strings.TrimSuffix(cleanPath, "/") + } + + if cleanPath == "" { + if obj.sb.Tree == nil { + return fmt.Errorf("woops, missing root directory") + } + return ErrExist // root directory already exists + } + + // try to add node to tree by first finding the parent node + parentPath, dirPath := path.Split(cleanPath) // looking for this + + f := &File{ + fs: obj, + Path: dirPath, + Mode: os.ModeDir, + // TODO: add perm to struct or let chmod below do it + } + + node, err := obj.find(parentPath) + if err != nil { // might be ErrNotExist + return err + } + + fi, err := node.Stat() + if err != nil { + return err + } + if !fi.IsDir() { // is the parent a suitable home? + return &os.PathError{Op: "mkdir", Path: name, Err: syscall.ENOTDIR} + } + + _, exists := node.findNode(dirPath) // does file already exist inside? + if exists { + return ErrExist + } + + // add to parent + node.Children = append(node.Children, f) + + // push new file up if not on server, and then push up the metadata + if err := f.Sync(); err != nil { + return err + } + + return obj.Chmod(name, perm) +} + +// MkdirAll creates a directory named path, along with any necessary parents, +// and returns nil, or else returns an error. The permission bits perm are used +// for all directories that MkdirAll creates. If path is already a directory, +// MkdirAll does nothing and returns nil. +func (obj *Fs) MkdirAll(path string, perm os.FileMode) error { + if err := obj.mount(); err != nil { + return err + } + // Copied mostly verbatim from golang stdlib. + // Fast path: if we can tell whether path is a directory or file, stop + // with success or error. + dir, err := obj.Stat(path) + if err == nil { + if dir.IsDir() { + return nil + } + return &os.PathError{Op: "mkdir", Path: path, Err: syscall.ENOTDIR} + } + + // Slow path: make sure parent exists and then call Mkdir for path. + i := len(path) + for i > 0 && IsPathSeparator(path[i-1]) { // Skip trailing path separator. + i-- + } + + j := i + for j > 0 && !IsPathSeparator(path[j-1]) { // Scan backward over element. + j-- + } + + if j > 1 { + // Create parent + err = obj.MkdirAll(path[0:j-1], perm) + if err != nil { + return err + } + } + + // Parent now exists; invoke Mkdir and use its result. + err = obj.Mkdir(path, perm) + if err != nil { + // Handle arguments like "foo/." by + // double-checking that directory doesn't exist. + dir, err1 := obj.Lstat(path) + if err1 == nil && dir.IsDir() { + return nil + } + return err + } + return nil +} + +// Open opens a path. It will be opened read-only. +func (obj *Fs) Open(name string) (afero.File, error) { + if err := obj.mount(); err != nil { + return nil, err + } + return fileOpen(obj, name) // this opens as read-only +} + +// OpenFile opens a path with a particular flag and permission. +func (obj *Fs) OpenFile(name string, flag int, perm os.FileMode) (afero.File, error) { + if err := obj.mount(); err != nil { + return nil, err + } + + chmod := false + f, err := fileOpen(obj, name) + if os.IsNotExist(err) && (flag&os.O_CREATE > 0) { + f, err = fileCreate(obj, name) + chmod = true + } + if err != nil { + return nil, err + } + f.readOnly = (flag == os.O_RDONLY) + + if flag&os.O_APPEND > 0 { + if _, err := f.Seek(0, os.SEEK_END); err != nil { + f.Close() + return nil, err + } + } + if flag&os.O_TRUNC > 0 && flag&(os.O_RDWR|os.O_WRONLY) > 0 { + if err := f.Truncate(0); err != nil { + f.Close() + return nil, err + } + } + if chmod { + // TODO: the golang stdlib doesn't check this error, should we? + if err := obj.Chmod(name, perm); err != nil { + return f, err // TODO: should we return the file handle? + } + } + return f, nil +} + +// Remove removes a path. +func (obj *Fs) Remove(name string) error { + if err := obj.mount(); err != nil { + return err + } + if name == "" { + return fmt.Errorf("invalid input path") + } + if !strings.HasPrefix(name, "/") { + return fmt.Errorf("invalid input path (not absolute)") + } + + // remove possible trailing slashes + cleanPath := path.Clean(name) + + for strings.HasSuffix(cleanPath, "/") { // bonus clean for "/" as input + cleanPath = strings.TrimSuffix(cleanPath, "/") + } + + if cleanPath == "" { + return fmt.Errorf("can't remove root") + } + + f, err := obj.find(name) // get the file + if err != nil { + return err + } + + if len(f.Children) > 0 { // this file or dir has children, can't remove! + return &os.PathError{Op: "remove", Path: name, Err: syscall.ENOTEMPTY} + } + + // find the parent node + parentPath, filePath := path.Split(cleanPath) // looking for this + + node, err := obj.find(parentPath) + if err != nil { // might be ErrNotExist + if os.IsNotExist(err) { // race! must have just disappeared + return nil + } + return err + } + + var index = -1 // int + for i, n := range node.Children { + if n.Path == filePath { + index = i // found here! + break + } + } + if index == -1 { + return fmt.Errorf("programming error") + } + // remove from list + node.Children = append(node.Children[:index], node.Children[index+1:]...) + return obj.sync() +} + +// RemoveAll removes path and any children it contains. It removes everything it +// can but returns the first error it encounters. If the path does not exist, +// RemoveAll returns nil (no error). +func (obj *Fs) RemoveAll(path string) error { + if err := obj.mount(); err != nil { + return err + } + + // Simple case: if Remove works, we're done. + err := obj.Remove(path) + if err == nil || os.IsNotExist(err) { + return nil + } + + // Otherwise, is this a directory we need to recurse into? + dir, serr := obj.Lstat(path) + if serr != nil { + // TODO: I didn't check this logic thoroughly (edge cases?) + if serr, ok := serr.(*os.PathError); ok && (os.IsNotExist(serr.Err) || serr.Err == syscall.ENOTDIR) { + return nil + } + return serr + } + if !dir.IsDir() { + // Not a directory; return the error from Remove. + return err + } + + // Directory. + fd, err := obj.Open(path) + if err != nil { + if os.IsNotExist(err) { + // Race. It was deleted between the Lstat and Open. + // Return nil per RemoveAll's docs. + return nil + } + return err + } + + // Remove contents & return first error. + err = nil + for { + // TODO: why not do this in one shot? is there a syscall limit? + names, err1 := fd.Readdirnames(100) + for _, name := range names { + err1 := obj.RemoveAll(path + string(PathSeparator) + name) + if err == nil { + err = err1 + } + } + if err1 == io.EOF { + break + } + // If Readdirnames returned an error, use it. + if err == nil { + err = err1 + } + if len(names) == 0 { + break + } + } + + // Close directory, because windows won't remove opened directory. + fd.Close() + + // Remove directory. + err1 := obj.Remove(path) + if err1 == nil || os.IsNotExist(err1) { + return nil + } + if err == nil { + err = err1 + } + return err +} + +// Rename moves or renames a file or directory. +// TODO: seems it's okay to move files or directories, but you can't clobber dirs +// but you can clobber single files. a dir can't clobber a file and a file can't +// clobber a dir. but a file can clobber another file but a dir can't clobber +// another dir. you can also transplant dirs or files into other dirs. +func (obj *Fs) Rename(oldname, newname string) error { + // XXX: do we need to check if dest path is inside src path? + // XXX: if dirs/files are next to each other, do we mess up the .Children list of the common parent? + if err := obj.mount(); err != nil { + return err + } + + if oldname == newname { + return nil + } + if oldname == "" || newname == "" { + return fmt.Errorf("invalid input path") + } + if !strings.HasPrefix(oldname, "/") || !strings.HasPrefix(newname, "/") { + return fmt.Errorf("invalid input path (not absolute)") + } + + // remove possible trailing slashes + srcCleanPath := path.Clean(oldname) + dstCleanPath := path.Clean(newname) + + src, err := obj.find(srcCleanPath) // get the file + if err != nil { + return err + } + + srcInfo, err := src.Stat() + if err != nil { + return err + } + + srcParentPath, srcName := path.Split(srcCleanPath) // looking for this + parent, err := obj.find(srcParentPath) + if err != nil { // might be ErrNotExist + return err + } + var rmi = -1 // index of node to remove from parent + // find the thing to be deleted + for i, n := range parent.Children { + if n.Path == srcName { + rmi = i // found here! + break + } + } + if rmi == -1 { + return fmt.Errorf("programming error") + } + + dst, err := obj.find(dstCleanPath) // does the destination already exist? + if err != nil && !os.IsNotExist(err) { + return err + } + if err == nil { // dst exists! + dstInfo, err := dst.Stat() + if err != nil { + return err + } + + // dir's can clobber anything or be clobbered apparently + if srcInfo.IsDir() || dstInfo.IsDir() { + return ErrExist // dir's can't clobber anything + } + + // remove from list by index + parent.Children = append(parent.Children[:rmi], parent.Children[rmi+1:]...) + + // we're a file clobbering another file... + // move file content from src -> dst and then delete src + // TODO: run a dst.Close() for extra safety first? + save := dst.Path // save the "name" + *dst = *src // TODO: is this safe? + dst.Path = save // "rename" it + + } else { // dst does not exist + + // check if the dst's parent exists and is a dir, if not, error + // if it is a dir, add src as a child to it and then delete src + dstParentPath, dstName := path.Split(dstCleanPath) // looking for this + node, err := obj.find(dstParentPath) + if err != nil { // might be ErrNotExist + return err + } + fi, err := node.Stat() + if err != nil { + return err + } + if !fi.IsDir() { // is the parent a suitable home? + return &os.LinkError{Op: "rename", Old: oldname, New: newname, Err: syscall.ENOTDIR} + } + + // remove from list by index + parent.Children = append(parent.Children[:rmi], parent.Children[rmi+1:]...) + + src.Path = dstName // "rename" it + node.Children = append(node.Children, src) // "copied" + } + + return obj.sync() // push up metadata changes +} + +// Stat returns some information about the particular path. +func (obj *Fs) Stat(name string) (os.FileInfo, error) { + if err := obj.mount(); err != nil { + return nil, err + } + if !strings.HasPrefix(name, "/") { + return nil, fmt.Errorf("invalid input path (not absolute)") + } + + f, err := obj.find(name) // get the file + if err != nil { + return nil, err + } + return f.Stat() +} + +// Lstat does exactly the same as Stat because we currently do not support +// symbolic links. +func (obj *Fs) Lstat(name string) (os.FileInfo, error) { + if err := obj.mount(); err != nil { + return nil, err + } + // TODO: we don't have symbolic links in our fs, so we pass this to stat + return obj.Stat(name) +} + +// Chmod changes the mode of a file. +func (obj *Fs) Chmod(name string, mode os.FileMode) error { + if err := obj.mount(); err != nil { + return err + } + if !strings.HasPrefix(name, "/") { + return fmt.Errorf("invalid input path (not absolute)") + } + + f, err := obj.find(name) // get the file + if err != nil { + return err + } + + f.Mode = f.Mode | mode // XXX: what is the correct way to do this? + return f.Sync() // push up the changed metadata +} + +// Chtimes changes the access and modification times of the named file, similar +// to the Unix utime() or utimes() functions. The underlying filesystem may +// truncate or round the values to a less precise time unit. If there is an +// error, it will be of type *PathError. +// FIXME: make sure everything we error is a *PathError +// TODO: atime is not currently implement and so it is silently ignored. +func (obj *Fs) Chtimes(name string, atime time.Time, mtime time.Time) error { + if err := obj.mount(); err != nil { + return err + } + if !strings.HasPrefix(name, "/") { + return fmt.Errorf("invalid input path (not absolute)") + } + + f, err := obj.find(name) // get the file + if err != nil { + return err + } + + f.ModTime = mtime + // TODO: add atime + return f.Sync() // push up the changed metadata +} + +// PathSplit splits a path into an array of tokens excluding any trailing empty +// tokens. +func PathSplit(p string) []string { + if p == "/" { // TODO: can't this all be expressed nicely in one line? + return []string{""} + } + return strings.Split(path.Clean(p), "/") +} diff --git a/etcd/fs/fs_test.go b/etcd/fs/fs_test.go new file mode 100644 index 00000000..8533b78c --- /dev/null +++ b/etcd/fs/fs_test.go @@ -0,0 +1,227 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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 fs_test // named this way to make it easier for examples + +import ( + "io" + "testing" + + "github.com/purpleidea/mgmt/etcd" + etcdfs "github.com/purpleidea/mgmt/etcd/fs" + "github.com/purpleidea/mgmt/util" + + "github.com/spf13/afero" +) + +// XXX: spawn etcd for this test, like `cdtmpmkdir && etcd` and then kill it... +// XXX: write a bunch more tests to test this + +// TODO: apparently using 0666 is equivalent to respecting the current umask +const ( + umask = 0666 + superblock = "/some/superblock" // TODO: generate randomly per test? +) + +func TestFs1(t *testing.T) { + etcdClient := &etcd.ClientEtcd{ + Seeds: []string{"localhost:2379"}, // endpoints + } + + if err := etcdClient.Connect(); err != nil { + t.Logf("client connection error: %+v", err) + return + } + defer etcdClient.Destroy() + + etcdFs := &etcdfs.Fs{ + Client: etcdClient.GetClient(), + Metadata: superblock, + DataPrefix: etcdfs.DefaultDataPrefix, + } + //var etcdFs afero.Fs = NewEtcdFs() + + if err := etcdFs.Mkdir("/", umask); err != nil { + t.Logf("error: %+v", err) + if err != etcdfs.ErrExist { + return + } + } + + if err := etcdFs.Mkdir("/tmp", umask); err != nil { + t.Logf("error: %+v", err) + if err != etcdfs.ErrExist { + return + } + } + + fi, err := etcdFs.Stat("/tmp") + if err != nil { + t.Logf("stat error: %+v", err) + return + } + + t.Logf("fi: %+v", fi) + t.Logf("isdir: %t", fi.IsDir()) + + f, err := etcdFs.Create("/tmp/foo") + if err != nil { + t.Logf("error: %+v", err) + return + } + + t.Logf("handle: %+v", f) + + i, err := f.WriteString("hello world!\n") + if err != nil { + t.Logf("error: %+v", err) + return + } + t.Logf("wrote: %d", i) + + if err := etcdFs.Mkdir("/tmp/d1", umask); err != nil { + t.Logf("error: %+v", err) + if err != etcdfs.ErrExist { + return + } + } + + if err := etcdFs.Rename("/tmp/foo", "/tmp/bar"); err != nil { + t.Logf("rename error: %+v", err) + return + } + + //f2, err := etcdFs.Create("/tmp/bar") + //if err != nil { + // t.Logf("error: %+v", err) + // return + //} + + //i2, err := f2.WriteString("hello bar!\n") + //if err != nil { + // t.Logf("error: %+v", err) + // return + //} + //t.Logf("wrote: %d", i2) + + dir, err := etcdFs.Open("/tmp") + if err != nil { + t.Logf("error: %+v", err) + return + } + names, err := dir.Readdirnames(-1) + if err != nil && err != io.EOF { + t.Logf("error: %+v", err) + return + } + for _, name := range names { + t.Logf("name in /tmp: %+v", name) + } + + //dir, err := etcdFs.Open("/") + //if err != nil { + // t.Logf("error: %+v", err) + // return + //} + //names, err := dir.Readdirnames(-1) + //if err != nil && err != io.EOF { + // t.Logf("error: %+v", err) + // return + //} + //for _, name := range names { + // t.Logf("name in /: %+v", name) + //} +} + +func TestFs2(t *testing.T) { + etcdClient := &etcd.ClientEtcd{ + Seeds: []string{"localhost:2379"}, // endpoints + } + + if err := etcdClient.Connect(); err != nil { + t.Logf("client connection error: %+v", err) + return + } + defer etcdClient.Destroy() + + etcdFs := &etcdfs.Fs{ + Client: etcdClient.GetClient(), + Metadata: superblock, + DataPrefix: etcdfs.DefaultDataPrefix, + } + + tree, err := util.FsTree(etcdFs, "/") + if err != nil { + t.Errorf("tree error: %+v", err) + return + } + t.Logf("tree: \n%s", tree) + + tree2, err := util.FsTree(etcdFs, "/tmp") + if err != nil { + t.Errorf("tree2 error: %+v", err) + return + } + t.Logf("tree2: \n%s", tree2) +} + +func TestFs3(t *testing.T) { + etcdClient := &etcd.ClientEtcd{ + Seeds: []string{"localhost:2379"}, // endpoints + } + + if err := etcdClient.Connect(); err != nil { + t.Logf("client connection error: %+v", err) + return + } + defer etcdClient.Destroy() + + etcdFs := &etcdfs.Fs{ + Client: etcdClient.GetClient(), + Metadata: superblock, + DataPrefix: etcdfs.DefaultDataPrefix, + } + + tree, err := util.FsTree(etcdFs, "/") + if err != nil { + t.Errorf("tree error: %+v", err) + return + } + t.Logf("tree: \n%s", tree) + + var memFs afero.Fs = afero.NewMemMapFs() + + if err := util.CopyFs(etcdFs, memFs, "/", "/", false); err != nil { + t.Errorf("CopyFs error: %+v", err) + return + } + if err := util.CopyFs(etcdFs, memFs, "/", "/", true); err != nil { + t.Errorf("CopyFs2 error: %+v", err) + return + } + if err := util.CopyFs(etcdFs, memFs, "/", "/tmp/d1/", false); err != nil { + t.Errorf("CopyFs3 error: %+v", err) + return + } + + tree2, err := util.FsTree(memFs, "/") + if err != nil { + t.Errorf("tree2 error: %+v", err) + return + } + t.Logf("tree2: \n%s", tree2) +} diff --git a/etcd/fs/util.go b/etcd/fs/util.go new file mode 100644 index 00000000..b56c06bc --- /dev/null +++ b/etcd/fs/util.go @@ -0,0 +1,88 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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 fs + +import ( + "os" + "path/filepath" + + "github.com/spf13/afero" +) + +// ReadAll reads from r until an error or EOF and returns the data it read. +// A successful call returns err == nil, not err == EOF. Because ReadAll is +// defined to read from src until EOF, it does not treat an EOF from Read +// as an error to be reported. +//func ReadAll(r io.Reader) ([]byte, error) { +// return afero.ReadAll(r) +//} + +// ReadDir reads the directory named by dirname and returns +// a list of sorted directory entries. +func (obj *Fs) ReadDir(dirname string) ([]os.FileInfo, error) { + return afero.ReadDir(obj, dirname) +} + +// ReadFile reads the file named by filename and returns the contents. +// A successful call returns err == nil, not err == EOF. Because ReadFile +// reads the whole file, it does not treat an EOF from Read as an error +// to be reported. +func (obj *Fs) ReadFile(filename string) ([]byte, error) { + return afero.ReadFile(obj, filename) +} + +// TempDir creates a new temporary directory in the directory dir +// with a name beginning with prefix and returns the path of the +// new directory. If dir is the empty string, TempDir uses the +// default directory for temporary files (see os.TempDir). +// Multiple programs calling TempDir simultaneously +// will not choose the same directory. It is the caller's responsibility +// to remove the directory when no longer needed. +func (obj *Fs) TempDir(dir, prefix string) (name string, err error) { + return afero.TempDir(obj, dir, prefix) +} + +// TempFile creates a new temporary file in the directory dir +// with a name beginning with prefix, opens the file for reading +// and writing, and returns the resulting *File. +// If dir is the empty string, TempFile uses the default directory +// for temporary files (see os.TempDir). +// Multiple programs calling TempFile simultaneously +// will not choose the same file. The caller can use f.Name() +// to find the pathname of the file. It is the caller's responsibility +// to remove the file when no longer needed. +func (obj *Fs) TempFile(dir, prefix string) (f afero.File, err error) { + return afero.TempFile(obj, dir, prefix) +} + +// WriteFile writes data to a file named by filename. +// If the file does not exist, WriteFile creates it with permissions perm; +// otherwise WriteFile truncates it before writing. +func (obj *Fs) WriteFile(filename string, data []byte, perm os.FileMode) error { + return afero.WriteFile(obj, filename, data, perm) +} + +// Walk walks the file tree rooted at root, calling walkFn for each file or +// directory in the tree, including root. All errors that arise visiting files +// and directories are filtered by walkFn. The files are walked in lexical +// order, which makes the output deterministic but means that for very +// large directories Walk can be inefficient. +// Walk does not follow symbolic links. +func (obj *Fs) Walk(root string, walkFn filepath.WalkFunc) error { + return afero.Walk(obj, root, walkFn) +} diff --git a/etcd/interfaces.go b/etcd/interfaces.go new file mode 100644 index 00000000..e44a78ad --- /dev/null +++ b/etcd/interfaces.go @@ -0,0 +1,30 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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 etcd + +import ( + etcd "github.com/coreos/etcd/clientv3" // "clientv3" +) + +// Client provides a simple interface specification for client requests. Both +// EmbdEtcd and ClientEtcd implement this. +type Client interface { + // TODO: add more method signatures + Get(path string, opts ...etcd.OpOption) (map[string]string, error) + Txn(ifcmps []etcd.Cmp, thenops, elseops []etcd.Op) (*etcd.TxnResponse, error) +} diff --git a/etcd/methods.go b/etcd/methods.go index 2002a308..2c45af21 100644 --- a/etcd/methods.go +++ b/etcd/methods.go @@ -39,7 +39,7 @@ func Nominate(obj *EmbdEtcd, hostname string, urls etcdtypes.URLs) error { defer log.Printf("Trace: Etcd: Nominate(%v): Finished!", hostname) } // nominate someone to be a server - nominate := fmt.Sprintf("/%s/nominated/%s", NS, hostname) + nominate := fmt.Sprintf("%s/nominated/%s", NS, hostname) ops := []etcd.Op{} // list of ops in this txn if urls != nil { ops = append(ops, etcd.OpPut(nominate, urls.String())) // TODO: add a TTL? (etcd.WithLease) @@ -57,7 +57,7 @@ func Nominate(obj *EmbdEtcd, hostname string, urls etcdtypes.URLs) error { // Nominated returns a urls map of nominated etcd server volunteers. // NOTE: I know 'nominees' might be more correct, but is less consistent here func Nominated(obj *EmbdEtcd) (etcdtypes.URLsMap, error) { - path := fmt.Sprintf("/%s/nominated/", NS) + path := fmt.Sprintf("%s/nominated/", NS) keyMap, err := obj.Get(path, etcd.WithPrefix()) // map[string]string, bool if err != nil { return nil, fmt.Errorf("nominated isn't available: %v", err) @@ -90,7 +90,7 @@ func Volunteer(obj *EmbdEtcd, urls etcdtypes.URLs) error { defer log.Printf("Trace: Etcd: Volunteer(%v): Finished!", obj.hostname) } // volunteer to be a server - volunteer := fmt.Sprintf("/%s/volunteers/%s", NS, obj.hostname) + volunteer := fmt.Sprintf("%s/volunteers/%s", NS, obj.hostname) ops := []etcd.Op{} // list of ops in this txn if urls != nil { // XXX: adding a TTL is crucial! (i think) @@ -112,7 +112,7 @@ func Volunteers(obj *EmbdEtcd) (etcdtypes.URLsMap, error) { log.Printf("Trace: Etcd: Volunteers()") defer log.Printf("Trace: Etcd: Volunteers(): Finished!") } - path := fmt.Sprintf("/%s/volunteers/", NS) + path := fmt.Sprintf("%s/volunteers/", NS) keyMap, err := obj.Get(path, etcd.WithPrefix()) if err != nil { return nil, fmt.Errorf("volunteers aren't available: %v", err) @@ -145,7 +145,7 @@ func AdvertiseEndpoints(obj *EmbdEtcd, urls etcdtypes.URLs) error { defer log.Printf("Trace: Etcd: AdvertiseEndpoints(%v): Finished!", obj.hostname) } // advertise endpoints - endpoints := fmt.Sprintf("/%s/endpoints/%s", NS, obj.hostname) + endpoints := fmt.Sprintf("%s/endpoints/%s", NS, obj.hostname) ops := []etcd.Op{} // list of ops in this txn if urls != nil { // TODO: add a TTL? (etcd.WithLease) @@ -167,7 +167,7 @@ func Endpoints(obj *EmbdEtcd) (etcdtypes.URLsMap, error) { log.Printf("Trace: Etcd: Endpoints()") defer log.Printf("Trace: Etcd: Endpoints(): Finished!") } - path := fmt.Sprintf("/%s/endpoints/", NS) + path := fmt.Sprintf("%s/endpoints/", NS) keyMap, err := obj.Get(path, etcd.WithPrefix()) if err != nil { return nil, fmt.Errorf("endpoints aren't available: %v", err) @@ -199,7 +199,7 @@ func SetHostnameConverged(obj *EmbdEtcd, hostname string, isConverged bool) erro log.Printf("Trace: Etcd: SetHostnameConverged(%s): %v", hostname, isConverged) defer log.Printf("Trace: Etcd: SetHostnameConverged(%v): Finished!", hostname) } - converged := fmt.Sprintf("/%s/converged/%s", NS, hostname) + converged := fmt.Sprintf("%s/converged/%s", NS, hostname) op := []etcd.Op{etcd.OpPut(converged, fmt.Sprintf("%t", isConverged))} if _, err := obj.Txn(nil, op, nil); err != nil { // TODO: do we need a skipConv flag here too? return fmt.Errorf("set converged failed") // exit in progress? @@ -213,7 +213,7 @@ func HostnameConverged(obj *EmbdEtcd) (map[string]bool, error) { log.Printf("Trace: Etcd: HostnameConverged()") defer log.Printf("Trace: Etcd: HostnameConverged(): Finished!") } - path := fmt.Sprintf("/%s/converged/", NS) + path := fmt.Sprintf("%s/converged/", NS) keyMap, err := obj.ComplexGet(path, true, etcd.WithPrefix()) // don't un-converge if err != nil { return nil, fmt.Errorf("converged values aren't available: %v", err) @@ -239,7 +239,7 @@ func HostnameConverged(obj *EmbdEtcd) (map[string]bool, error) { // AddHostnameConvergedWatcher adds a watcher with a callback that runs on // hostname state changes. func AddHostnameConvergedWatcher(obj *EmbdEtcd, callbackFn func(map[string]bool) error) (func(), error) { - path := fmt.Sprintf("/%s/converged/", NS) + path := fmt.Sprintf("%s/converged/", NS) internalCbFn := func(re *RE) error { // TODO: get the value from the response, and apply delta... // for now, just run a get operation which is easier to code! @@ -258,7 +258,7 @@ func SetClusterSize(obj *EmbdEtcd, value uint16) error { log.Printf("Trace: Etcd: SetClusterSize(): %v", value) defer log.Printf("Trace: Etcd: SetClusterSize(): Finished!") } - key := fmt.Sprintf("/%s/idealClusterSize", NS) + key := fmt.Sprintf("%s/idealClusterSize", NS) if err := obj.Set(key, strconv.FormatUint(uint64(value), 10)); err != nil { return fmt.Errorf("function SetClusterSize failed: %v", err) // exit in progress? @@ -268,7 +268,7 @@ func SetClusterSize(obj *EmbdEtcd, value uint16) error { // GetClusterSize gets the ideal target cluster size of etcd peers. func GetClusterSize(obj *EmbdEtcd) (uint16, error) { - key := fmt.Sprintf("/%s/idealClusterSize", NS) + key := fmt.Sprintf("%s/idealClusterSize", NS) keyMap, err := obj.Get(key) if err != nil { return 0, fmt.Errorf("function GetClusterSize failed: %v", err) diff --git a/etcd/resources.go b/etcd/resources.go index 42ed7921..461e254f 100644 --- a/etcd/resources.go +++ b/etcd/resources.go @@ -34,7 +34,7 @@ import ( // collection prefixes and filters that we care about... func WatchResources(obj *EmbdEtcd) chan error { ch := make(chan error, 1) // buffer it so we can measure it - path := fmt.Sprintf("/%s/exported/", NS) + path := fmt.Sprintf("%s/exported/", NS) callback := func(re *RE) error { // TODO: is this even needed? it used to happen on conn errors log.Printf("Etcd: Watch: Path: %v", path) // event @@ -61,7 +61,7 @@ func WatchResources(obj *EmbdEtcd) chan error { // SetResources exports all of the resources which we pass in to etcd. func SetResources(obj *EmbdEtcd, hostname string, resourceList []resources.Res) error { - // key structure is /$NS/exported/$hostname/resources/$uid = $data + // key structure is $NS/exported/$hostname/resources/$uid = $data var kindFilter []string // empty to get from everyone hostnameFilter := []string{hostname} @@ -83,7 +83,7 @@ func SetResources(obj *EmbdEtcd, hostname string, resourceList []resources.Res) log.Fatalf("Etcd: SetResources: Error: Empty kind: %v", res.GetName()) } uid := fmt.Sprintf("%s/%s", res.GetKind(), res.GetName()) - path := fmt.Sprintf("/%s/exported/%s/resources/%s", NS, hostname, uid) + path := fmt.Sprintf("%s/exported/%s/resources/%s", NS, hostname, uid) if data, err := resources.ResToB64(res); err == nil { ifs = append(ifs, etcd.Compare(etcd.Value(path), "=", data)) // desired state ops = append(ops, etcd.OpPut(path, data)) @@ -108,7 +108,7 @@ func SetResources(obj *EmbdEtcd, hostname string, resourceList []resources.Res) log.Fatalf("Etcd: SetResources: Error: Empty kind: %v", res.GetName()) } uid := fmt.Sprintf("%s/%s", res.GetKind(), res.GetName()) - path := fmt.Sprintf("/%s/exported/%s/resources/%s", NS, hostname, uid) + path := fmt.Sprintf("%s/exported/%s/resources/%s", NS, hostname, uid) if match(res, resourceList) { // if we match, no need to delete! continue @@ -134,10 +134,10 @@ func SetResources(obj *EmbdEtcd, hostname string, resourceList []resources.Res) // If the kindfilter or hostnameFilter is empty, then it assumes no filtering... // TODO: Expand this with a more powerful filter based on what we eventually // support in our collect DSL. Ideally a server side filter like WithFilter() -// We could do this if the pattern was /$NS/exported/$kind/$hostname/$uid = $data. +// We could do this if the pattern was $NS/exported/$kind/$hostname/$uid = $data. func GetResources(obj *EmbdEtcd, hostnameFilter, kindFilter []string) ([]resources.Res, error) { - // key structure is /$NS/exported/$hostname/resources/$uid = $data - path := fmt.Sprintf("/%s/exported/", NS) + // key structure is $NS/exported/$hostname/resources/$uid = $data + path := fmt.Sprintf("%s/exported/", NS) resourceList := []resources.Res{} keyMap, err := obj.Get(path, etcd.WithPrefix(), etcd.WithSort(etcd.SortByKey, etcd.SortAscend)) if err != nil { diff --git a/etcd/scheduler/alphastrategy.go b/etcd/scheduler/alphastrategy.go new file mode 100644 index 00000000..cd96258e --- /dev/null +++ b/etcd/scheduler/alphastrategy.go @@ -0,0 +1,49 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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 scheduler // TODO: i'd like this to be a separate package, but cycles! + +import ( + "fmt" + "sort" +) + +func init() { + Register("alpha", func() Strategy { return &alphaStrategy{} }) // must register the func and name +} + +type alphaStrategy struct { + // no state to store +} + +// Schedule returns the first host out of a sorted group of available hostnames. +func (obj *alphaStrategy) Schedule(hostnames map[string]string, opts *schedulerOptions) ([]string, error) { + if len(hostnames) <= 0 { + return nil, fmt.Errorf("strategy: cannot schedule from zero hosts") + } + if opts.maxCount <= 0 { + return nil, fmt.Errorf("strategy: cannot schedule with a max of zero") + } + + sortedHosts := []string{} + for key := range hostnames { + sortedHosts = append(sortedHosts, key) + } + sort.Strings(sortedHosts) + + return []string{sortedHosts[0]}, nil // pick first host +} diff --git a/etcd/scheduler/options.go b/etcd/scheduler/options.go new file mode 100644 index 00000000..b70d6dd6 --- /dev/null +++ b/etcd/scheduler/options.go @@ -0,0 +1,100 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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 scheduler + +import ( + "fmt" +) + +// Option is a type that can be used to configure the scheduler. +type Option func(*schedulerOptions) + +// schedulerOptions represents the different possible configurable options. Not +// all options necessarily work for each scheduler strategy algorithm. +type schedulerOptions struct { + debug bool + logf func(format string, v ...interface{}) + strategy Strategy + maxCount int // TODO: should this be *int to know when it's set? + reuseLease bool + sessionTTL int // TODO: should this be *int to know when it's set? + hostsFilter []string + // TODO: add more options +} + +// Debug specifies whether we should run in debug mode or not. +func Debug(debug bool) Option { + return func(so *schedulerOptions) { + so.debug = debug + } +} + +// Logf passes a logger function that we can use if so desired. +func Logf(logf func(format string, v ...interface{})) Option { + return func(so *schedulerOptions) { + so.logf = logf + } +} + +// StrategyKind sets the scheduler strategy used. +func StrategyKind(strategy string) Option { + return func(so *schedulerOptions) { + f, exists := registeredStrategies[strategy] + if !exists { + panic(fmt.Sprintf("scheduler: undefined strategy: %s", strategy)) + } + so.strategy = f() + } +} + +// MaxCount is the maximum number of hosts that should get simultaneously +// scheduled. +func MaxCount(maxCount int) Option { + return func(so *schedulerOptions) { + if maxCount > 0 { + so.maxCount = maxCount + } + } +} + +// ReuseLease specifies whether we should try and re-use the lease between runs. +// Ordinarily it would get discarded with each new version (deploy) of the code. +func ReuseLease(reuseLease bool) Option { + return func(so *schedulerOptions) { + so.reuseLease = reuseLease + } +} + +// SessionTTL is the amount of time to delay before expiring a key on abrupt +// host disconnect of if ReuseLease is true. +func SessionTTL(sessionTTL int) Option { + return func(so *schedulerOptions) { + if sessionTTL > 0 { + so.sessionTTL = sessionTTL + } + } +} + +// HostsFilter specifies a manual list of hosts, to use as a subset of whatever +// was auto-discovered. +// XXX: think more about this idea... +func HostsFilter(hosts []string) Option { + return func(so *schedulerOptions) { + so.hostsFilter = hosts + } +} diff --git a/etcd/scheduler/rrstrategy.go b/etcd/scheduler/rrstrategy.go new file mode 100644 index 00000000..87585213 --- /dev/null +++ b/etcd/scheduler/rrstrategy.go @@ -0,0 +1,84 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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 scheduler // TODO: i'd like this to be a separate package, but cycles! + +import ( + "fmt" + "sort" + + "github.com/purpleidea/mgmt/util" +) + +func init() { + Register("rr", func() Strategy { return &rrStrategy{} }) // must register the func and name +} + +type rrStrategy struct { + // some stored state + hosts []string +} + +// Schedule returns hosts in round robin style from the available hostnames. +func (obj *rrStrategy) Schedule(hostnames map[string]string, opts *schedulerOptions) ([]string, error) { + if len(hostnames) <= 0 { + return nil, fmt.Errorf("strategy: cannot schedule from zero hosts") + } + if opts.maxCount <= 0 { + return nil, fmt.Errorf("strategy: cannot schedule with a max of zero") + } + + // always get a deterministic list of current hosts first... + sortedHosts := []string{} + for key := range hostnames { + sortedHosts = append(sortedHosts, key) + } + sort.Strings(sortedHosts) + + if obj.hosts == nil { + obj.hosts = []string{} // initialize if needed + } + + // add any new hosts we learned about, to the end of the list + for _, x := range sortedHosts { + if !util.StrInList(x, obj.hosts) { + obj.hosts = append(obj.hosts, x) + } + } + + // remove any hosts we previouly knew about from the list + for ix := len(obj.hosts) - 1; ix >= 0; ix-- { + if !util.StrInList(obj.hosts[ix], sortedHosts) { + // delete entry at this index + obj.hosts = append(obj.hosts[:ix], obj.hosts[ix+1:]...) + } + } + + // get the maximum number of hosts to return + max := len(obj.hosts) // can't return more than we have + if opts.maxCount < max { // found a smaller limit + max = opts.maxCount + } + + result := []string{} + // now return the number of needed hosts from the list + for i := 0; i < max; i++ { + result = append(result, obj.hosts[i]) + } + + return result, nil +} diff --git a/etcd/scheduler/scheduler.go b/etcd/scheduler/scheduler.go new file mode 100644 index 00000000..944b4a68 --- /dev/null +++ b/etcd/scheduler/scheduler.go @@ -0,0 +1,570 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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 scheduler implements a distributed consensus scheduler with etcd. +package scheduler + +import ( + "context" + "errors" + "fmt" + "sort" + "strings" + "sync" + + etcd "github.com/coreos/etcd/clientv3" + "github.com/coreos/etcd/clientv3/concurrency" + pb "github.com/coreos/etcd/etcdserver/etcdserverpb" + errwrap "github.com/pkg/errors" +) + +const ( + // DefaultSessionTTL is the number of seconds to wait before a dead or + // unresponsive host is removed from the scheduled pool. + DefaultSessionTTL = 10 // seconds + + // DefaultMaxCount is the maximum number of hosts to schedule on if not + // specified. + DefaultMaxCount = 1 // TODO: what is the logical value to choose? +Inf? + + hostnameJoinChar = "," // char used to join and split lists of hostnames +) + +// ErrEndOfResults is a sentinel that represents no more results will be coming. +var ErrEndOfResults = errors.New("scheduler: end of results") + +var schedulerLeases = make(map[string]etcd.LeaseID) // process lifetime in-memory lease store + +// schedulerResult represents output from the scheduler. +type schedulerResult struct { + hosts []string + err error +} + +// Result is what is returned when you request a scheduler. You can call methods +// on it, and it stores the necessary state while you're running. When one of +// these is produced, the scheduler has already kicked off running for you +// automatically. +type Result struct { + results chan *schedulerResult + closeFunc func() // run this when you're done with the scheduler // TODO: replace with an input `context` +} + +// Next returns the next output from the scheduler when it changes. This blocks +// until a new value is available, which is why you may wish to use a context to +// cancel any read from this. It returns ErrEndOfResults if the scheduler shuts +// down. +func (obj *Result) Next(ctx context.Context) ([]string, error) { + select { + case val, ok := <-obj.results: + if !ok { + return nil, ErrEndOfResults + } + return val.hosts, val.err + + case <-ctx.Done(): + return nil, ctx.Err() + } +} + +// Shutdown causes everything to clean up. We no longer need the scheduler. +// TODO: should this be named Close() instead? Should it return an error? +func (obj *Result) Shutdown() { + obj.closeFunc() + // XXX: should we have a waitgroup to wait for it all to close? +} + +// TODO: use: https://github.com/coreos/etcd/pull/8488 when available +func leaseValue(key string) etcd.Cmp { + return etcd.Cmp{Key: []byte(key), Target: pb.Compare_LEASE} +} + +// Schedule returns a scheduler result which can be queried with it's available +// methods. This automatically causes different etcd clients sharing the same +// path to discover each other and be part of the scheduled set. On close the +// keys expire and will get removed from the scheduled set. Different options +// can be passed in to customize the behaviour. Hostname represents the unique +// identifier for the caller. The behaviour is undefined if this is run more +// than once with the same path and hostname simultaneously. +func Schedule(client *etcd.Client, path string, hostname string, opts ...Option) (*Result, error) { + if strings.HasSuffix(path, "/") { + return nil, fmt.Errorf("scheduler: path must not end with the slash char") + } + if !strings.HasPrefix(path, "/") { + return nil, fmt.Errorf("scheduler: path must start with the slash char") + } + if hostname == "" { + return nil, fmt.Errorf("scheduler: hostname must not be empty") + } + if strings.Contains(hostname, hostnameJoinChar) { + return nil, fmt.Errorf("scheduler: hostname must not contain join char: %s", hostnameJoinChar) + } + + // key structure is $path/election = ??? + // key structure is $path/exchange/$hostname = ??? + // key structure is $path/scheduled = ??? + + options := &schedulerOptions{ // default scheduler options + // If reuseLease is false, then on host disconnect, that hosts + // entry will immediately expire, and the scheduler will react + // instantly and remove that host entry from the list. If this + // is true, or if the host closes without a clean shutdown, it + // will take the TTL number of seconds to remove the key. This + // can be set using the concurrency.WithTTL option to Session. + reuseLease: false, + sessionTTL: DefaultSessionTTL, + maxCount: DefaultMaxCount, + } + for _, optionFunc := range opts { // apply the scheduler options + optionFunc(options) + } + + if options.strategy == nil { + return nil, fmt.Errorf("scheduler: strategy must be specified") + } + + sessionOptions := []concurrency.SessionOption{} + + // here we try to re-use lease between multiple runs of the code + // TODO: is it a good idea to try and re-use the lease b/w runs? + if options.reuseLease { + if leaseID, exists := schedulerLeases[path]; exists { + sessionOptions = append(sessionOptions, concurrency.WithLease(leaseID)) + } + } + // ttl for key expiry on abrupt disconnection or if reuseLease is true! + if options.sessionTTL > 0 { + sessionOptions = append(sessionOptions, concurrency.WithTTL(options.sessionTTL)) + } + + //options.debug = true // use this for local debugging + session, err := concurrency.NewSession(client, sessionOptions...) + if err != nil { + return nil, errwrap.Wrapf(err, "scheduler: could not create session") + } + leaseID := session.Lease() + if options.reuseLease { + // save for next time, otherwise run session.Close() somewhere + schedulerLeases[path] = leaseID + } + + ctx, cancel := context.WithCancel(context.Background()) // cancel below + //defer cancel() // do NOT do this, as it would cause an early cancel! + + // stored scheduler results + scheduledPath := fmt.Sprintf("%s/scheduled", path) + scheduledChan := client.Watcher.Watch(ctx, scheduledPath) + + // exchange hostname, and attach it to session (leaseID) so it expires + // (gets deleted) when we disconnect... + exchangePath := fmt.Sprintf("%s/exchange", path) + exchangePathHost := fmt.Sprintf("%s/%s", exchangePath, hostname) + exchangePathPrefix := fmt.Sprintf("%s/", exchangePath) + + // open the watch *before* we set our key so that we can see the change! + watchChan := client.Watcher.Watch(ctx, exchangePathPrefix, etcd.WithPrefix()) + + data := "TODO" // XXX: no data to exchange alongside hostnames yet + ifops := []etcd.Cmp{ + etcd.Compare(etcd.Value(exchangePathHost), "=", data), + etcd.Compare(leaseValue(exchangePathHost), "=", int64(leaseID)), // XXX: remove int64() after 3.3.0 + } + elsop := etcd.OpPut(exchangePathHost, data, etcd.WithLease(leaseID)) + + // it's important to do this in one transaction, and atomically, because + // this way, we only generate one watch event, and only when it's needed + // updating leaseID, or key expiry (deletion) both generate watch events + // XXX: context!!! + if txn, err := client.KV.Txn(context.TODO()).If(ifops...).Then([]etcd.Op{}...).Else(elsop).Commit(); err != nil { + defer cancel() // cancel to avoid leaks if we exit early... + return nil, errwrap.Wrapf(err, "could not exchange in `%s`", path) + } else if txn.Succeeded { + options.logf("txn did nothing...") // then branch + } else { + options.logf("txn did an update...") + } + + // create an election object + electionPath := fmt.Sprintf("%s/election", path) + election := concurrency.NewElection(session, electionPath) + electionChan := election.Observe(ctx) + + elected := "" // who we "assume" is elected + wg := &sync.WaitGroup{} + ch := make(chan *schedulerResult) + closeChan := make(chan struct{}) + send := func(hosts []string, err error) bool { // helper function for sending + select { + case ch <- &schedulerResult{ // send + hosts: hosts, + err: err, + }: + return true + case <-closeChan: // unblock + return false // not sent + } + } + + once := &sync.Once{} + onceBody := func() { // do not call directly, use closeFunc! + //cancel() // TODO: is this needed here? + // request a graceful shutdown, caller must call this to + // shutdown when they are finished with the scheduler... + // calling this will cause their hosts channels to close + close(closeChan) // send a close signal + } + closeFunc := func() { + once.Do(onceBody) + } + result := &Result{ + results: ch, + // TODO: we could accept a context to watch for cancel instead? + closeFunc: closeFunc, + } + + mutex := &sync.Mutex{} + var campaignClose chan struct{} + var campaignRunning bool + // goroutine to vote for someone as scheduler! each participant must be + // able to run this or nobody will be around to vote if others are down + campaignFunc := func() { + options.logf("starting campaign...") + // the mutex ensures we don't fly past the wg.Wait() if someone + // shuts down the scheduler right as we are about to start this + // campaigning loop up. we do not want to fail unnecessarily... + mutex.Lock() + wg.Add(1) + mutex.Unlock() + go func() { + defer wg.Done() + + ctx, cancel := context.WithCancel(context.Background()) + go func() { + defer cancel() // run cancel to stop campaigning... + select { + case <-campaignClose: + return + case <-closeChan: + return + } + }() + for { + // TODO: previously, this looped infinitely fast + // TODO: add some rate limiting here for initial + // campaigning which occasionally loops a lot... + if options.debug { + //fmt.Printf(".") // debug + options.logf("campaigning...") + } + + // "Campaign puts a value as eligible for the election. + // It blocks until it is elected, an error occurs, or + // the context is cancelled." + + // vote for ourselves, as it's the only host we can + // guarantee is alive, otherwise we wouldn't be voting! + // it would be more sensible to vote for the last valid + // hostname to keep things more stable, but if that + // information was stale, and that host wasn't alive, + // then this would defeat the point of picking them! + if err := election.Campaign(ctx, hostname); err != nil { + if err != context.Canceled { + send(nil, errwrap.Wrapf(err, "scheduler: error campaigning")) + } + return + } + } + }() + } + + go func() { + defer close(ch) + if !options.reuseLease { + defer session.Close() // this revokes the lease... + } + + defer func() { + // XXX: should we ever resign? why would this block and thus need a context? + if elected == hostname { // TODO: is it safe to just always do this? + if err := election.Resign(context.TODO()); err != nil { // XXX: add a timeout? + } + } + elected = "" // we don't care anymore! + }() + + // this "last" defer (first to run) should block until the other + // goroutine has closed so we don't Close an in-use session, etc + defer wg.Wait() + + go func() { + defer cancel() // run cancel to "free" Observe... + + defer wg.Wait() // also wait here if parent exits first + + select { + case <-closeChan: + // we want the above wg.Wait() to work if this + // close happens. lock with the campaign start + defer mutex.Unlock() + mutex.Lock() + return + } + }() + hostnames := make(map[string]string) + for { + select { + case val, ok := <-electionChan: + if options.debug { + options.logf("electionChan(%t): %+v", ok, val) + } + if !ok { + if options.debug { + options.logf("elections stream shutdown...") + } + electionChan = nil + // done + // TODO: do we need to send on error channel? + // XXX: maybe if context was not called to exit us? + + // ensure everyone waiting on closeChan + // gets cleaned up so we free mem, etc! + if watchChan == nil && scheduledChan == nil { // all now closed + closeFunc() + return + } + continue + + } + + elected = string(val.Kvs[0].Value) + //if options.debug { + options.logf("elected: %s", elected) + //} + if elected != hostname { // not me! + // start up the campaign function + if !campaignRunning { + campaignClose = make(chan struct{}) + campaignFunc() // run + campaignRunning = true + } + continue // someone else does the scheduling... + } else { // campaigning while i am it loops fast + // shutdown the campaign function + if campaignRunning { + close(campaignClose) + wg.Wait() + campaignRunning = false + } + } + + // i was voted in to make the scheduling choice! + + case watchResp, ok := <-watchChan: + if options.debug { + options.logf("watchChan(%t): %+v", ok, watchResp) + } + if !ok { + if options.debug { + options.logf("watch stream shutdown...") + } + watchChan = nil + // done + // TODO: do we need to send on error channel? + // XXX: maybe if context was not called to exit us? + + // ensure everyone waiting on closeChan + // gets cleaned up so we free mem, etc! + if electionChan == nil && scheduledChan == nil { // all now closed + closeFunc() + return + } + continue + } + + err := watchResp.Err() + if watchResp.Canceled || err == context.Canceled { + // channel get closed shortly... + continue + } + if watchResp.Header.Revision == 0 { // by inspection + // received empty message ? + // switched client connection ? + // FIXME: what should we do here ? + continue + } + if err != nil { + send(nil, errwrap.Wrapf(err, "scheduler: exchange watcher failed")) + continue + } + if len(watchResp.Events) == 0 { // nothing interesting + continue + } + + options.logf("running exchange values get...") + resp, err := client.Get(ctx, exchangePathPrefix, etcd.WithPrefix(), etcd.WithSort(etcd.SortByKey, etcd.SortAscend)) + if err != nil || resp == nil { + if err != nil { + send(nil, errwrap.Wrapf(err, "scheduler: could not get exchange values in `%s`", path)) + } else { // if resp == nil + send(nil, fmt.Errorf("scheduler: could not get exchange values in `%s`, resp is nil", path)) + } + continue + } + + // FIXME: the value key could instead be host + // specific information which is used for some + // purpose, eg: seconds active, and other data? + hostnames = make(map[string]string) // reset + for _, x := range resp.Kvs { + k := string(x.Key) + if !strings.HasPrefix(k, exchangePathPrefix) { + continue + } + k = k[len(exchangePathPrefix):] // strip + hostnames[k] = string(x.Value) + } + if options.debug { + options.logf("available hostnames: %+v", hostnames) + } + + case scheduledResp, ok := <-scheduledChan: + if options.debug { + options.logf("scheduledChan(%t): %+v", ok, scheduledResp) + } + if !ok { + if options.debug { + options.logf("scheduled stream shutdown...") + } + scheduledChan = nil + // done + // TODO: do we need to send on error channel? + // XXX: maybe if context was not called to exit us? + + // ensure everyone waiting on closeChan + // gets cleaned up so we free mem, etc! + if electionChan == nil && watchChan == nil { // all now closed + closeFunc() + return + } + continue + } + // event! continue below and get new result... + + // NOTE: not needed, exit this via Observe ctx cancel, + // which will ultimately cause the chan to shutdown... + //case <-closeChan: + // return + } // end select + + if len(hostnames) == 0 { + if options.debug { + options.logf("zero hosts available") + } + continue // not enough hosts available + } + + // if we're currently elected, make a scheduling decision + // if not, lookup the existing leader scheduling decision + if elected != hostname { + options.logf("i am not the leader, running scheduling result get...") + resp, err := client.Get(ctx, scheduledPath) + if err != nil || resp == nil || len(resp.Kvs) != 1 { + if err != nil { + send(nil, errwrap.Wrapf(err, "scheduler: could not get scheduling result in `%s`", path)) + } else if resp == nil { + send(nil, fmt.Errorf("scheduler: could not get scheduling result in `%s`, resp is nil", path)) + } else if len(resp.Kvs) > 1 { + send(nil, fmt.Errorf("scheduler: could not get scheduling result in `%s`, resp kvs: %+v", path, resp.Kvs)) + } + // if len(resp.Kvs) == 0, we shouldn't error + // in that situation it's just too early... + continue + } + + result := string(resp.Kvs[0].Value) + hosts := strings.Split(result, hostnameJoinChar) + + if options.debug { + options.logf("sending hosts: %+v", hosts) + } + // send that on channel! + if !send(hosts, nil) { + //return // pass instead, let channels clean up + } + continue + } + + // i am the leader, run scheduler and store result + options.logf("i am elected, running scheduler...") + + // run actual scheduler and decide who should be chosen + // TODO: is there any additional data that we can pass + // to the scheduler so it can make a better decision ? + hosts, err := options.strategy.Schedule(hostnames, options) + if err != nil { + send(nil, errwrap.Wrapf(err, "scheduler: strategy failed")) + continue + } + sort.Strings(hosts) // for consistency + + options.logf("storing scheduling result...") + data := strings.Join(hosts, hostnameJoinChar) + ifops := []etcd.Cmp{ + etcd.Compare(etcd.Value(scheduledPath), "=", data), + } + elsop := etcd.OpPut(scheduledPath, data) + + // it's important to do this in one transaction, and atomically, because + // this way, we only generate one watch event, and only when it's needed + // updating leaseID, or key expiry (deletion) both generate watch events + // XXX: context!!! + if _, err := client.KV.Txn(context.TODO()).If(ifops...).Then([]etcd.Op{}...).Else(elsop).Commit(); err != nil { + send(nil, errwrap.Wrapf(err, "scheduler: could not set scheduling result in `%s`", path)) + continue + } + + if options.debug { + options.logf("sending hosts: %+v", hosts) + } + // send that on channel! + if !send(hosts, nil) { + //return // pass instead, let channels clean up + } + } + }() + + // kick off an initial campaign if none exist already... + options.logf("checking for existing leader...") + leaderResult, err := election.Leader(ctx) + if err == concurrency.ErrElectionNoLeader { + // start up the campaign function + if !campaignRunning { + campaignClose = make(chan struct{}) + campaignFunc() // run + campaignRunning = true + } + } + if options.debug { + if err != nil { + options.logf("leader information error: %+v", err) + } else { + options.logf("leader information: %+v", leaderResult) + } + } + + return result, nil +} diff --git a/etcd/scheduler/strategy.go b/etcd/scheduler/strategy.go new file mode 100644 index 00000000..e0786146 --- /dev/null +++ b/etcd/scheduler/strategy.go @@ -0,0 +1,51 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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 scheduler + +import ( + "fmt" +) + +// registeredStrategies is a global map of all possible strategy implementations +// which can be used. You should never touch this map directly. Use methods like +// Register instead. +var registeredStrategies = make(map[string]func() Strategy) // must initialize + +// Strategy represents the methods a scheduler strategy must implement. +type Strategy interface { + Schedule(hostnames map[string]string, opts *schedulerOptions) ([]string, error) +} + +// Register takes a func and its name and makes it available for use. It is +// commonly called in the init() method of the func at program startup. There is +// no matching Unregister function. +func Register(name string, fn func() Strategy) { + if _, ok := registeredStrategies[name]; ok { + panic(fmt.Sprintf("a strategy named %s is already registered", name)) + } + //gob.Register(fn()) + registeredStrategies[name] = fn +} + +type nilStrategy struct { +} + +// Schedule returns an error for any scheduling request for this nil strategy. +func (obj *nilStrategy) Schedule(hostnames map[string]string, opts *schedulerOptions) ([]string, error) { + return nil, fmt.Errorf("scheduler: cannot schedule with nil scheduler") +} diff --git a/etcd/str.go b/etcd/str.go index d8bce464..2bc4ea9a 100644 --- a/etcd/str.go +++ b/etcd/str.go @@ -32,9 +32,13 @@ var ErrNotExist = errors.New("errNotExist") // WatchStr returns a channel which spits out events on key activity. // FIXME: It should close the channel when it's done, and spit out errors when // something goes wrong. +// XXX: since the caller of this (via the World API) has no way to tell it it's +// done, does that mean we leak go-routines since it might still be running, but +// perhaps even blocked??? Could this cause a dead-lock? Should we instead return +// some sort of struct which has a close method with it to ask for a shutdown? func WatchStr(obj *EmbdEtcd, key string) chan error { - // new key structure is /$NS/strings/$key = $data - path := fmt.Sprintf("/%s/strings/%s", NS, key) + // new key structure is $NS/strings/$key = $data + path := fmt.Sprintf("%s/strings/%s", NS, key) ch := make(chan error, 1) // FIXME: fix our API so that we get a close event on shutdown. callback := func(re *RE) error { @@ -54,8 +58,8 @@ func WatchStr(obj *EmbdEtcd, key string) chan error { // GetStr collects the string which matches a global namespace in etcd. func GetStr(obj *EmbdEtcd, key string) (string, error) { - // new key structure is /$NS/strings/$key = $data - path := fmt.Sprintf("/%s/strings/%s", NS, key) + // new key structure is $NS/strings/$key = $data + path := fmt.Sprintf("%s/strings/%s", NS, key) keyMap, err := obj.Get(path, etcd.WithPrefix()) if err != nil { return "", errwrap.Wrapf(err, "could not get strings in: %s", key) @@ -82,8 +86,8 @@ func GetStr(obj *EmbdEtcd, key string) (string, error) { // nil, then it deletes the key. Otherwise the value should point to a string. // TODO: TTL or delete disconnect? func SetStr(obj *EmbdEtcd, key string, data *string) error { - // key structure is /$NS/strings/$key = $data - path := fmt.Sprintf("/%s/strings/%s", NS, key) + // key structure is $NS/strings/$key = $data + path := fmt.Sprintf("%s/strings/%s", NS, key) ifs := []etcd.Cmp{} // list matching the desired state ops := []etcd.Op{} // list of ops in this transaction (then) els := []etcd.Op{} // list of ops in this transaction (else) diff --git a/etcd/strmap.go b/etcd/strmap.go index 6bb9f893..a5d825ad 100644 --- a/etcd/strmap.go +++ b/etcd/strmap.go @@ -31,8 +31,8 @@ import ( // FIXME: It should close the channel when it's done, and spit out errors when // something goes wrong. func WatchStrMap(obj *EmbdEtcd, key string) chan error { - // new key structure is /$NS/strings/$key/$hostname = $data - path := fmt.Sprintf("/%s/strings/%s", NS, key) + // new key structure is $NS/strings/$key/$hostname = $data + path := fmt.Sprintf("%s/strings/%s", NS, key) ch := make(chan error, 1) // FIXME: fix our API so that we get a close event on shutdown. callback := func(re *RE) error { @@ -52,12 +52,12 @@ func WatchStrMap(obj *EmbdEtcd, key string) chan error { // GetStrMap collects all of the strings which match a namespace in etcd. func GetStrMap(obj *EmbdEtcd, hostnameFilter []string, key string) (map[string]string, error) { - // old key structure is /$NS/strings/$hostname/$key = $data - // new key structure is /$NS/strings/$key/$hostname = $data + // old key structure is $NS/strings/$hostname/$key = $data + // new key structure is $NS/strings/$key/$hostname = $data // FIXME: if we have the $key as the last token (old key structure), we // can allow the key to contain the slash char, otherwise we need to // verify that one isn't present in the input string. - path := fmt.Sprintf("/%s/strings/%s", NS, key) + path := fmt.Sprintf("%s/strings/%s", NS, key) keyMap, err := obj.Get(path, etcd.WithPrefix(), etcd.WithSort(etcd.SortByKey, etcd.SortAscend)) if err != nil { return nil, errwrap.Wrapf(err, "could not get strings in: %s", key) @@ -92,8 +92,8 @@ func GetStrMap(obj *EmbdEtcd, hostnameFilter []string, key string) (map[string]s // nil, then it deletes the key. Otherwise the value should point to a string. // TODO: TTL or delete disconnect? func SetStrMap(obj *EmbdEtcd, hostname, key string, data *string) error { - // key structure is /$NS/strings/$key/$hostname = $data - path := fmt.Sprintf("/%s/strings/%s/%s", NS, key, hostname) + // key structure is $NS/strings/$key/$hostname = $data + path := fmt.Sprintf("%s/strings/%s/%s", NS, key, hostname) ifs := []etcd.Cmp{} // list matching the desired state ops := []etcd.Op{} // list of ops in this transaction (then) els := []etcd.Op{} // list of ops in this transaction (else) diff --git a/etcd/world.go b/etcd/world.go index dc71ad4c..db3a660b 100644 --- a/etcd/world.go +++ b/etcd/world.go @@ -18,13 +18,24 @@ package etcd import ( + "fmt" + "net/url" + "strings" + + etcdfs "github.com/purpleidea/mgmt/etcd/fs" + "github.com/purpleidea/mgmt/etcd/scheduler" "github.com/purpleidea/mgmt/resources" ) // World is an etcd backed implementation of the World interface. type World struct { - Hostname string // uuid for the consumer of these - EmbdEtcd *EmbdEtcd + Hostname string // uuid for the consumer of these + EmbdEtcd *EmbdEtcd + MetadataPrefix string // expected metadata prefix + StoragePrefix string // storage prefix for etcdfs storage + StandaloneFs resources.Fs // store an fs here for local usage + Debug bool + Logf func(format string, v ...interface{}) } // ResWatch returns a channel which spits out events on possible exported @@ -93,3 +104,49 @@ func (obj *World) StrMapSet(namespace, value string) error { func (obj *World) StrMapDel(namespace string) error { return SetStrMap(obj.EmbdEtcd, obj.Hostname, namespace, nil) } + +// Scheduler returns a scheduling result of hosts in a particular namespace. +func (obj *World) Scheduler(namespace string, opts ...scheduler.Option) (*scheduler.Result, error) { + modifiedOpts := []scheduler.Option{} + for _, o := range opts { + modifiedOpts = append(modifiedOpts, o) // copy in + } + + modifiedOpts = append(modifiedOpts, scheduler.Debug(obj.Debug)) + modifiedOpts = append(modifiedOpts, scheduler.Logf(obj.Logf)) + + return scheduler.Schedule(obj.EmbdEtcd.GetClient(), fmt.Sprintf("%s/scheduler/%s", NS, namespace), obj.Hostname, modifiedOpts...) +} + +// Fs returns a distributed file system from a unique URI. For single host +// execution that doesn't span more than a single host, this file system might +// actually be a local or memory backed file system, so actually only +// distributed within the boredom that is a single host cluster. +func (obj *World) Fs(uri string) (resources.Fs, error) { + u, err := url.Parse(uri) + if err != nil { + return nil, err + } + + // we're in standalone mode + if u.Scheme == "memmapfs" && u.Path == "/" { + return obj.StandaloneFs, nil + } + + if u.Scheme != "etcdfs" { + return nil, fmt.Errorf("unknown scheme: `%s`", u.Scheme) + } + if u.Path == "" { + return nil, fmt.Errorf("empty path: %s", u.Path) + } + if !strings.HasPrefix(u.Path, obj.MetadataPrefix) { + return nil, fmt.Errorf("wrong path prefix: %s", u.Path) + } + + etcdFs := &etcdfs.Fs{ + Client: obj.EmbdEtcd.GetClient(), + Metadata: u.Path, + DataPrefix: obj.StoragePrefix, + } + return etcdFs, nil +} diff --git a/examples/graph0.hcl b/examples/hcl/graph0.hcl similarity index 100% rename from examples/graph0.hcl rename to examples/hcl/graph0.hcl diff --git a/examples/graph1.hcl b/examples/hcl/graph1.hcl similarity index 100% rename from examples/graph1.hcl rename to examples/hcl/graph1.hcl diff --git a/examples/hil.hcl b/examples/hcl/hil.hcl similarity index 100% rename from examples/hil.hcl rename to examples/hcl/hil.hcl diff --git a/examples/lang/contains0.mcl b/examples/lang/contains0.mcl new file mode 100644 index 00000000..e434fc6a --- /dev/null +++ b/examples/lang/contains0.mcl @@ -0,0 +1,22 @@ +$set = ["a", "b", "c", "d",] + +$c1 = "x1" in ["x1", "x2", "x3",] +$c2 = 42 in [4, 13, 42,] +$c3 = "x" in $set +$c4 = "b" in $set + +$s = printf("1: %t, 2: %t, 3: %t, 4: %t\n", $c1, $c2, $c3, $c4) + +file "/tmp/mgmt/contains" { + content => $s, +} + +$x = if hostname() in ["h1", "h3",] { + printf("i (%s) am one of the chosen few!\n", hostname()) +} else { + printf("i (%s) was not chosen :(\n", hostname()) +} + +file "/tmp/mgmt/hello-${hostname()}" { + content => $x, +} diff --git a/examples/lang/datetime1.mcl b/examples/lang/datetime1.mcl new file mode 100644 index 00000000..e898ed97 --- /dev/null +++ b/examples/lang/datetime1.mcl @@ -0,0 +1,4 @@ +$d = datetime() +file "/tmp/mgmt/datetime" { + content => template("Hello! It is now: {{ datetimeprint . }}\n", $d), +} diff --git a/examples/lang/datetime2.mcl b/examples/lang/datetime2.mcl new file mode 100644 index 00000000..9cd495ba --- /dev/null +++ b/examples/lang/datetime2.mcl @@ -0,0 +1,14 @@ +$secplusone = datetime() + $ayear + +# note the order of the assignment (year can come later in the code) +$ayear = 60 * 60 * 24 * 365 # is a year in seconds (31536000) + +$tmplvalues = struct{year => $secplusone, load => $theload,} + +$theload = structlookup(load(), "x1") + +if 5 > 3 { + file "/tmp/mgmt/datetime" { + content => template("Now + 1 year is: {{ .year }} seconds, aka: {{ datetimeprint .year }}\n\nload average: {{ .load }}\n", $tmplvalues), + } +} diff --git a/examples/lang/datetime3.mcl b/examples/lang/datetime3.mcl new file mode 100644 index 00000000..03ad2474 --- /dev/null +++ b/examples/lang/datetime3.mcl @@ -0,0 +1,14 @@ +$secplusone = datetime() + $ayear + +# note the order of the assignment (year can come later in the code) +$ayear = 60 * 60 * 24 * 365 # is a year in seconds (31536000) + +$tmplvalues = struct{year => $secplusone, load => $theload, vumeter => $vumeter,} + +$theload = structlookup(load(), "x1") + +$vumeter = vumeter("====", 10, 0.9) + +file "/tmp/mgmt/datetime" { + content => template("Now + 1 year is: {{ .year }} seconds, aka: {{ datetimeprint .year }}\n\nload average: {{ .load }}\n\nvu: {{ .vumeter }}\n", $tmplvalues), +} diff --git a/examples/lang/exchange0.mcl b/examples/lang/exchange0.mcl new file mode 100644 index 00000000..97af41e9 --- /dev/null +++ b/examples/lang/exchange0.mcl @@ -0,0 +1,13 @@ +# run this example with these commands +# watch -n 0.1 'tail *' # run this in /tmp/mgmt/ +# time ./mgmt run --lang examples/lang/exchange0.mcl --hostname h1 --ideal-cluster-size 1 --tmp-prefix --no-pgp +# time ./mgmt run --lang examples/lang/exchange0.mcl --hostname h2 --seeds http://127.0.0.1:2379 --client-urls http://127.0.0.1:2381 --server-urls http://127.0.0.1:2382 --tmp-prefix --no-pgp +# time ./mgmt run --lang examples/lang/exchange0.mcl --hostname h3 --seeds http://127.0.0.1:2379 --client-urls http://127.0.0.1:2383 --server-urls http://127.0.0.1:2384 --tmp-prefix --no-pgp +# time ./mgmt run --lang examples/lang/exchange0.mcl --hostname h4 --seeds http://127.0.0.1:2379 --client-urls http://127.0.0.1:2385 --server-urls http://127.0.0.1:2386 --tmp-prefix --no-pgp + +$rand = random1(8) +$exchanged = exchange("keyns", $rand) + +file "/tmp/mgmt/exchange-${hostname()}" { + content => template("Found: {{ . }}\n", $exchanged), +} diff --git a/examples/lang/hello0.mcl b/examples/lang/hello0.mcl new file mode 100644 index 00000000..7c90489a --- /dev/null +++ b/examples/lang/hello0.mcl @@ -0,0 +1,4 @@ +file "/tmp/mgmt-hello-world" { + content => "hello world from @purpleidea\n", + state => "exists", +} diff --git a/examples/lang/history1.mcl b/examples/lang/history1.mcl new file mode 100644 index 00000000..90df8b76 --- /dev/null +++ b/examples/lang/history1.mcl @@ -0,0 +1,7 @@ +$dt = datetime() + +$hystvalues = {"ix0" => $dt, "ix1" => $dt{1}, "ix2" => $dt{2}, "ix3" => $dt{3},} + +file "/tmp/mgmt/history" { + content => template("Index(0) {{.ix0}}: {{ datetimeprint .ix0 }}\nIndex(1) {{.ix1}}: {{ datetimeprint .ix1 }}\nIndex(2) {{.ix2}}: {{ datetimeprint .ix2 }}\nIndex(3) {{.ix3}}: {{ datetimeprint .ix3 }}\n", $hystvalues), +} diff --git a/examples/lang/hostname0.mcl b/examples/lang/hostname0.mcl new file mode 100644 index 00000000..4538a350 --- /dev/null +++ b/examples/lang/hostname0.mcl @@ -0,0 +1,4 @@ +file "/tmp/mgmt/${hostname()}" { + content => "hello from ${hostname()}!\n", + state => "exists", +} diff --git a/examples/lang/hysteresis1.mcl b/examples/lang/hysteresis1.mcl new file mode 100644 index 00000000..54792f67 --- /dev/null +++ b/examples/lang/hysteresis1.mcl @@ -0,0 +1,31 @@ +file "/tmp/mgmt/systemload" { + content => template("load average: {{ .load }} threshold: {{ .threshold }}\n", $tmplvalues), +} + +$tmplvalues = struct{load => $theload, threshold => $threshold,} + +$theload = structlookup(load(), "x1") +$threshold = 1.5 # change me if you like + +# simple hysteresis implementation +$h1 = $theload > $threshold +$h2 = $theload{1} > $threshold +$h3 = $theload{2} > $threshold +$unload = $h1 || $h2 || $h3 + +virt "mgmt1" { + uri => "qemu:///session", + cpus => 1, + memory => 524288, + state => "running", + transient => true, +} + +# this vm shuts down under load... +virt "mgmt2" { + uri => "qemu:///session", + cpus => 1, + memory => 524288, + state => if $unload { "shutoff" } else { "running" }, + transient => true, +} diff --git a/examples/lang/interpolate1.mcl b/examples/lang/interpolate1.mcl new file mode 100644 index 00000000..41fa954c --- /dev/null +++ b/examples/lang/interpolate1.mcl @@ -0,0 +1,5 @@ +$audience = "WORLD!" +file "/tmp/mgmt/hello" { + content => "hello ${audience}!\n", + state => "exists", +} diff --git a/examples/lang/load0.mcl b/examples/lang/load0.mcl new file mode 100644 index 00000000..960fe4e5 --- /dev/null +++ b/examples/lang/load0.mcl @@ -0,0 +1,9 @@ +$theload = load() + +$x1 = structlookup($theload, "x1") +$x5 = structlookup($theload, "x5") +$x15 = structlookup($theload, "x15") + +print "print1" { + msg => printf("load average: %f, %f, %f", $x1, $x5, $x15), +} diff --git a/examples/lang/maplookup1.mcl b/examples/lang/maplookup1.mcl new file mode 100644 index 00000000..2a7aca28 --- /dev/null +++ b/examples/lang/maplookup1.mcl @@ -0,0 +1,13 @@ +$m = {"k1" => 42, "k2" => 13,} + +$found = maplookup($m, "k1", 99) + +print "print1" { + msg => printf("found value of: %d", $found), +} + +$notfound = maplookup($m, "k3", 99) + +print "print2" { + msg => printf("notfound value of: %d", $notfound), +} diff --git a/examples/lang/math1.mcl b/examples/lang/math1.mcl new file mode 100644 index 00000000..98a46331 --- /dev/null +++ b/examples/lang/math1.mcl @@ -0,0 +1,4 @@ +test "t1" { + int64 => (4 + 32) * 15 - 8, + anotherstr => printf("the answer is: %d", 42), +} diff --git a/examples/lang/printf1.mcl b/examples/lang/printf1.mcl new file mode 100644 index 00000000..9a3b9abf --- /dev/null +++ b/examples/lang/printf1.mcl @@ -0,0 +1,8 @@ +test "printf-a" { + anotherstr => printf("the %s is: %d", "answer", 42), +} + +$format = "a %s is: %f" +test "printf-b" { + anotherstr => printf($format, "cool number", 3.14159), +} diff --git a/examples/lang/schedule0.mcl b/examples/lang/schedule0.mcl new file mode 100644 index 00000000..cbbc05f9 --- /dev/null +++ b/examples/lang/schedule0.mcl @@ -0,0 +1,18 @@ +# here are all the possible options: +#$opts = struct{strategy => "rr", max => 3, reuse => false, ttl => 10,} + +# although an empty struct is valid too: +#$opts = struct{} + +# we'll just use a smaller subset today: +$opts = struct{strategy => "rr", max => 2, ttl => 10,} + +# schedule in a particular namespace with options: +$set = schedule("xsched", $opts) + +# and if you want, you can omit the options entirely: +#$set = schedule("xsched") + +file "/tmp/mgmt/scheduled-${hostname()}" { + content => template("set: {{ . }}\n", $set), +} diff --git a/examples/lang/shadowing1.mcl b/examples/lang/shadowing1.mcl new file mode 100644 index 00000000..37932299 --- /dev/null +++ b/examples/lang/shadowing1.mcl @@ -0,0 +1,10 @@ +$x = "hello" +if true { + $x = "i am shadowed" # this is allowed, but not a good practice to intentionally shadow + print "inner-scope" { + msg => $x, # contents are: i am shadowed + } +} +print "top-scope" { + msg => $x, # contents are: hello +} diff --git a/examples/lang/states0.mcl b/examples/lang/states0.mcl new file mode 100644 index 00000000..f862e715 --- /dev/null +++ b/examples/lang/states0.mcl @@ -0,0 +1,35 @@ +$ns = "estate" +$exchanged = kvlookup($ns) + +$state = maplookup($exchanged, $hostname, "default") + +if $state == "one" || $state == "default" { + + file "/tmp/mgmt/state" { + content => "state: one\n", + } + kv "${ns}" { + key => $ns, + value => "two", + } +} +if $state == "two" { + + file "/tmp/mgmt/state" { + content => "state: two\n", + } + kv "${ns}" { + key => $ns, + value => "three", + } +} +if $state == "three" { + + file "/tmp/mgmt/state" { + content => "state: three\n", + } + kv "${ns}" { + key => $ns, + value => "one", + } +} diff --git a/examples/lang/structlookup1.mcl b/examples/lang/structlookup1.mcl new file mode 100644 index 00000000..c3812246 --- /dev/null +++ b/examples/lang/structlookup1.mcl @@ -0,0 +1,13 @@ +$st = struct{f1 => 42, f2 => true, f3 => 3.14,} + +$f1 = structlookup($st, "f1") + +print "print1" { + msg => printf("f1 field is: %d", $f1), +} + +$f2 = structlookup($st, "f2") + +print "print2" { + msg => printf("f2 field is: %t", $f2), +} diff --git a/examples/lang/virt1.mcl b/examples/lang/virt1.mcl new file mode 100644 index 00000000..14686e54 --- /dev/null +++ b/examples/lang/virt1.mcl @@ -0,0 +1,7 @@ +virt "mgmt3" { + uri => "qemu:///session", + cpus => 1, + memory => 524288, + state => "running", + transient => true, +} diff --git a/examples/lib/exec-send-recv.go b/examples/lib/exec-send-recv.go index 58ddd56c..e5f61998 100644 --- a/examples/lib/exec-send-recv.go +++ b/examples/lib/exec-send-recv.go @@ -14,6 +14,15 @@ import ( mgmt "github.com/purpleidea/mgmt/lib" "github.com/purpleidea/mgmt/pgraph" "github.com/purpleidea/mgmt/resources" + + "github.com/urfave/cli" +) + +// XXX: this has not been updated to latest GAPI/Deploy API. Patches welcome! + +const ( + // Name is the name of this frontend. + Name = "libmgmt" ) // MyGAPI implements the main GAPI interface. @@ -36,6 +45,39 @@ func NewMyGAPI(data gapi.Data, name string, interval uint) (*MyGAPI, error) { return obj, obj.Init(data) } +// Cli takes a cli.Context, and returns our GAPI if activated. All arguments +// should take the prefix of the registered name. On activation, if there are +// any validation problems, you should return an error. If this was not +// activated, then you should return a nil GAPI and a nil error. +func (obj *MyGAPI) Cli(c *cli.Context, fs resources.Fs) (*gapi.Deploy, error) { + if s := c.String(obj.Name); c.IsSet(obj.Name) { + if s != "" { + return nil, fmt.Errorf("input is not empty") + } + + return &gapi.Deploy{ + Name: obj.Name, + Noop: c.GlobalBool("noop"), + Sema: c.GlobalInt("sema"), + GAPI: &MyGAPI{ + // TODO: add properties here... + }, + }, nil + } + return nil, nil // we weren't activated! +} + +// CliFlags returns a list of flags used by this deploy subcommand. +func (obj *MyGAPI) CliFlags() []cli.Flag { + return []cli.Flag{ + cli.StringFlag{ + Name: obj.Name, + Value: "", + Usage: "run", + }, + } +} + // Init initializes the MyGAPI struct. func (obj *MyGAPI) Init(data gapi.Data) error { if obj.initialized { @@ -53,7 +95,7 @@ func (obj *MyGAPI) Init(data gapi.Data) error { // Graph returns a current Graph. func (obj *MyGAPI) Graph() (*pgraph.Graph, error) { if !obj.initialized { - return nil, fmt.Errorf("libmgmt: MyGAPI is not initialized") + return nil, fmt.Errorf("%s: MyGAPI is not initialized", Name) } g, err := pgraph.NewGraph(obj.Name) @@ -135,7 +177,7 @@ func (obj *MyGAPI) Next() chan gapi.Next { defer close(ch) // this will run before the obj.wg.Done() if !obj.initialized { next := gapi.Next{ - Err: fmt.Errorf("libmgmt: MyGAPI is not initialized"), + Err: fmt.Errorf("%s: MyGAPI is not initialized", Name), Exit: true, // exit, b/c programming error? } ch <- next @@ -164,7 +206,7 @@ func (obj *MyGAPI) Next() chan gapi.Next { return } - log.Printf("libmgmt: Generating new graph...") + log.Printf("%s: Generating new graph...", Name) select { case ch <- gapi.Next{}: // trigger a run case <-obj.closeChan: @@ -178,7 +220,7 @@ func (obj *MyGAPI) Next() chan gapi.Next { // Close shuts down the MyGAPI. func (obj *MyGAPI) Close() error { if !obj.initialized { - return fmt.Errorf("libmgmt: MyGAPI is not initialized") + return fmt.Errorf("%s: MyGAPI is not initialized", Name) } close(obj.closeChan) obj.wg.Wait() @@ -199,10 +241,10 @@ func Run() error { obj.ConvergedTimeout = -1 obj.Noop = false // FIXME: careful! - obj.GAPI = &MyGAPI{ // graph API - Name: "libmgmt", // TODO: set on compilation - Interval: 60 * 10, // arbitrarily change graph every 15 seconds - } + //obj.GAPI = &MyGAPI{ // graph API + // Name: "libmgmt", // TODO: set on compilation + // Interval: 60 * 10, // arbitrarily change graph every 15 seconds + //} if err := obj.Init(); err != nil { return err diff --git a/examples/lib/libmgmt-subgraph0.go b/examples/lib/libmgmt-subgraph0.go index fc372860..0f2bf17d 100644 --- a/examples/lib/libmgmt-subgraph0.go +++ b/examples/lib/libmgmt-subgraph0.go @@ -16,8 +16,20 @@ import ( "github.com/purpleidea/mgmt/resources" errwrap "github.com/pkg/errors" + "github.com/urfave/cli" ) +// XXX: this has not been updated to latest GAPI/Deploy API. Patches welcome! + +const ( + // Name is the name of this frontend. + Name = "libmgmt" +) + +func init() { + gapi.Register(Name, func() gapi.GAPI { return &MyGAPI{} }) // register +} + // MyGAPI implements the main GAPI interface. type MyGAPI struct { Name string // graph name @@ -38,6 +50,39 @@ func NewMyGAPI(data gapi.Data, name string, interval uint) (*MyGAPI, error) { return obj, obj.Init(data) } +// Cli takes a cli.Context, and returns our GAPI if activated. All arguments +// should take the prefix of the registered name. On activation, if there are +// any validation problems, you should return an error. If this was not +// activated, then you should return a nil GAPI and a nil error. +func (obj *MyGAPI) Cli(c *cli.Context, fs resources.Fs) (*gapi.Deploy, error) { + if s := c.String(obj.Name); c.IsSet(obj.Name) { + if s != "" { + return nil, fmt.Errorf("input is not empty") + } + + return &gapi.Deploy{ + Name: obj.Name, + Noop: c.GlobalBool("noop"), + Sema: c.GlobalInt("sema"), + GAPI: &MyGAPI{ + // TODO: add properties here... + }, + }, nil + } + return nil, nil // we weren't activated! +} + +// CliFlags returns a list of flags used by this deploy subcommand. +func (obj *MyGAPI) CliFlags() []cli.Flag { + return []cli.Flag{ + cli.StringFlag{ + Name: obj.Name, + Value: "", + Usage: "run", + }, + } +} + // Init initializes the MyGAPI struct. func (obj *MyGAPI) Init(data gapi.Data) error { if obj.initialized { @@ -87,7 +132,7 @@ func (obj *MyGAPI) subGraph() (*pgraph.Graph, error) { // Graph returns a current Graph. func (obj *MyGAPI) Graph() (*pgraph.Graph, error) { if !obj.initialized { - return nil, fmt.Errorf("libmgmt: MyGAPI is not initialized") + return nil, fmt.Errorf("%s: MyGAPI is not initialized", Name) } g, err := pgraph.NewGraph(obj.Name) @@ -142,7 +187,7 @@ func (obj *MyGAPI) Next() chan gapi.Next { defer close(ch) // this will run before the obj.wg.Done() if !obj.initialized { next := gapi.Next{ - Err: fmt.Errorf("libmgmt: MyGAPI is not initialized"), + Err: fmt.Errorf("%s: MyGAPI is not initialized", Name), Exit: true, // exit, b/c programming error? } ch <- next @@ -171,7 +216,7 @@ func (obj *MyGAPI) Next() chan gapi.Next { return } - log.Printf("libmgmt: Generating new graph...") + log.Printf("%s: Generating new graph...", Name) select { case ch <- gapi.Next{}: // trigger a run case <-obj.closeChan: @@ -185,7 +230,7 @@ func (obj *MyGAPI) Next() chan gapi.Next { // Close shuts down the MyGAPI. func (obj *MyGAPI) Close() error { if !obj.initialized { - return fmt.Errorf("libmgmt: MyGAPI is not initialized") + return fmt.Errorf("%s: MyGAPI is not initialized", Name) } close(obj.closeChan) obj.wg.Wait() @@ -197,19 +242,19 @@ func (obj *MyGAPI) Close() error { func Run() error { obj := &mgmt.Main{} - obj.Program = "libmgmt" // TODO: set on compilation - obj.Version = "0.0.1" // TODO: set on compilation - obj.TmpPrefix = true // disable for easy debugging + obj.Program = Name // TODO: set on compilation + obj.Version = "0.0.1" // TODO: set on compilation + obj.TmpPrefix = true // disable for easy debugging //prefix := "/tmp/testprefix/" //obj.Prefix = &p // enable for easy debugging obj.IdealClusterSize = -1 obj.ConvergedTimeout = -1 obj.Noop = false // FIXME: careful! - obj.GAPI = &MyGAPI{ // graph API - Name: "libmgmt", // TODO: set on compilation - Interval: 60 * 10, // arbitrarily change graph every 15 seconds - } + //obj.GAPI = &MyGAPI{ // graph API + // Name: Name, // TODO: set on compilation + // Interval: 60 * 10, // arbitrarily change graph every 15 seconds + //} if err := obj.Init(); err != nil { return err diff --git a/examples/lib/libmgmt-subgraph1.go b/examples/lib/libmgmt-subgraph1.go index a269ad04..6f266016 100644 --- a/examples/lib/libmgmt-subgraph1.go +++ b/examples/lib/libmgmt-subgraph1.go @@ -14,6 +14,15 @@ import ( mgmt "github.com/purpleidea/mgmt/lib" "github.com/purpleidea/mgmt/pgraph" "github.com/purpleidea/mgmt/resources" + + "github.com/urfave/cli" +) + +// XXX: this has not been updated to latest GAPI/Deploy API. Patches welcome! + +const ( + // Name is the name of this frontend. + Name = "libmgmt" ) // MyGAPI implements the main GAPI interface. @@ -36,6 +45,39 @@ func NewMyGAPI(data gapi.Data, name string, interval uint) (*MyGAPI, error) { return obj, obj.Init(data) } +// Cli takes a cli.Context, and returns our GAPI if activated. All arguments +// should take the prefix of the registered name. On activation, if there are +// any validation problems, you should return an error. If this was not +// activated, then you should return a nil GAPI and a nil error. +func (obj *MyGAPI) Cli(c *cli.Context, fs resources.Fs) (*gapi.Deploy, error) { + if s := c.String(obj.Name); c.IsSet(obj.Name) { + if s != "" { + return nil, fmt.Errorf("input is not empty") + } + + return &gapi.Deploy{ + Name: obj.Name, + Noop: c.GlobalBool("noop"), + Sema: c.GlobalInt("sema"), + GAPI: &MyGAPI{ + // TODO: add properties here... + }, + }, nil + } + return nil, nil // we weren't activated! +} + +// CliFlags returns a list of flags used by this deploy subcommand. +func (obj *MyGAPI) CliFlags() []cli.Flag { + return []cli.Flag{ + cli.StringFlag{ + Name: obj.Name, + Value: "", + Usage: "run", + }, + } +} + // Init initializes the MyGAPI struct. func (obj *MyGAPI) Init(data gapi.Data) error { if obj.initialized { @@ -53,7 +95,7 @@ func (obj *MyGAPI) Init(data gapi.Data) error { // Graph returns a current Graph. func (obj *MyGAPI) Graph() (*pgraph.Graph, error) { if !obj.initialized { - return nil, fmt.Errorf("libmgmt: MyGAPI is not initialized") + return nil, fmt.Errorf("%s: MyGAPI is not initialized", Name) } g, err := pgraph.NewGraph(obj.Name) @@ -132,7 +174,7 @@ func (obj *MyGAPI) Next() chan gapi.Next { defer close(ch) // this will run before the obj.wg.Done() if !obj.initialized { next := gapi.Next{ - Err: fmt.Errorf("libmgmt: MyGAPI is not initialized"), + Err: fmt.Errorf("%s: MyGAPI is not initialized", Name), Exit: true, // exit, b/c programming error? } ch <- next @@ -161,7 +203,7 @@ func (obj *MyGAPI) Next() chan gapi.Next { return } - log.Printf("libmgmt: Generating new graph...") + log.Printf("%s: Generating new graph...", Name) select { case ch <- gapi.Next{}: // trigger a run case <-obj.closeChan: @@ -175,7 +217,7 @@ func (obj *MyGAPI) Next() chan gapi.Next { // Close shuts down the MyGAPI. func (obj *MyGAPI) Close() error { if !obj.initialized { - return fmt.Errorf("libmgmt: MyGAPI is not initialized") + return fmt.Errorf("%s: MyGAPI is not initialized", Name) } close(obj.closeChan) obj.wg.Wait() @@ -196,10 +238,10 @@ func Run() error { obj.ConvergedTimeout = -1 obj.Noop = false // FIXME: careful! - obj.GAPI = &MyGAPI{ // graph API - Name: "libmgmt", // TODO: set on compilation - Interval: 60 * 10, // arbitrarily change graph every 15 seconds - } + //obj.GAPI = &MyGAPI{ // graph API + // Name: "libmgmt", // TODO: set on compilation + // Interval: 60 * 10, // arbitrarily change graph every 15 seconds + //} if err := obj.Init(); err != nil { return err diff --git a/examples/lib/libmgmt1.go b/examples/lib/libmgmt1.go index 59169b41..4bced6e7 100644 --- a/examples/lib/libmgmt1.go +++ b/examples/lib/libmgmt1.go @@ -15,6 +15,15 @@ import ( "github.com/purpleidea/mgmt/pgraph" "github.com/purpleidea/mgmt/resources" "github.com/purpleidea/mgmt/yamlgraph" + + "github.com/urfave/cli" +) + +// XXX: this has not been updated to latest GAPI/Deploy API. Patches welcome! + +const ( + // Name is the name of this frontend. + Name = "libmgmt" ) // MyGAPI implements the main GAPI interface. @@ -37,6 +46,39 @@ func NewMyGAPI(data gapi.Data, name string, interval uint) (*MyGAPI, error) { return obj, obj.Init(data) } +// Cli takes a cli.Context, and returns our GAPI if activated. All arguments +// should take the prefix of the registered name. On activation, if there are +// any validation problems, you should return an error. If this was not +// activated, then you should return a nil GAPI and a nil error. +func (obj *MyGAPI) Cli(c *cli.Context, fs resources.Fs) (*gapi.Deploy, error) { + if s := c.String(obj.Name); c.IsSet(obj.Name) { + if s != "" { + return nil, fmt.Errorf("input is not empty") + } + + return &gapi.Deploy{ + Name: obj.Name, + Noop: c.GlobalBool("noop"), + Sema: c.GlobalInt("sema"), + GAPI: &MyGAPI{ + // TODO: add properties here... + }, + }, nil + } + return nil, nil // we weren't activated! +} + +// CliFlags returns a list of flags used by this deploy subcommand. +func (obj *MyGAPI) CliFlags() []cli.Flag { + return []cli.Flag{ + cli.StringFlag{ + Name: obj.Name, + Value: "", + Usage: "run", + }, + } +} + // Init initializes the MyGAPI struct. func (obj *MyGAPI) Init(data gapi.Data) error { if obj.initialized { @@ -54,7 +96,7 @@ func (obj *MyGAPI) Init(data gapi.Data) error { // Graph returns a current Graph. func (obj *MyGAPI) Graph() (*pgraph.Graph, error) { if !obj.initialized { - return nil, fmt.Errorf("libmgmt: MyGAPI is not initialized") + return nil, fmt.Errorf("%s: MyGAPI is not initialized", Name) } n1, err := resources.NewNamedResource("noop", "noop1") @@ -96,7 +138,7 @@ func (obj *MyGAPI) Next() chan gapi.Next { defer close(ch) // this will run before the obj.wg.Done() if !obj.initialized { next := gapi.Next{ - Err: fmt.Errorf("libmgmt: MyGAPI is not initialized"), + Err: fmt.Errorf("%s: MyGAPI is not initialized", Name), Exit: true, // exit, b/c programming error? } ch <- next @@ -124,7 +166,7 @@ func (obj *MyGAPI) Next() chan gapi.Next { return } - log.Printf("libmgmt: Generating new graph...") + log.Printf("%s: Generating new graph...", Name) select { case ch <- gapi.Next{}: // trigger a run case <-obj.closeChan: @@ -138,7 +180,7 @@ func (obj *MyGAPI) Next() chan gapi.Next { // Close shuts down the MyGAPI. func (obj *MyGAPI) Close() error { if !obj.initialized { - return fmt.Errorf("libmgmt: MyGAPI is not initialized") + return fmt.Errorf("%s: MyGAPI is not initialized", Name) } close(obj.closeChan) obj.wg.Wait() @@ -157,10 +199,10 @@ func Run() error { obj.ConvergedTimeout = -1 obj.Noop = true - obj.GAPI = &MyGAPI{ // graph API - Name: "libmgmt", // TODO: set on compilation - Interval: 15, // arbitrarily change graph every 15 seconds - } + //obj.GAPI = &MyGAPI{ // graph API + // Name: "libmgmt", // TODO: set on compilation + // Interval: 15, // arbitrarily change graph every 15 seconds + //} if err := obj.Init(); err != nil { return err diff --git a/examples/lib/libmgmt2.go b/examples/lib/libmgmt2.go index f0f52422..fff27182 100644 --- a/examples/lib/libmgmt2.go +++ b/examples/lib/libmgmt2.go @@ -15,6 +15,15 @@ import ( mgmt "github.com/purpleidea/mgmt/lib" "github.com/purpleidea/mgmt/pgraph" "github.com/purpleidea/mgmt/resources" + + "github.com/urfave/cli" +) + +// XXX: this has not been updated to latest GAPI/Deploy API. Patches welcome! + +const ( + // Name is the name of this frontend. + Name = "libmgmt" ) // MyGAPI implements the main GAPI interface. @@ -39,6 +48,39 @@ func NewMyGAPI(data gapi.Data, name string, interval uint, count uint) (*MyGAPI, return obj, obj.Init(data) } +// Cli takes a cli.Context, and returns our GAPI if activated. All arguments +// should take the prefix of the registered name. On activation, if there are +// any validation problems, you should return an error. If this was not +// activated, then you should return a nil GAPI and a nil error. +func (obj *MyGAPI) Cli(c *cli.Context, fs resources.Fs) (*gapi.Deploy, error) { + if s := c.String(obj.Name); c.IsSet(obj.Name) { + if s != "" { + return nil, fmt.Errorf("input is not empty") + } + + return &gapi.Deploy{ + Name: obj.Name, + Noop: c.GlobalBool("noop"), + Sema: c.GlobalInt("sema"), + GAPI: &MyGAPI{ + // TODO: add properties here... + }, + }, nil + } + return nil, nil // we weren't activated! +} + +// CliFlags returns a list of flags used by this deploy subcommand. +func (obj *MyGAPI) CliFlags() []cli.Flag { + return []cli.Flag{ + cli.StringFlag{ + Name: obj.Name, + Value: "", + Usage: "run", + }, + } +} + // Init initializes the MyGAPI struct. func (obj *MyGAPI) Init(data gapi.Data) error { if obj.initialized { @@ -56,7 +98,7 @@ func (obj *MyGAPI) Init(data gapi.Data) error { // Graph returns a current Graph. func (obj *MyGAPI) Graph() (*pgraph.Graph, error) { if !obj.initialized { - return nil, fmt.Errorf("libmgmt: MyGAPI is not initialized") + return nil, fmt.Errorf("%s: MyGAPI is not initialized", Name) } g, err := pgraph.NewGraph(obj.Name) @@ -89,7 +131,7 @@ func (obj *MyGAPI) Next() chan gapi.Next { defer close(ch) // this will run before the obj.wg.Done() if !obj.initialized { next := gapi.Next{ - Err: fmt.Errorf("libmgmt: MyGAPI is not initialized"), + Err: fmt.Errorf("%s: MyGAPI is not initialized", Name), Exit: true, // exit, b/c programming error? } ch <- next @@ -117,7 +159,7 @@ func (obj *MyGAPI) Next() chan gapi.Next { return } - log.Printf("libmgmt: Generating new graph...") + log.Printf("%s: Generating new graph...", Name) select { case ch <- gapi.Next{}: // trigger a run case <-obj.closeChan: @@ -131,7 +173,7 @@ func (obj *MyGAPI) Next() chan gapi.Next { // Close shuts down the MyGAPI. func (obj *MyGAPI) Close() error { if !obj.initialized { - return fmt.Errorf("libmgmt: MyGAPI is not initialized") + return fmt.Errorf("%s: MyGAPI is not initialized", Name) } close(obj.closeChan) obj.wg.Wait() @@ -150,11 +192,11 @@ func Run(count uint) error { obj.ConvergedTimeout = -1 obj.Noop = true - obj.GAPI = &MyGAPI{ // graph API - Name: "libmgmt", // TODO: set on compilation - Count: count, // number of vertices to add - Interval: 15, // arbitrarily change graph every 15 seconds - } + //obj.GAPI = &MyGAPI{ // graph API + // Name: "libmgmt", // TODO: set on compilation + // Count: count, // number of vertices to add + // Interval: 15, // arbitrarily change graph every 15 seconds + //} if err := obj.Init(); err != nil { return err diff --git a/examples/lib/libmgmt3.go b/examples/lib/libmgmt3.go index 98e0c2d6..6a231e6b 100644 --- a/examples/lib/libmgmt3.go +++ b/examples/lib/libmgmt3.go @@ -14,6 +14,15 @@ import ( mgmt "github.com/purpleidea/mgmt/lib" "github.com/purpleidea/mgmt/pgraph" "github.com/purpleidea/mgmt/resources" + + "github.com/urfave/cli" +) + +// XXX: this has not been updated to latest GAPI/Deploy API. Patches welcome! + +const ( + // Name is the name of this frontend. + Name = "libmgmt" ) // MyGAPI implements the main GAPI interface. @@ -36,6 +45,39 @@ func NewMyGAPI(data gapi.Data, name string, interval uint) (*MyGAPI, error) { return obj, obj.Init(data) } +// Cli takes a cli.Context, and returns our GAPI if activated. All arguments +// should take the prefix of the registered name. On activation, if there are +// any validation problems, you should return an error. If this was not +// activated, then you should return a nil GAPI and a nil error. +func (obj *MyGAPI) Cli(c *cli.Context, fs resources.Fs) (*gapi.Deploy, error) { + if s := c.String(obj.Name); c.IsSet(obj.Name) { + if s != "" { + return nil, fmt.Errorf("input is not empty") + } + + return &gapi.Deploy{ + Name: obj.Name, + Noop: c.GlobalBool("noop"), + Sema: c.GlobalInt("sema"), + GAPI: &MyGAPI{ + // TODO: add properties here... + }, + }, nil + } + return nil, nil // we weren't activated! +} + +// CliFlags returns a list of flags used by this deploy subcommand. +func (obj *MyGAPI) CliFlags() []cli.Flag { + return []cli.Flag{ + cli.StringFlag{ + Name: obj.Name, + Value: "", + Usage: "run", + }, + } +} + // Init initializes the MyGAPI struct. func (obj *MyGAPI) Init(data gapi.Data) error { if obj.initialized { @@ -53,7 +95,7 @@ func (obj *MyGAPI) Init(data gapi.Data) error { // Graph returns a current Graph. func (obj *MyGAPI) Graph() (*pgraph.Graph, error) { if !obj.initialized { - return nil, fmt.Errorf("libmgmt: MyGAPI is not initialized") + return nil, fmt.Errorf("%s: MyGAPI is not initialized", Name) } g, err := pgraph.NewGraph(obj.Name) @@ -138,7 +180,7 @@ func (obj *MyGAPI) Next() chan gapi.Next { defer close(ch) // this will run before the obj.wg.Done() if !obj.initialized { next := gapi.Next{ - Err: fmt.Errorf("libmgmt: MyGAPI is not initialized"), + Err: fmt.Errorf("%s: MyGAPI is not initialized", Name), Exit: true, // exit, b/c programming error? } ch <- next @@ -166,7 +208,7 @@ func (obj *MyGAPI) Next() chan gapi.Next { return } - log.Printf("libmgmt: Generating new graph...") + log.Printf("%s: Generating new graph...", Name) select { case ch <- gapi.Next{}: // trigger a run case <-obj.closeChan: @@ -180,7 +222,7 @@ func (obj *MyGAPI) Next() chan gapi.Next { // Close shuts down the MyGAPI. func (obj *MyGAPI) Close() error { if !obj.initialized { - return fmt.Errorf("libmgmt: MyGAPI is not initialized") + return fmt.Errorf("%s: MyGAPI is not initialized", Name) } close(obj.closeChan) obj.wg.Wait() @@ -201,10 +243,10 @@ func Run() error { obj.ConvergedTimeout = -1 obj.Noop = false // FIXME: careful! - obj.GAPI = &MyGAPI{ // graph API - Name: "libmgmt", // TODO: set on compilation - Interval: 60 * 10, // arbitrarily change graph every 15 seconds - } + //obj.GAPI = &MyGAPI{ // graph API + // Name: "libmgmt", // TODO: set on compilation + // Interval: 60 * 10, // arbitrarily change graph every 15 seconds + //} if err := obj.Init(); err != nil { return err diff --git a/examples/augeas1.yaml b/examples/yaml/augeas1.yaml similarity index 100% rename from examples/augeas1.yaml rename to examples/yaml/augeas1.yaml diff --git a/examples/autoedges1.yaml b/examples/yaml/autoedges1.yaml similarity index 100% rename from examples/autoedges1.yaml rename to examples/yaml/autoedges1.yaml diff --git a/examples/autoedges2.yaml b/examples/yaml/autoedges2.yaml similarity index 100% rename from examples/autoedges2.yaml rename to examples/yaml/autoedges2.yaml diff --git a/examples/autoedges3.yaml b/examples/yaml/autoedges3.yaml similarity index 100% rename from examples/autoedges3.yaml rename to examples/yaml/autoedges3.yaml diff --git a/examples/autoedges4.yaml b/examples/yaml/autoedges4.yaml similarity index 100% rename from examples/autoedges4.yaml rename to examples/yaml/autoedges4.yaml diff --git a/examples/autoedges5.yaml b/examples/yaml/autoedges5.yaml similarity index 100% rename from examples/autoedges5.yaml rename to examples/yaml/autoedges5.yaml diff --git a/examples/autogroup1.yaml b/examples/yaml/autogroup1.yaml similarity index 100% rename from examples/autogroup1.yaml rename to examples/yaml/autogroup1.yaml diff --git a/examples/autogroup2.yaml b/examples/yaml/autogroup2.yaml similarity index 100% rename from examples/autogroup2.yaml rename to examples/yaml/autogroup2.yaml diff --git a/examples/aws_ec2_1.yaml b/examples/yaml/aws_ec2_1.yaml similarity index 100% rename from examples/aws_ec2_1.yaml rename to examples/yaml/aws_ec2_1.yaml diff --git a/examples/deep-dirs.yaml b/examples/yaml/deep-dirs.yaml similarity index 100% rename from examples/deep-dirs.yaml rename to examples/yaml/deep-dirs.yaml diff --git a/examples/etcd1a.yaml b/examples/yaml/etcd1a.yaml similarity index 100% rename from examples/etcd1a.yaml rename to examples/yaml/etcd1a.yaml diff --git a/examples/etcd1b.yaml b/examples/yaml/etcd1b.yaml similarity index 100% rename from examples/etcd1b.yaml rename to examples/yaml/etcd1b.yaml diff --git a/examples/etcd1c.yaml b/examples/yaml/etcd1c.yaml similarity index 100% rename from examples/etcd1c.yaml rename to examples/yaml/etcd1c.yaml diff --git a/examples/etcd1d.yaml b/examples/yaml/etcd1d.yaml similarity index 100% rename from examples/etcd1d.yaml rename to examples/yaml/etcd1d.yaml diff --git a/examples/etcd1e.yaml b/examples/yaml/etcd1e.yaml similarity index 100% rename from examples/etcd1e.yaml rename to examples/yaml/etcd1e.yaml diff --git a/examples/exec1.yaml b/examples/yaml/exec1.yaml similarity index 100% rename from examples/exec1.yaml rename to examples/yaml/exec1.yaml diff --git a/examples/exec1a.yaml b/examples/yaml/exec1a.yaml similarity index 100% rename from examples/exec1a.yaml rename to examples/yaml/exec1a.yaml diff --git a/examples/exec1b.yaml b/examples/yaml/exec1b.yaml similarity index 100% rename from examples/exec1b.yaml rename to examples/yaml/exec1b.yaml diff --git a/examples/exec1c.yaml b/examples/yaml/exec1c.yaml similarity index 100% rename from examples/exec1c.yaml rename to examples/yaml/exec1c.yaml diff --git a/examples/exec1d.yaml b/examples/yaml/exec1d.yaml similarity index 100% rename from examples/exec1d.yaml rename to examples/yaml/exec1d.yaml diff --git a/examples/exec2.yaml b/examples/yaml/exec2.yaml similarity index 100% rename from examples/exec2.yaml rename to examples/yaml/exec2.yaml diff --git a/examples/exec3-sema.yaml b/examples/yaml/exec3-sema.yaml similarity index 100% rename from examples/exec3-sema.yaml rename to examples/yaml/exec3-sema.yaml diff --git a/examples/exec3.yaml b/examples/yaml/exec3.yaml similarity index 100% rename from examples/exec3.yaml rename to examples/yaml/exec3.yaml diff --git a/examples/file0.yaml b/examples/yaml/file0.yaml similarity index 100% rename from examples/file0.yaml rename to examples/yaml/file0.yaml diff --git a/examples/file1.yaml b/examples/yaml/file1.yaml similarity index 100% rename from examples/file1.yaml rename to examples/yaml/file1.yaml diff --git a/examples/file2.yaml b/examples/yaml/file2.yaml similarity index 100% rename from examples/file2.yaml rename to examples/yaml/file2.yaml diff --git a/examples/file3.yaml b/examples/yaml/file3.yaml similarity index 100% rename from examples/file3.yaml rename to examples/yaml/file3.yaml diff --git a/examples/file4.yaml b/examples/yaml/file4.yaml similarity index 100% rename from examples/file4.yaml rename to examples/yaml/file4.yaml diff --git a/examples/graph0.yaml b/examples/yaml/graph0.yaml similarity index 100% rename from examples/graph0.yaml rename to examples/yaml/graph0.yaml diff --git a/examples/graph10.yaml b/examples/yaml/graph10.yaml similarity index 100% rename from examples/graph10.yaml rename to examples/yaml/graph10.yaml diff --git a/examples/graph1a.yaml b/examples/yaml/graph1a.yaml similarity index 100% rename from examples/graph1a.yaml rename to examples/yaml/graph1a.yaml diff --git a/examples/graph1b.yaml b/examples/yaml/graph1b.yaml similarity index 100% rename from examples/graph1b.yaml rename to examples/yaml/graph1b.yaml diff --git a/examples/graph3a.yaml b/examples/yaml/graph3a.yaml similarity index 100% rename from examples/graph3a.yaml rename to examples/yaml/graph3a.yaml diff --git a/examples/graph3b.yaml b/examples/yaml/graph3b.yaml similarity index 100% rename from examples/graph3b.yaml rename to examples/yaml/graph3b.yaml diff --git a/examples/graph3c.yaml b/examples/yaml/graph3c.yaml similarity index 100% rename from examples/graph3c.yaml rename to examples/yaml/graph3c.yaml diff --git a/examples/graph4.yaml b/examples/yaml/graph4.yaml similarity index 100% rename from examples/graph4.yaml rename to examples/yaml/graph4.yaml diff --git a/examples/graph5.yaml b/examples/yaml/graph5.yaml similarity index 100% rename from examples/graph5.yaml rename to examples/yaml/graph5.yaml diff --git a/examples/graph6.yaml b/examples/yaml/graph6.yaml similarity index 100% rename from examples/graph6.yaml rename to examples/yaml/graph6.yaml diff --git a/examples/graph7.yaml b/examples/yaml/graph7.yaml similarity index 100% rename from examples/graph7.yaml rename to examples/yaml/graph7.yaml diff --git a/examples/graph9.yaml b/examples/yaml/graph9.yaml similarity index 100% rename from examples/graph9.yaml rename to examples/yaml/graph9.yaml diff --git a/examples/group1.yaml b/examples/yaml/group1.yaml similarity index 100% rename from examples/group1.yaml rename to examples/yaml/group1.yaml diff --git a/examples/hostname.yaml b/examples/yaml/hostname.yaml similarity index 100% rename from examples/hostname.yaml rename to examples/yaml/hostname.yaml diff --git a/examples/kv1.yaml b/examples/yaml/kv1.yaml similarity index 100% rename from examples/kv1.yaml rename to examples/yaml/kv1.yaml diff --git a/examples/kv2.yaml b/examples/yaml/kv2.yaml similarity index 100% rename from examples/kv2.yaml rename to examples/yaml/kv2.yaml diff --git a/examples/kv3.yaml b/examples/yaml/kv3.yaml similarity index 100% rename from examples/kv3.yaml rename to examples/yaml/kv3.yaml diff --git a/examples/kv4.yaml b/examples/yaml/kv4.yaml similarity index 100% rename from examples/kv4.yaml rename to examples/yaml/kv4.yaml diff --git a/examples/limit1.yaml b/examples/yaml/limit1.yaml similarity index 100% rename from examples/limit1.yaml rename to examples/yaml/limit1.yaml diff --git a/examples/msg1.yaml b/examples/yaml/msg1.yaml similarity index 100% rename from examples/msg1.yaml rename to examples/yaml/msg1.yaml diff --git a/examples/noop0.yaml b/examples/yaml/noop0.yaml similarity index 100% rename from examples/noop0.yaml rename to examples/yaml/noop0.yaml diff --git a/examples/noop1.yaml b/examples/yaml/noop1.yaml similarity index 100% rename from examples/noop1.yaml rename to examples/yaml/noop1.yaml diff --git a/examples/noop2.yaml b/examples/yaml/noop2.yaml similarity index 100% rename from examples/noop2.yaml rename to examples/yaml/noop2.yaml diff --git a/examples/nspawn1.yaml b/examples/yaml/nspawn1.yaml similarity index 100% rename from examples/nspawn1.yaml rename to examples/yaml/nspawn1.yaml diff --git a/examples/pkg1.yaml b/examples/yaml/pkg1.yaml similarity index 100% rename from examples/pkg1.yaml rename to examples/yaml/pkg1.yaml diff --git a/examples/pkg2.yaml b/examples/yaml/pkg2.yaml similarity index 100% rename from examples/pkg2.yaml rename to examples/yaml/pkg2.yaml diff --git a/examples/poll1.yaml b/examples/yaml/poll1.yaml similarity index 100% rename from examples/poll1.yaml rename to examples/yaml/poll1.yaml diff --git a/examples/remote1.yaml b/examples/yaml/remote1.yaml similarity index 100% rename from examples/remote1.yaml rename to examples/yaml/remote1.yaml diff --git a/examples/remote2a.yaml b/examples/yaml/remote2a.yaml similarity index 100% rename from examples/remote2a.yaml rename to examples/yaml/remote2a.yaml diff --git a/examples/remote2b.yaml b/examples/yaml/remote2b.yaml similarity index 100% rename from examples/remote2b.yaml rename to examples/yaml/remote2b.yaml diff --git a/examples/retry1.yaml b/examples/yaml/retry1.yaml similarity index 100% rename from examples/retry1.yaml rename to examples/yaml/retry1.yaml diff --git a/examples/svc1.yaml b/examples/yaml/svc1.yaml similarity index 100% rename from examples/svc1.yaml rename to examples/yaml/svc1.yaml diff --git a/examples/svc2.yaml b/examples/yaml/svc2.yaml similarity index 100% rename from examples/svc2.yaml rename to examples/yaml/svc2.yaml diff --git a/examples/timer1.yaml b/examples/yaml/timer1.yaml similarity index 100% rename from examples/timer1.yaml rename to examples/yaml/timer1.yaml diff --git a/examples/timer2.yaml b/examples/yaml/timer2.yaml similarity index 100% rename from examples/timer2.yaml rename to examples/yaml/timer2.yaml diff --git a/examples/user1.yaml b/examples/yaml/user1.yaml similarity index 100% rename from examples/user1.yaml rename to examples/yaml/user1.yaml diff --git a/examples/virt1.yaml b/examples/yaml/virt1.yaml similarity index 100% rename from examples/virt1.yaml rename to examples/yaml/virt1.yaml diff --git a/examples/virt2.yaml b/examples/yaml/virt2.yaml similarity index 100% rename from examples/virt2.yaml rename to examples/yaml/virt2.yaml diff --git a/examples/virt3.yaml b/examples/yaml/virt3.yaml similarity index 100% rename from examples/virt3.yaml rename to examples/yaml/virt3.yaml diff --git a/examples/virt4.yaml b/examples/yaml/virt4.yaml similarity index 100% rename from examples/virt4.yaml rename to examples/yaml/virt4.yaml diff --git a/gapi/deploy.go b/gapi/deploy.go new file mode 100644 index 00000000..594fe3a7 --- /dev/null +++ b/gapi/deploy.go @@ -0,0 +1,68 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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 gapi + +import ( + "bytes" + "encoding/base64" + "encoding/gob" + + errwrap "github.com/pkg/errors" +) + +func init() { + gob.Register(&Deploy{}) +} + +// Deploy represents a deploy action, include the type of GAPI to deploy, the +// payload of that GAPI, and any deploy specific parameters that were chosen. +// TODO: add staged rollout functionality to this struct +// TODO: add proper authentication with gpg key signing +type Deploy struct { + Name string // lang, hcl, puppet, yaml, yaml2, etc... + //Sync bool // wait for everyone to close previous GAPI before switching + Noop bool + Sema int // sema override + GAPI GAPI +} + +// ToB64 encodes a deploy struct as a base64 encoded string. +func (obj *Deploy) ToB64() (string, error) { + b := bytes.Buffer{} + e := gob.NewEncoder(&b) + err := e.Encode(&obj) // pass with & + if err != nil { + return "", errwrap.Wrapf(err, "gob failed to encode") + } + return base64.StdEncoding.EncodeToString(b.Bytes()), nil +} + +// NewDeployFromB64 decodes a deploy struct from a base64 encoded string. +func NewDeployFromB64(str string) (*Deploy, error) { + var deploy *Deploy + bb, err := base64.StdEncoding.DecodeString(str) + if err != nil { + return nil, errwrap.Wrapf(err, "base64 failed to decode") + } + b := bytes.NewBuffer(bb) + d := gob.NewDecoder(b) + if err := d.Decode(&deploy); err != nil { // pass with & + return nil, errwrap.Wrapf(err, "gob failed to decode") + } + return deploy, nil +} diff --git a/gapi/gapi.go b/gapi/gapi.go index f8fa232d..0d5403ab 100644 --- a/gapi/gapi.go +++ b/gapi/gapi.go @@ -19,17 +19,39 @@ package gapi import ( + "encoding/gob" + "fmt" + "github.com/purpleidea/mgmt/pgraph" "github.com/purpleidea/mgmt/resources" + + "github.com/urfave/cli" ) +// RegisteredGAPIs is a global map of all possible GAPIs which can be used. You +// should never touch this map directly. Use methods like Register instead. +var RegisteredGAPIs = make(map[string]func() GAPI) // must initialize this map + +// Register takes a GAPI and its name and makes it available for use. There is +// no matching Unregister function. +func Register(name string, fn func() GAPI) { + if _, ok := RegisteredGAPIs[name]; ok { + panic(fmt.Sprintf("a GAPI named %s is already registered", name)) + } + gob.Register(fn()) + RegisteredGAPIs[name] = fn +} + // Data is the set of input values passed into the GAPI structs via Init. type Data struct { + Program string // name of the originating program Hostname string // uuid for the host, required for GAPI World resources.World Noop bool NoConfigWatch bool NoStreamWatch bool + Debug bool + Logf func(format string, v ...interface{}) // NOTE: we can add more fields here if needed by GAPI endpoints } @@ -48,6 +70,9 @@ type Next struct { // GAPI is a Graph API that represents incoming graphs and change streams. type GAPI interface { + Cli(c *cli.Context, fs resources.Fs) (*Deploy, error) + CliFlags() []cli.Flag + Init(Data) error // initializes the GAPI and passes in useful data Graph() (*pgraph.Graph, error) // returns the most recent pgraph Next() chan Next // returns a stream of switch events diff --git a/gapi/helpers.go b/gapi/helpers.go new file mode 100644 index 00000000..c130458a --- /dev/null +++ b/gapi/helpers.go @@ -0,0 +1,57 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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 gapi + +import ( + "io/ioutil" + + "github.com/purpleidea/mgmt/resources" + "github.com/purpleidea/mgmt/util" + + errwrap "github.com/pkg/errors" +) + +// Umask is the default to use when none has been specified. +// TODO: apparently using 0666 is equivalent to respecting the current umask +const Umask = 0666 + +// CopyFileToFs copies a file from src path on the local fs to a dst path on fs. +func CopyFileToFs(fs resources.Fs, src, dst string) error { + data, err := ioutil.ReadFile(src) + if err != nil { + return errwrap.Wrapf(err, "can't read from file `%s`", src) + } + if err := fs.WriteFile(dst, data, Umask); err != nil { + return errwrap.Wrapf(err, "can't write to file `%s`", dst) + } + return nil +} + +// CopyStringToFs copies a file from src path on the local fs to a dst path on +// fs. +func CopyStringToFs(fs resources.Fs, str, dst string) error { + if err := fs.WriteFile(dst, []byte(str), Umask); err != nil { + return errwrap.Wrapf(err, "can't write to file `%s`", dst) + } + return nil +} + +// CopyDirToFs copies a dir from src path on the local fs to a dst path on fs. +func CopyDirToFs(fs resources.Fs, src, dst string) error { + return util.CopyDiskToFs(fs, src, dst, false) +} diff --git a/hcl/gapi.go b/hcl/gapi.go index 5615daef..9aefc651 100644 --- a/hcl/gapi.go +++ b/hcl/gapi.go @@ -25,11 +25,26 @@ import ( "github.com/purpleidea/mgmt/gapi" "github.com/purpleidea/mgmt/pgraph" "github.com/purpleidea/mgmt/recwatch" + "github.com/purpleidea/mgmt/resources" + + errwrap "github.com/pkg/errors" + "github.com/urfave/cli" ) +const ( + // Name is the name of this frontend. + Name = "hcl" + // Start is the entry point filename that we use. It is arbitrary. + Start = "/start.hcl" +) + +func init() { + gapi.Register(Name, func() gapi.GAPI { return &GAPI{} }) // register +} + // GAPI ... type GAPI struct { - File *string + InputURI string initialized bool data gapi.Data @@ -38,16 +53,43 @@ type GAPI struct { configWatcher *recwatch.ConfigWatcher } -// NewGAPI ... -func NewGAPI(data gapi.Data, file *string) (*GAPI, error) { - if file == nil { - return nil, fmt.Errorf("empty file given") - } +// Cli takes a cli.Context, and returns our GAPI if activated. All arguments +// should take the prefix of the registered name. On activation, if there are +// any validation problems, you should return an error. If this was not +// activated, then you should return a nil GAPI and a nil error. +func (obj *GAPI) Cli(c *cli.Context, fs resources.Fs) (*gapi.Deploy, error) { + if s := c.String(Name); c.IsSet(Name) { + if s == "" { + return nil, fmt.Errorf("%s input is empty", Name) + } - obj := &GAPI{ - File: file, + // TODO: single file input for now + if err := gapi.CopyFileToFs(fs, s, Start); err != nil { + return nil, errwrap.Wrapf(err, "can't copy code from `%s` to `%s`", s, Start) + } + + return &gapi.Deploy{ + Name: Name, + Noop: c.GlobalBool("noop"), + Sema: c.GlobalInt("sema"), + GAPI: &GAPI{ + InputURI: fs.URI(), + // TODO: add properties here... + }, + }, nil + } + return nil, nil // we weren't activated! +} + +// CliFlags returns a list of flags used by this deploy subcommand. +func (obj *GAPI) CliFlags() []cli.Flag { + return []cli.Flag{ + cli.StringFlag{ + Name: fmt.Sprintf("%s", Name), + Value: "", + Usage: "hcl graph definition to run", + }, } - return obj, obj.Init(data) } // Init ... @@ -55,11 +97,9 @@ func (obj *GAPI) Init(d gapi.Data) error { if obj.initialized { return fmt.Errorf("already initialized") } - - if obj.File == nil { - return fmt.Errorf("file cannot be nil") + if obj.InputURI == "" { + return fmt.Errorf("the InputURI param must be specified") } - obj.data = d obj.closeChan = make(chan struct{}) obj.initialized = true @@ -70,7 +110,21 @@ func (obj *GAPI) Init(d gapi.Data) error { // Graph ... func (obj *GAPI) Graph() (*pgraph.Graph, error) { - config, err := loadHcl(obj.File) + if !obj.initialized { + return nil, fmt.Errorf("%s: GAPI is not initialized", Name) + } + + fs, err := obj.data.World.Fs(obj.InputURI) // open the remote file system + if err != nil { + return nil, errwrap.Wrapf(err, "can't load code from file system `%s`", obj.InputURI) + } + + b, err := fs.ReadFile(Start) // read the single file out of it + if err != nil { + return nil, errwrap.Wrapf(err, "can't read code from file `%s`", Start) + } + + config, err := loadHcl(b) if err != nil { return nil, fmt.Errorf("unable to parse graph: %s", err) } @@ -88,7 +142,7 @@ func (obj *GAPI) Next() chan gapi.Next { defer close(ch) if !obj.initialized { next := gapi.Next{ - Err: fmt.Errorf("hcl: GAPI is not initialized"), + Err: fmt.Errorf("%s: GAPI is not initialized", Name), Exit: true, } ch <- next @@ -97,12 +151,7 @@ func (obj *GAPI) Next() chan gapi.Next { startChan := make(chan struct{}) // start signal close(startChan) // kick it off! - watchChan, configChan := make(chan error), make(chan error) - if obj.data.NoConfigWatch { - configChan = nil - } else { - configChan = obj.configWatcher.ConfigWatch(*obj.File) // simple - } + watchChan := make(chan error) if obj.data.NoStreamWatch { watchChan = nil } else { @@ -117,7 +166,6 @@ func (obj *GAPI) Next() chan gapi.Next { case <-startChan: startChan = nil case err, ok = <-watchChan: - case err, ok = <-configChan: if !ok { return } @@ -125,7 +173,7 @@ func (obj *GAPI) Next() chan gapi.Next { return } - log.Printf("hcl: generating new graph") + log.Printf("%s: generating new graph", Name) next := gapi.Next{ Err: err, } @@ -144,7 +192,7 @@ func (obj *GAPI) Next() chan gapi.Next { // Close ... func (obj *GAPI) Close() error { if !obj.initialized { - return fmt.Errorf("hcl: GAPI is not initialized") + return fmt.Errorf("%s: GAPI is not initialized", Name) } obj.configWatcher.Close() diff --git a/hil/interpolate.go b/hcl/hil/interpolate.go similarity index 100% rename from hil/interpolate.go rename to hcl/hil/interpolate.go diff --git a/hcl/parse.go b/hcl/parse.go index 6a50f035..832d89cc 100644 --- a/hcl/parse.go +++ b/hcl/parse.go @@ -19,7 +19,6 @@ package hcl import ( "fmt" - "io/ioutil" "log" "strings" @@ -27,7 +26,7 @@ import ( "github.com/hashicorp/hcl/hcl/ast" "github.com/hashicorp/hil" "github.com/purpleidea/mgmt/gapi" - hv "github.com/purpleidea/mgmt/hil" + hv "github.com/purpleidea/mgmt/hcl/hil" "github.com/purpleidea/mgmt/pgraph" "github.com/purpleidea/mgmt/resources" ) @@ -236,14 +235,9 @@ func graphFromConfig(c *Config, data gapi.Data) (*pgraph.Graph, error) { return graph, nil } -func loadHcl(f *string) (*Config, error) { - if f == nil { - return nil, fmt.Errorf("empty file given") - } - - data, err := ioutil.ReadFile(*f) - if err != nil { - return nil, fmt.Errorf("unable to read file: %v", err) +func loadHcl(data []byte) (*Config, error) { + if len(data) == 0 { + return nil, fmt.Errorf("empty data given") } file, err := hcl.ParseBytes(data) diff --git a/lang/.gitignore b/lang/.gitignore new file mode 100644 index 00000000..f6e322fe --- /dev/null +++ b/lang/.gitignore @@ -0,0 +1,3 @@ +lexer.nn.go +y.go +y.output diff --git a/lang/Makefile b/lang/Makefile new file mode 100644 index 00000000..8af4c522 --- /dev/null +++ b/lang/Makefile @@ -0,0 +1,41 @@ +# Mgmt +# Copyright (C) 2013-2018+ 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 . + +SHELL = /usr/bin/env bash +.PHONY: all build clean + +OLDGOYACC := $(shell go version | grep -E 'go1.6|go1.7') + +all: build + +build: lexer.nn.go y.go + +clean: + rm lexer.nn.go y.go y.output || true + +lexer.nn.go: lexer.nex + nex -e lexer.nex + @ROOT="$$( cd "$$( dirname "$${BASH_SOURCE[0]}" )" && cd .. && pwd )" && $$ROOT/misc/header.sh 'lexer.nn.go' + +y.go: parser.y +ifneq ($(OLDGOYACC),) + go tool yacc parser.y +else + @# newer golang versions moved yacc into: golang.org/x/tools/cmd/goyacc + goyacc parser.y +endif + @ROOT="$$( cd "$$( dirname "$${BASH_SOURCE[0]}" )" && cd .. && pwd )" && $$ROOT/misc/header.sh 'y.go' diff --git a/lang/funcs/contains_polyfunc.go b/lang/funcs/contains_polyfunc.go new file mode 100644 index 00000000..8bde41ee --- /dev/null +++ b/lang/funcs/contains_polyfunc.go @@ -0,0 +1,223 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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 funcs + +import ( + "fmt" + + "github.com/purpleidea/mgmt/lang/interfaces" + "github.com/purpleidea/mgmt/lang/types" + + errwrap "github.com/pkg/errors" +) + +const ( + // ContainsFuncName is the name this function is registered as. This + // starts with an underscore so that it cannot be used from the lexer. + // XXX: change to _contains and add syntax in the lexer/parser + ContainsFuncName = "contains" +) + +func init() { + Register(ContainsFuncName, func() interfaces.Func { return &ContainsPolyFunc{} }) // must register the func and name +} + +// ContainsPolyFunc returns true if a value is found in a list. Otherwise false. +type ContainsPolyFunc struct { + Type *types.Type // this is the type of value stored in our list + + init *interfaces.Init + last types.Value // last value received to use for diff + + result types.Value // last calculated output + + closeChan chan struct{} +} + +// Polymorphisms returns the list of possible function signatures available for +// this static polymorphic function. It relies on type and value hints to limit +// the number of returned possibilities. +func (obj *ContainsPolyFunc) Polymorphisms(partialType *types.Type, partialValues []types.Value) ([]*types.Type, error) { + // TODO: return `variant` as arg for now -- maybe there's a better way? + variant := []*types.Type{types.NewType("func(needle variant, haystack variant) bool")} + + if partialType == nil { + return variant, nil + } + + var typ *types.Type + + ord := partialType.Ord + if partialType.Map != nil { + if len(ord) != 2 { + return nil, fmt.Errorf("must have exactly three args in contains func") + } + if tNeedle, exists := partialType.Map[ord[0]]; exists && tNeedle != nil { + typ = tNeedle // solved + } + if tHaystack, exists := partialType.Map[ord[1]]; exists && tHaystack != nil { + if tHaystack.Kind != types.KindList { + return nil, fmt.Errorf("second arg must be of kind list") + } + if typ != nil && typ.Cmp(tHaystack.Val) != nil { + return nil, fmt.Errorf("list contents in second arg for contains must match search type") + } + typ = tHaystack.Val // solved + } + } + + if tOut := partialType.Out; tOut != nil { + if tOut.Kind != types.KindBool { + return nil, fmt.Errorf("return type must be a bool") + } + } + + if typ == nil { + return variant, nil + } + + typFunc := types.NewType(fmt.Sprintf("func(needle %s, haystack []%s) bool", typ.String(), typ.String())) + + // TODO: type check that the partialValues are compatible + + return []*types.Type{typFunc}, nil // solved! +} + +// Build is run to turn the polymorphic, undeterminted function, into the +// specific statically type version. It is usually run after Unify completes, +// and must be run before Info() and any of the other Func interface methods are +// used. This function is idempotent, as long as the arg isn't changed between +// runs. +func (obj *ContainsPolyFunc) Build(typ *types.Type) error { + // typ is the KindFunc signature we're trying to build... + if typ.Kind != types.KindFunc { + return fmt.Errorf("input type must be of kind func") + } + + if len(typ.Ord) != 2 { + return fmt.Errorf("the contains function needs exactly two args") + } + if typ.Out == nil { + return fmt.Errorf("return type of function must be specified") + } + if typ.Map == nil { + return fmt.Errorf("invalid input type") + } + + tNeedle, exists := typ.Map[typ.Ord[0]] + if !exists || tNeedle == nil { + return fmt.Errorf("first arg must be specified") + } + + tHaystack, exists := typ.Map[typ.Ord[1]] + if !exists || tHaystack == nil { + return fmt.Errorf("second arg must be specified") + } + + if tHaystack.Kind != types.KindList { + return fmt.Errorf("second argument must be of kind list") + } + + if err := tHaystack.Val.Cmp(tNeedle); err != nil { + return errwrap.Wrapf(err, "type of first arg must match type of list elements in second arg") + } + + if err := typ.Out.Cmp(types.TypeBool); err != nil { + return errwrap.Wrapf(err, "return type must be a boolean") + } + + obj.Type = tNeedle // type of value stored in our list + return nil +} + +// Validate tells us if the input struct takes a valid form. +func (obj *ContainsPolyFunc) Validate() error { + if obj.Type == nil { // build must be run first + return fmt.Errorf("type is still unspecified") + } + return nil +} + +// Info returns some static info about itself. Build must be called before this +// will return correct data. +func (obj *ContainsPolyFunc) Info() *interfaces.Info { + typ := types.NewType(fmt.Sprintf("func(needle %s, haystack []%s) bool", obj.Type.String(), obj.Type.String())) + return &interfaces.Info{ + Pure: true, + Memo: false, + Sig: typ, // func kind + Err: obj.Validate(), + } +} + +// Init runs some startup code for this fact. +func (obj *ContainsPolyFunc) Init(init *interfaces.Init) error { + obj.init = init + obj.closeChan = make(chan struct{}) + return nil +} + +// Stream returns the changing values that this func has over time. +func (obj *ContainsPolyFunc) Stream() error { + defer close(obj.init.Output) // the sender closes + for { + select { + case input, ok := <-obj.init.Input: + if !ok { + return nil // can't output any more + } + //if err := input.Type().Cmp(obj.Info().Sig.Input); err != nil { + // return errwrap.Wrapf(err, "wrong function input") + //} + + if obj.last != nil && input.Cmp(obj.last) == nil { + continue // value didn't change, skip it + } + obj.last = input // store for next + + needle := input.Struct()["needle"] + haystack := (input.Struct()["haystack"]).(*types.ListValue) + + _, exists := haystack.Contains(needle) + var result types.Value = &types.BoolValue{V: exists} + + // if previous input was `2 + 4`, but now it + // changed to `1 + 5`, the result is still the + // same, so we can skip sending an update... + if obj.result != nil && result.Cmp(obj.result) == nil { + continue // result didn't change + } + obj.result = result // store new result + + case <-obj.closeChan: + return nil + } + + select { + case obj.init.Output <- obj.result: // send + case <-obj.closeChan: + return nil + } + } +} + +// Close runs some shutdown code for this fact and turns off the stream. +func (obj *ContainsPolyFunc) Close() error { + close(obj.closeChan) + return nil +} diff --git a/lang/funcs/core/exchange_polyfunc.go b/lang/funcs/core/exchange_polyfunc.go new file mode 100644 index 00000000..7c9d0576 --- /dev/null +++ b/lang/funcs/core/exchange_polyfunc.go @@ -0,0 +1,178 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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 core // TODO: should this be in its own individual package? + +import ( + "fmt" + + "github.com/purpleidea/mgmt/lang/funcs" + "github.com/purpleidea/mgmt/lang/interfaces" + "github.com/purpleidea/mgmt/lang/types" + + errwrap "github.com/pkg/errors" +) + +func init() { + funcs.Register("exchange", func() interfaces.Func { return &ExchangeFunc{} }) // must register the func and name +} + +// ExchangeFunc is special function which returns all the values of a given key +// in the exposed world, and sets it's own. +type ExchangeFunc struct { + init *interfaces.Init + + namespace string + value string + + last types.Value + result types.Value // last calculated output + + watchChan chan error + closeChan chan struct{} +} + +// Validate makes sure we've built our struct properly. It is usually unused for +// normal functions that users can use directly. +func (obj *ExchangeFunc) Validate() error { + return nil +} + +// Info returns some static info about itself. +func (obj *ExchangeFunc) Info() *interfaces.Info { + return &interfaces.Info{ + Pure: false, // definitely false + Memo: false, + // TODO: do we want to allow this to be statically polymorphic, + // and have value be any type we might want? + // output is map of: hostname => value + Sig: types.NewType("func(namespace str, value str) {str: str}"), + Err: obj.Validate(), + } +} + +// Init runs some startup code for this fact. +func (obj *ExchangeFunc) Init(init *interfaces.Init) error { + obj.init = init + obj.watchChan = make(chan error) // XXX: sender should close this, but did I implement that part yet??? + obj.closeChan = make(chan struct{}) + return nil +} + +// Stream returns the changing values that this func has over time. +func (obj *ExchangeFunc) Stream() error { + defer close(obj.init.Output) // the sender closes + for { + select { + // TODO: should this first chan be run as a priority channel to + // avoid some sort of glitch? is that even possible? can our + // hostname check with reality (below) fix that? + case input, ok := <-obj.init.Input: + if !ok { + obj.init.Input = nil // don't infinite loop back + continue // no more inputs, but don't return! + } + //if err := input.Type().Cmp(obj.Info().Sig.Input); err != nil { + // return errwrap.Wrapf(err, "wrong function input") + //} + + if obj.last != nil && input.Cmp(obj.last) == nil { + continue // value didn't change, skip it + } + obj.last = input // store for next + + namespace := input.Struct()["namespace"].Str() + if namespace == "" { + return fmt.Errorf("can't use an empty namespace") + } + if obj.init.Debug { + obj.init.Logf("namespace: %s", namespace) + } + + // TODO: support changing the namespace over time... + // TODO: possibly removing our stored value there first! + if obj.namespace == "" { + obj.namespace = namespace // store it + obj.watchChan = obj.init.World.StrMapWatch(obj.namespace) // watch for var changes + } else if obj.namespace != namespace { + return fmt.Errorf("can't change namespace, previously: `%s`", obj.namespace) + } + + value := input.Struct()["value"].Str() + if obj.init.Debug { + obj.init.Logf("value: %+v", value) + } + + if err := obj.init.World.StrMapSet(obj.namespace, value); err != nil { + return errwrap.Wrapf(err, "namespace write error of `%s` to `%s`", value, obj.namespace) + } + + continue // we get values on the watch chan, not here! + + case err, ok := <-obj.watchChan: + if !ok { // closed + // XXX: if we close, perhaps the engine is + // switching etcd hosts and we should retry? + // maybe instead we should get an "etcd + // reconnect" signal, and the lang will restart? + return nil + } + if err != nil { + return errwrap.Wrapf(err, "channel watch failed on `%s`", obj.namespace) + } + + keyMap, err := obj.init.World.StrMapGet(obj.namespace) + if err != nil { + return errwrap.Wrapf(err, "channel read failed on `%s`", obj.namespace) + } + + var result types.Value + + d := types.NewMap(obj.Info().Sig.Out) + for k, v := range keyMap { + key := &types.StrValue{V: k} + val := &types.StrValue{V: v} + if err := d.Add(key, val); err != nil { + return errwrap.Wrapf(err, "map could not add key `%s`, val: `%s`", k, v) + } + } + result = d // put map into interface type + + // if the result is still the same, skip sending an update... + if obj.result != nil && result.Cmp(obj.result) == nil { + continue // result didn't change + } + obj.result = result // store new result + + case <-obj.closeChan: + return nil + } + + select { + case obj.init.Output <- obj.result: // send + // pass + case <-obj.closeChan: + return nil + } + } +} + +// Close runs some shutdown code for this fact and turns off the stream. +func (obj *ExchangeFunc) Close() error { + close(obj.closeChan) + return nil +} diff --git a/lang/funcs/core/kvlookupfunc.go b/lang/funcs/core/kvlookupfunc.go new file mode 100644 index 00000000..1ca0bcfc --- /dev/null +++ b/lang/funcs/core/kvlookupfunc.go @@ -0,0 +1,185 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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 core // TODO: should this be in its own individual package? + +import ( + "fmt" + + "github.com/purpleidea/mgmt/lang/funcs" + "github.com/purpleidea/mgmt/lang/interfaces" + "github.com/purpleidea/mgmt/lang/types" + + errwrap "github.com/pkg/errors" +) + +func init() { + funcs.Register("kvlookup", func() interfaces.Func { return &KVLookupFunc{} }) // must register the func and name +} + +// KVLookupFunc is special function which returns all the values of a given key +// in the exposed world. It is similar to exchange, but it does not set a key. +type KVLookupFunc struct { + init *interfaces.Init + + namespace string + value string + + last types.Value + result types.Value // last calculated output + + watchChan chan error + closeChan chan struct{} +} + +// Validate makes sure we've built our struct properly. It is usually unused for +// normal functions that users can use directly. +func (obj *KVLookupFunc) Validate() error { + return nil +} + +// Info returns some static info about itself. +func (obj *KVLookupFunc) Info() *interfaces.Info { + return &interfaces.Info{ + Pure: false, // definitely false + Memo: false, + // output is map of: hostname => value + Sig: types.NewType("func(namespace str) {str: str}"), + Err: obj.Validate(), + } +} + +// Init runs some startup code for this fact. +func (obj *KVLookupFunc) Init(init *interfaces.Init) error { + obj.init = init + obj.watchChan = make(chan error) // XXX: sender should close this, but did I implement that part yet??? + obj.closeChan = make(chan struct{}) + return nil +} + +// Stream returns the changing values that this func has over time. +func (obj *KVLookupFunc) Stream() error { + defer close(obj.init.Output) // the sender closes + for { + select { + // TODO: should this first chan be run as a priority channel to + // avoid some sort of glitch? is that even possible? can our + // hostname check with reality (below) fix that? + case input, ok := <-obj.init.Input: + if !ok { + obj.init.Input = nil // don't infinite loop back + continue // no more inputs, but don't return! + } + //if err := input.Type().Cmp(obj.Info().Sig.Input); err != nil { + // return errwrap.Wrapf(err, "wrong function input") + //} + + if obj.last != nil && input.Cmp(obj.last) == nil { + continue // value didn't change, skip it + } + obj.last = input // store for next + + namespace := input.Struct()["namespace"].Str() + if namespace == "" { + return fmt.Errorf("can't use an empty namespace") + } + if obj.init.Debug { + obj.init.Logf("namespace: %s", namespace) + } + + // TODO: support changing the namespace over time... + // TODO: possibly removing our stored value there first! + if obj.namespace == "" { + obj.namespace = namespace // store it + obj.watchChan = obj.init.World.StrMapWatch(obj.namespace) // watch for var changes + + result, err := obj.buildMap() // build the map... + if err != nil { + return err + } + select { + case obj.init.Output <- result: // send one! + // pass + case <-obj.closeChan: + return nil + } + + } else if obj.namespace != namespace { + return fmt.Errorf("can't change namespace, previously: `%s`", obj.namespace) + } + + continue // we get values on the watch chan, not here! + + case err, ok := <-obj.watchChan: + if !ok { // closed + // XXX: if we close, perhaps the engine is + // switching etcd hosts and we should retry? + // maybe instead we should get an "etcd + // reconnect" signal, and the lang will restart? + return nil + } + if err != nil { + return errwrap.Wrapf(err, "channel watch failed on `%s`", obj.namespace) + } + + result, err := obj.buildMap() // build the map... + if err != nil { + return err + } + + // if the result is still the same, skip sending an update... + if obj.result != nil && result.Cmp(obj.result) == nil { + continue // result didn't change + } + obj.result = result // store new result + + case <-obj.closeChan: + return nil + } + + select { + case obj.init.Output <- obj.result: // send + // pass + case <-obj.closeChan: + return nil + } + } +} + +// Close runs some shutdown code for this fact and turns off the stream. +func (obj *KVLookupFunc) Close() error { + close(obj.closeChan) + return nil +} + +// buildMap builds the result map which we'll need. It uses struct variables. +func (obj *KVLookupFunc) buildMap() (types.Value, error) { + keyMap, err := obj.init.World.StrMapGet(obj.namespace) + if err != nil { + return nil, errwrap.Wrapf(err, "channel read failed on `%s`", obj.namespace) + } + + d := types.NewMap(obj.Info().Sig.Out) + for k, v := range keyMap { + key := &types.StrValue{V: k} + val := &types.StrValue{V: v} + if err := d.Add(key, val); err != nil { + return nil, errwrap.Wrapf(err, "map could not add key `%s`, val: `%s`", k, v) + } + } + return d, nil // put map into interface type +} diff --git a/lang/funcs/core/printf_polyfunc.go b/lang/funcs/core/printf_polyfunc.go new file mode 100644 index 00000000..33c785d5 --- /dev/null +++ b/lang/funcs/core/printf_polyfunc.go @@ -0,0 +1,366 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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 core // TODO: should this be in its own individual package? + +import ( + "fmt" + + "github.com/purpleidea/mgmt/lang/funcs" + "github.com/purpleidea/mgmt/lang/interfaces" + "github.com/purpleidea/mgmt/lang/types" + "github.com/purpleidea/mgmt/util" + + errwrap "github.com/pkg/errors" +) + +func init() { + funcs.Register("printf", func() interfaces.Func { return &PrintfFunc{} }) +} + +const ( + // XXX: does this need to be `a` ? -- for now yes, fix this compiler bug + //formatArgName = "format" // name of the first arg + formatArgName = "a" // name of the first arg +) + +// PrintfFunc is a static polymorphic function that compiles a format string and +// returns the output as a string. It bases its output on the values passed in +// to it. It examines the type of the arguments at compile time and then +// determines the static function signature by parsing the format string and +// using that to determine the final function signature. One consequence of this +// is that the format string must be a static string which is known at compile +// time. This is reasonable, because if it was a reactive, changing string, then +// we could expect the type signature to change, which is not allowed in our +// statically typed language. +type PrintfFunc struct { + Type *types.Type // final full type of our function + + init *interfaces.Init + last types.Value // last value received to use for diff + + result string // last calculated output + + closeChan chan struct{} +} + +// Polymorphisms returns the possible type signature for this function. In this +// case, since the number of arguments can be infinite, it returns the final +// precise type if it can be gleamed from the format argument. If it cannot, it +// is because either the format argument was not known statically, or because +// it had an invalid format string. +func (obj *PrintfFunc) Polymorphisms(partialType *types.Type, partialValues []types.Value) ([]*types.Type, error) { + if partialType == nil || len(partialValues) < 1 { + return nil, fmt.Errorf("first argument must be a static format string") + } + + if partialType.Out != nil && partialType.Out.Cmp(types.TypeStr) != nil { + return nil, fmt.Errorf("return value of printf must be str") + } + + ord := partialType.Ord + if partialType.Map != nil { + if len(ord) < 1 { + return nil, fmt.Errorf("must have at least one arg in printf func") + } + if t, exists := partialType.Map[ord[0]]; exists && t != nil { + if t.Cmp(types.TypeStr) != nil { + return nil, fmt.Errorf("first arg for printf must be an str") + } + } + } + + format := partialValues[0].Str() // must not panic + typList, err := parseFormatToTypeList(format) + if err != nil { + return nil, errwrap.Wrapf(err, "could not parse format string") + } + + typ := &types.Type{ + Kind: types.KindFunc, // function type + Map: make(map[string]*types.Type), + Ord: []string{}, + Out: types.TypeStr, + } + // add first arg + typ.Map[formatArgName] = types.TypeStr + typ.Ord = append(typ.Ord, formatArgName) + + for i, x := range typList { + name := util.NumToAlpha(i + 1) // +1 to skip the format arg + if name == formatArgName { + return nil, fmt.Errorf("could not build function with %d args", i+1) + } + + // if we also had even more partial type information, check it! + if t, exists := partialType.Map[ord[i+1]]; exists && t != nil { + if err := t.Cmp(x); err != nil { + return nil, errwrap.Wrapf(err, "arg %d does not match expected type", i+1) + } + } + + typ.Map[name] = x + typ.Ord = append(typ.Ord, name) + } + + return []*types.Type{typ}, nil // return a list with a single possibility +} + +// Build takes the now known function signature and stores it so that this +// function can appear to be static. That type is used to build our function +// statically. +func (obj *PrintfFunc) Build(typ *types.Type) error { + if typ.Kind != types.KindFunc { + return fmt.Errorf("input type must be of kind func") + } + if len(typ.Ord) < 1 { + return fmt.Errorf("the printf function needs at least one arg") + } + if typ.Out == nil { + return fmt.Errorf("return type of function must be specified") + } + if typ.Out.Cmp(types.TypeStr) != nil { + return fmt.Errorf("return type of function must be an str") + } + if typ.Map == nil { + return fmt.Errorf("invalid input type") + } + + t0, exists := typ.Map[typ.Ord[0]] + if !exists || t0 == nil { + return fmt.Errorf("first arg must be specified") + } + if t0.Cmp(types.TypeStr) != nil { + return fmt.Errorf("first arg for printf must be an str") + } + + obj.Type = typ // function type is now known! + return nil +} + +// Validate makes sure we've built our struct properly. It is usually unused for +// normal functions that users can use directly. +func (obj *PrintfFunc) Validate() error { + if obj.Type == nil { // build must be run first + return fmt.Errorf("type is still unspecified") + } + return nil +} + +// Info returns some static info about itself. +func (obj *PrintfFunc) Info() *interfaces.Info { + return &interfaces.Info{ + Pure: true, + Memo: false, + Sig: obj.Type, + Err: obj.Validate(), + } +} + +// Init runs some startup code for this fact. +func (obj *PrintfFunc) Init(init *interfaces.Init) error { + obj.init = init + obj.closeChan = make(chan struct{}) + return nil +} + +// Stream returns the changing values that this func has over time. +func (obj *PrintfFunc) Stream() error { + defer close(obj.init.Output) // the sender closes + for { + select { + case input, ok := <-obj.init.Input: + if !ok { + return nil // can't output any more + } + //if err := input.Type().Cmp(obj.Info().Sig.Input); err != nil { + // return errwrap.Wrapf(err, "wrong function input") + //} + + if obj.last != nil && input.Cmp(obj.last) == nil { + continue // value didn't change, skip it + } + obj.last = input // store for next + + format := input.Struct()[formatArgName].Str() + values := []types.Value{} + for _, name := range obj.Type.Ord { + if name == formatArgName { // skip format arg + continue + } + x := input.Struct()[name] + values = append(values, x) + } + + result, err := compileFormatToString(format, values) + if err != nil { + return err // no errwrap needed b/c helper func + } + + if obj.result == result { + continue // result didn't change + } + obj.result = result // store new result + + case <-obj.closeChan: + return nil + } + + select { + case obj.init.Output <- &types.StrValue{ + V: obj.result, + }: + case <-obj.closeChan: + return nil + } + } +} + +// Close runs some shutdown code for this fact and turns off the stream. +func (obj *PrintfFunc) Close() error { + close(obj.closeChan) + return nil +} + +// valueToString prints our values how we expect for printf. +// FIXME: if this turns out to be useful, add it to the types package. +func valueToString(value types.Value) string { + // FIXME: this is just an "easy-out" implementation for now... + return fmt.Sprintf("%v", value.Value()) + + //switch x := value.Type().Kind; x { + //case types.KindBool: + // return value.String() + //case types.KindStr: + // return value.Str() // use this since otherwise it adds " & " + //case types.KindInt: + // return value.String() + //case types.KindFloat: + // // TODO: use formatting flags ? + // return value.String() + //} + //panic("unhandled type") // TODO: not fully implemented yet +} + +// parseFormatToTypeList takes a format string and returns a list of types that +// it expects to use in the order found in the format string. +// FIXME: add support for more types, and add tests! +func parseFormatToTypeList(format string) ([]*types.Type, error) { + typList := []*types.Type{} + inType := false + for i := 0; i < len(format); i++ { + + // some normal char... + if !inType && format[i] != '%' { + continue + } + + // in a type or we're a % + if format[i] == '%' { + if inType { + // it's a %% + inType = false + } else { + // start looking for type specification! + inType = true + } + continue + } + + // we must be in a type + switch format[i] { + case 't': + typList = append(typList, types.TypeBool) + case 's': + typList = append(typList, types.TypeStr) + case 'd': + typList = append(typList, types.TypeInt) + + // TODO: parse fancy formats like %0.2f and stuff + case 'f': + typList = append(typList, types.TypeFloat) + + // FIXME: add fancy types like: %[]s, %[]f, %{s:f}, etc... + + default: + return nil, fmt.Errorf("invalid format string at %d", i) + } + inType = false // done + } + + return typList, nil +} + +// compileFormatToString takes a format string and a list of values and returns +// the compiled/templated output. +// FIXME: add support for more types, and add tests! +func compileFormatToString(format string, values []types.Value) (string, error) { + output := "" + ix := 0 + inType := false + for i := 0; i < len(format); i++ { + + // some normal char... + if !inType && format[i] != '%' { + output += string(format[i]) + continue + } + + // in a type or we're a % + if format[i] == '%' { + if inType { + // it's a %% + output += string(format[i]) + inType = false + } else { + // start looking for type specification! + inType = true + } + continue + } + + // we must be in a type + var typ *types.Type + switch format[i] { + case 't': + typ = types.TypeBool + case 's': + typ = types.TypeStr + case 'd': + typ = types.TypeInt + + // TODO: parse fancy formats like %0.2f and stuff + case 'f': + typ = types.TypeFloat + + // FIXME: add fancy types like: %[]s, %[]f, %{s:f}, etc... + + default: + return "", fmt.Errorf("invalid format string at %d", i) + } + inType = false // done + + if err := typ.Cmp(values[ix].Type()); err != nil { + return "", errwrap.Wrapf(err, "unexpected type") + } + + output += valueToString(values[ix]) + ix++ // consume one value + } + + return output, nil +} diff --git a/lang/funcs/core/random1func.go b/lang/funcs/core/random1func.go new file mode 100644 index 00000000..04c24a0b --- /dev/null +++ b/lang/funcs/core/random1func.go @@ -0,0 +1,155 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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 core // TODO: should this be in its own individual package? + +import ( + "crypto/rand" + "fmt" + "math/big" + + "github.com/purpleidea/mgmt/lang/funcs" + "github.com/purpleidea/mgmt/lang/interfaces" + "github.com/purpleidea/mgmt/lang/types" + + errwrap "github.com/pkg/errors" +) + +const alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + +func init() { + funcs.Register("random1", func() interfaces.Func { return &Random1Func{} }) +} + +// Random1Func returns one random string of a certain length. +// XXX: return a stream instead, and combine this with a first(?) function which +// takes the first value and then puts backpressure on the stream. This should +// notify parent functions somehow that their values are no longer required so +// that they can shutdown if possible. Maybe it should be returning a stream of +// floats [0,1] as well, which someone can later map to the alphabet that they +// want. Should random() take an interval to know how often to spit out values? +// It could also just do it once per second, and we could filter for less. If we +// want something high precision, we could add that in the future... We could +// name that "random" and this one can be "random1" until we deprecate it. +type Random1Func struct { + init *interfaces.Init + + finished bool // did we send the random string? + + closeChan chan struct{} +} + +// Validate makes sure we've built our struct properly. It is usually unused for +// normal functions that users can use directly. +func (obj *Random1Func) Validate() error { + return nil +} + +// Info returns some static info about itself. +func (obj *Random1Func) Info() *interfaces.Info { + return &interfaces.Info{ + Pure: false, + Sig: types.NewType("func(length int) str"), + Err: obj.Validate(), + } +} + +// generate generates a random string. +func generate(length uint16) (string, error) { + max := len(alphabet) - 1 // last index + output := "" + + // FIXME: have someone verify this is cryptographically secure & correct + for i := uint16(0); i < length; i++ { + big, err := rand.Int(rand.Reader, big.NewInt(int64(max))) + if err != nil { + return "", errwrap.Wrapf(err, "could not generate random string") + } + ix := big.Int64() + output += string(alphabet[ix]) + } + + if length != 0 && output == "" { // safety against empty strings + return "", fmt.Errorf("string is empty") + } + + if uint16(len(output)) != length { // safety against weird bugs + return "", fmt.Errorf("random string is too short") // bug! + } + + return output, nil +} + +// Init runs some startup code for this const. +func (obj *Random1Func) Init(init *interfaces.Init) error { + obj.init = init + obj.closeChan = make(chan struct{}) + return nil +} + +// Stream returns the single value that was generated and then closes. +func (obj *Random1Func) Stream() error { + defer close(obj.init.Output) // the sender closes + var result string + for { + select { + case input, ok := <-obj.init.Input: + if !ok { + return nil // can't output any more + } + //if err := input.Type().Cmp(obj.Info().Sig.Input); err != nil { + // return errwrap.Wrapf(err, "wrong function input") + //} + + if obj.finished { + // TODO: continue instead? + return fmt.Errorf("you can only pass a single input to random") + } + + length := input.Struct()["length"].Int() + // TODO: if negative, randomly pick a length ? + if length < 0 { + return fmt.Errorf("can't generate a negative length") + } + + var err error + if result, err = generate(uint16(length)); err != nil { + return err // no errwrap needed b/c helper func + } + + case <-obj.closeChan: + return nil + } + + select { + case obj.init.Output <- &types.StrValue{ + V: result, + }: + // we only send one value, then wait for input to close + obj.finished = true + + case <-obj.closeChan: + return nil + } + } +} + +// Close runs some shutdown code for this const and turns off the stream. +func (obj *Random1Func) Close() error { + close(obj.closeChan) + return nil +} diff --git a/lang/funcs/core/schedule_polyfunc.go b/lang/funcs/core/schedule_polyfunc.go new file mode 100644 index 00000000..3c5d8e6d --- /dev/null +++ b/lang/funcs/core/schedule_polyfunc.go @@ -0,0 +1,448 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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 . + +// test with: +// time ./mgmt run --lang examples/lang/schedule0.mcl --hostname h1 --ideal-cluster-size 1 --tmp-prefix --no-pgp +// time ./mgmt run --lang examples/lang/schedule0.mcl --hostname h2 --seeds http://127.0.0.1:2379 --client-urls http://127.0.0.1:2381 --server-urls http://127.0.0.1:2382 --tmp-prefix --no-pgp +// time ./mgmt run --lang examples/lang/schedule0.mcl --hostname h3 --seeds http://127.0.0.1:2379 --client-urls http://127.0.0.1:2383 --server-urls http://127.0.0.1:2384 --tmp-prefix --no-pgp +// kill h2 (should see h1 and h3 pick [h1, h3] instead) +// restart h2 (should see [h1, h3] as before) +// kill h3 (should see h1 and h2 pick [h1, h2] instead) +// restart h3 (should see [h1, h2] as before) +// kill h3 +// kill h2 +// kill h1... all done! + +package core // TODO: should this be in its own individual package? + +import ( + "context" + "fmt" + + "github.com/purpleidea/mgmt/etcd/scheduler" // TODO: is it okay to import this without abstraction? + "github.com/purpleidea/mgmt/lang/funcs" + "github.com/purpleidea/mgmt/lang/interfaces" + "github.com/purpleidea/mgmt/lang/types" + + errwrap "github.com/pkg/errors" +) + +const ( + // DefaultStrategy is the strategy to use if none has been specified. + DefaultStrategy = "rr" +) + +func init() { + funcs.Register("schedule", func() interfaces.Func { return &SchedulePolyFunc{} }) // must register the func and name +} + +// SchedulePolyFunc is special function which determines where code should run +// in the cluster. +type SchedulePolyFunc struct { + Type *types.Type // this is the type of value stored in our list + + init *interfaces.Init + + namespace string + scheduler *scheduler.Result + + last types.Value + result types.Value // last calculated output + + watchChan chan *schedulerResult + closeChan chan struct{} +} + +// validOpts returns the available mapping of valid opts fields to types. +func (obj *SchedulePolyFunc) validOpts() map[string]*types.Type { + return map[string]*types.Type{ + "strategy": types.TypeStr, + "max": types.TypeInt, + "reuse": types.TypeBool, + "ttl": types.TypeInt, + } +} + +// Polymorphisms returns the list of possible function signatures available for +// this static polymorphic function. It relies on type and value hints to limit +// the number of returned possibilities. +func (obj *SchedulePolyFunc) Polymorphisms(partialType *types.Type, partialValues []types.Value) ([]*types.Type, error) { + // TODO: technically, we could generate all permutations of the struct! + //variant := []*types.Type{} + //t0 := types.NewType("func(namespace str) []str") + //variant = append(variant, t0) + //validOpts := obj.validOpts() + //for ? := ? range { // generate all permutations of the struct... + // t := types.NewType(fmt.Sprintf("func(namespace str, opts %s) []str", ?)) + // variant = append(variant, t) + //} + //if partialType == nil { + // return variant, nil + //} + + if partialType == nil { + return nil, fmt.Errorf("zero type information given") + } + + var typ *types.Type + + if tOut := partialType.Out; tOut != nil { + if err := tOut.Cmp(types.NewType("[]str")); err != nil { + return nil, errwrap.Wrapf(err, "return type must be a list of strings") + } + } + + ord := partialType.Ord + if partialType.Map != nil { + if len(ord) == 0 { + return nil, fmt.Errorf("must have at least one arg in schedule func") + } + + if tNamespace, exists := partialType.Map[ord[0]]; exists && tNamespace != nil { + if err := tNamespace.Cmp(types.TypeStr); err != nil { + return nil, errwrap.Wrapf(err, "first arg must be an str") + } + } + if len(ord) == 1 { + return []*types.Type{types.NewType("func(namespace str) []str")}, nil // done! + } + + if len(ord) != 2 { + return nil, fmt.Errorf("must have either one or two args in schedule func") + } + + if tOpts, exists := partialType.Map[ord[1]]; exists { + if tOpts == nil { // usually a `struct{}` + typFunc := types.NewType("func(namespace str, opts variant) []str") + return []*types.Type{typFunc}, nil // solved! + } + + if tOpts.Kind != types.KindStruct { + return nil, fmt.Errorf("second arg must be of kind struct") + } + + validOpts := obj.validOpts() + for _, name := range tOpts.Ord { + t := tOpts.Map[name] + value, exists := validOpts[name] + if !exists { + return nil, fmt.Errorf("unexpected opts field: `%s`", name) + } + + if err := t.Cmp(value); err != nil { + return nil, errwrap.Wrapf(err, "expected different type for opts field: `%s`", name) + } + } + + typ = tOpts // solved + } + } + + if typ == nil { + return nil, fmt.Errorf("not enough type information") + } + + typFunc := types.NewType(fmt.Sprintf("func(namespace str, opts %s) []str", typ.String())) + + // TODO: type check that the partialValues are compatible + + return []*types.Type{typFunc}, nil // solved! +} + +// Build is run to turn the polymorphic, undeterminted function, into the +// specific statically type version. It is usually run after Unify completes, +// and must be run before Info() and any of the other Func interface methods are +// used. This function is idempotent, as long as the arg isn't changed between +// runs. +func (obj *SchedulePolyFunc) Build(typ *types.Type) error { + // typ is the KindFunc signature we're trying to build... + if typ.Kind != types.KindFunc { + return fmt.Errorf("input type must be of kind func") + } + + if len(typ.Ord) != 1 && len(typ.Ord) != 2 { + return fmt.Errorf("the schedule function needs either one or two args") + } + if typ.Out == nil { + return fmt.Errorf("return type of function must be specified") + } + if typ.Map == nil { + return fmt.Errorf("invalid input type") + } + + if err := typ.Out.Cmp(types.NewType("[]str")); err != nil { + return errwrap.Wrapf(err, "return type must be a list of strings") + } + + tNamespace, exists := typ.Map[typ.Ord[0]] + if !exists || tNamespace == nil { + return fmt.Errorf("first arg must be specified") + } + + if len(typ.Ord) == 1 { + obj.Type = nil + return nil // done early, 2nd arg is absent! + } + tOpts, exists := typ.Map[typ.Ord[1]] + if !exists || tOpts == nil { + return fmt.Errorf("second argument was missing") + } + + if tOpts.Kind != types.KindStruct { + return fmt.Errorf("second argument must be of kind struct") + } + + validOpts := obj.validOpts() + for _, name := range tOpts.Ord { + t := tOpts.Map[name] + value, exists := validOpts[name] + if !exists { + return fmt.Errorf("unexpected opts field: `%s`", name) + } + + if err := t.Cmp(value); err != nil { + return errwrap.Wrapf(err, "expected different type for opts field: `%s`", name) + } + } + + obj.Type = tOpts // type of opts struct, even an empty: `struct{}` + return nil +} + +// Validate tells us if the input struct takes a valid form. +func (obj *SchedulePolyFunc) Validate() error { + // obj.Type can be nil if no 2nd arg is given, or a struct (even empty!) + if obj.Type != nil && obj.Type.Kind != types.KindStruct { // build must be run first + return fmt.Errorf("type must be nil or a struct") + } + return nil +} + +// Info returns some static info about itself. Build must be called before this +// will return correct data. +func (obj *SchedulePolyFunc) Info() *interfaces.Info { + typ := types.NewType("func(namespace str) []str") // simplest form + if obj.Type != nil { + typ = types.NewType(fmt.Sprintf("func(namespace str, opts %s) []str", obj.Type.String())) + } + return &interfaces.Info{ + Pure: false, // definitely false + Memo: false, + // output is list of hostnames chosen + Sig: typ, // func kind + Err: obj.Validate(), + } +} + +// Init runs some startup code for this fact. +func (obj *SchedulePolyFunc) Init(init *interfaces.Init) error { + obj.init = init + obj.watchChan = make(chan *schedulerResult) + obj.closeChan = make(chan struct{}) + //obj.init.Debug = true // use this for local debugging + return nil +} + +// Stream returns the changing values that this func has over time. +func (obj *SchedulePolyFunc) Stream() error { + defer close(obj.init.Output) // the sender closes + for { + select { + // TODO: should this first chan be run as a priority channel to + // avoid some sort of glitch? is that even possible? can our + // hostname check with reality (below) fix that? + case input, ok := <-obj.init.Input: + if !ok { + obj.init.Input = nil // don't infinite loop back + continue // no more inputs, but don't return! + } + //if err := input.Type().Cmp(obj.Info().Sig.Input); err != nil { + // return errwrap.Wrapf(err, "wrong function input") + //} + + if obj.last != nil && input.Cmp(obj.last) == nil { + continue // value didn't change, skip it + } + obj.last = input // store for next + + namespace := input.Struct()["namespace"].Str() + if namespace == "" { + return fmt.Errorf("can't use an empty namespace") + } + + opts := make(map[string]types.Value) // empty "struct" + if val, exists := input.Struct()["opts"]; exists { + opts = val.Struct() + } + + if obj.init.Debug { + obj.init.Logf("namespace: %s", namespace) + } + + schedulerOpts := []scheduler.Option{} + // don't add bad or zero-value options + + defaultStrategy := true + if val, exists := opts["strategy"]; exists { + if strategy := val.Str(); strategy != "" { + if obj.init.Debug { + obj.init.Logf("opts: strategy: %s", strategy) + } + defaultStrategy = false + schedulerOpts = append(schedulerOpts, scheduler.StrategyKind(strategy)) + } + } + if defaultStrategy { // we always need to add one! + schedulerOpts = append(schedulerOpts, scheduler.StrategyKind(DefaultStrategy)) + } + if val, exists := opts["max"]; exists { + // TODO: check for overflow + if max := int(val.Int()); max > 0 { + if obj.init.Debug { + obj.init.Logf("opts: max: %d", max) + } + schedulerOpts = append(schedulerOpts, scheduler.MaxCount(max)) + } + } + if val, exists := opts["reuse"]; exists { + reuse := val.Bool() + if obj.init.Debug { + obj.init.Logf("opts: reuse: %t", reuse) + } + schedulerOpts = append(schedulerOpts, scheduler.ReuseLease(reuse)) + } + if val, exists := opts["ttl"]; exists { + // TODO: check for overflow + if ttl := int(val.Int()); ttl > 0 { + if obj.init.Debug { + obj.init.Logf("opts: ttl: %d", ttl) + } + schedulerOpts = append(schedulerOpts, scheduler.SessionTTL(ttl)) + } + } + + // TODO: support changing the namespace over time... + // TODO: possibly removing our stored value there first! + if obj.namespace == "" { + obj.namespace = namespace // store it + + if obj.init.Debug { + obj.init.Logf("starting scheduler...") + } + var err error + obj.scheduler, err = obj.init.World.Scheduler(obj.namespace, schedulerOpts...) + if err != nil { + return errwrap.Wrapf(err, "can't create scheduler") + } + + // process the stream of scheduling output... + go func() { + defer close(obj.watchChan) + ctx, cancel := context.WithCancel(context.Background()) + go func() { + defer cancel() // unblock Next() + defer obj.scheduler.Shutdown() + select { + case <-obj.closeChan: + return + } + }() + for { + hosts, err := obj.scheduler.Next(ctx) + select { + case obj.watchChan <- &schedulerResult{ + hosts: hosts, + err: err, + }: + + case <-obj.closeChan: + return + } + } + }() + + } else if obj.namespace != namespace { + return fmt.Errorf("can't change namespace, previously: `%s`", obj.namespace) + } + + continue // we send values on the watch chan, not here! + + case schedulerResult, ok := <-obj.watchChan: + if !ok { // closed + // XXX: maybe etcd reconnected? (fix etcd implementation) + + // XXX: if we close, perhaps the engine is + // switching etcd hosts and we should retry? + // maybe instead we should get an "etcd + // reconnect" signal, and the lang will restart? + return nil + } + if err := schedulerResult.err; err != nil { + if err == scheduler.ErrEndOfResults { + //return nil // TODO: we should probably fix the reconnect issue and use this here + return fmt.Errorf("scheduler shutdown, reconnect bug?") // XXX: fix etcd reconnects + } + return errwrap.Wrapf(err, "channel watch failed on `%s`", obj.namespace) + } + + if obj.init.Debug { + obj.init.Logf("got hosts: %+v", schedulerResult.hosts) + } + + var result types.Value + l := types.NewList(obj.Info().Sig.Out) + for _, val := range schedulerResult.hosts { + if err := l.Add(&types.StrValue{V: val}); err != nil { + return errwrap.Wrapf(err, "list could not add val: `%s`", val) + } + } + result = l // set list as result + + if obj.init.Debug { + obj.init.Logf("result: %+v", result) + } + + // if the result is still the same, skip sending an update... + if obj.result != nil && result.Cmp(obj.result) == nil { + continue // result didn't change + } + obj.result = result // store new result + + case <-obj.closeChan: + return nil + } + + select { + case obj.init.Output <- obj.result: // send + // pass + case <-obj.closeChan: + return nil + } + } +} + +// Close runs some shutdown code for this fact and turns off the stream. +func (obj *SchedulePolyFunc) Close() error { + close(obj.closeChan) + return nil +} + +// schedulerResult combines our internal events into a single message packet. +type schedulerResult struct { + hosts []string + err error +} diff --git a/lang/funcs/core/template_polyfunc.go b/lang/funcs/core/template_polyfunc.go new file mode 100644 index 00000000..33fb2546 --- /dev/null +++ b/lang/funcs/core/template_polyfunc.go @@ -0,0 +1,290 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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 core // TODO: should this be in its own individual package? + +import ( + "bytes" + "fmt" + "text/template" + "time" + + "github.com/purpleidea/mgmt/lang/funcs" + "github.com/purpleidea/mgmt/lang/interfaces" + "github.com/purpleidea/mgmt/lang/types" + + errwrap "github.com/pkg/errors" +) + +func init() { + funcs.Register("template", func() interfaces.Func { return &TemplateFunc{} }) +} + +// TemplateName is the name of our template as required by the template library. +const TemplateName = "template" + +// TemplateFunc is a static polymorphic function that compiles a template and +// returns the output as a string. It bases its output on the values passed in +// to it. It examines the type of the second argument (the input data vars) at +// compile time and then determines the static functions signature by including +// that in the overall signature. +// XXX: do we need to add events if any of the internal functions change over time? +type TemplateFunc struct { + Type *types.Type // type of vars + + init *interfaces.Init + last types.Value // last value received to use for diff + + result string // last calculated output + + closeChan chan struct{} +} + +// Polymorphisms returns the possible type signatures for this template. In this +// case, since the second argument can be an infinite number of values, it +// instead returns either the final precise type (if it can be gleamed from the +// input partials) or if it cannot, it returns a single entry with the complete +// type but with the variable second argument specified as a `variant` type. +// If it encounters any partial type specifications which are not possible, then +// it errors out. This could happen if you specified a non string template arg. +// XXX: is there a better API than returning a buried `variant` type? +func (obj *TemplateFunc) Polymorphisms(partialType *types.Type, partialValues []types.Value) ([]*types.Type, error) { + // TODO: return `variant` as second arg for now -- maybe there's a better way? + variant := []*types.Type{types.NewType("func(a str, b variant) str")} + + if partialType == nil { + return variant, nil + } + + if partialType.Out != nil && partialType.Out.Cmp(types.TypeStr) != nil { + return nil, fmt.Errorf("return value of template must be str") + } + + ord := partialType.Ord + if partialType.Map != nil { + if len(ord) != 2 { + return nil, fmt.Errorf("must have exactly two args in template func") + } + if t, exists := partialType.Map[ord[0]]; exists && t != nil { + if t.Cmp(types.TypeStr) != nil { + return nil, fmt.Errorf("first arg for template must be an str") + } + } + if t, exists := partialType.Map[ord[1]]; exists && t != nil { + // known vars type! w00t! + return []*types.Type{types.NewType(fmt.Sprintf("func(a str, b %s) str", t.String()))}, nil + } + } + + return variant, nil +} + +// Build takes the now known function signature and stores it so that this +// function can appear to be static. It extracts the type of the vars argument, +// which is the dynamic part which can change. That type is used to build our +// function statically. +func (obj *TemplateFunc) Build(typ *types.Type) error { + if typ.Kind != types.KindFunc { + return fmt.Errorf("input type must be of kind func") + } + if len(typ.Ord) != 2 { + return fmt.Errorf("the template function needs exactly two args") + } + if typ.Out == nil { + return fmt.Errorf("return type of function must be specified") + } + if typ.Out.Cmp(types.TypeStr) != nil { + return fmt.Errorf("return type of function must be an str") + } + if typ.Map == nil { + return fmt.Errorf("invalid input type") + } + + t0, exists := typ.Map[typ.Ord[0]] + if !exists || t0 == nil { + return fmt.Errorf("first arg must be specified") + } + if t0.Cmp(types.TypeStr) != nil { + return fmt.Errorf("first arg for template must be an str") + } + + t1, exists := typ.Map[typ.Ord[1]] + if !exists || t1 == nil { + return fmt.Errorf("second arg must be specified") + } + obj.Type = t1 // extracted vars type is now known! + + return nil +} + +// Validate makes sure we've built our struct properly. It is usually unused for +// normal functions that users can use directly. +func (obj *TemplateFunc) Validate() error { + if obj.Type == nil { // build must be run first + return fmt.Errorf("type is still unspecified") + } + return nil +} + +// Info returns some static info about itself. +func (obj *TemplateFunc) Info() *interfaces.Info { + return &interfaces.Info{ + Pure: true, + Memo: false, + Sig: types.NewType(fmt.Sprintf("func(template str, vars %s) str", obj.Type.String())), + Err: obj.Validate(), + } +} + +// Init runs some startup code for this fact. +func (obj *TemplateFunc) Init(init *interfaces.Init) error { + obj.init = init + obj.closeChan = make(chan struct{}) + return nil +} + +// run runs a template and returns the result. +func (obj *TemplateFunc) run(templateText string, vars types.Value) (string, error) { + funcMap := map[string]interface{}{ + // XXX: can these functions come from normal funcValue things + // that we build for the interfaces.Func part? + // TODO: add a bunch of stdlib-like stuff here... + "datetimeprint": func(epochDelta int64) string { // TODO: rename + return time.Unix(epochDelta, 0).String() + }, + } + + var err error + tmpl := template.New(TemplateName) + tmpl = tmpl.Funcs(funcMap) + tmpl, err = tmpl.Parse(templateText) + if err != nil { + return "", errwrap.Wrapf(err, "template: parse error") + } + + buf := new(bytes.Buffer) + // NOTE: any objects in here can have their methods called by the template! + var data interface{} // can be many types, eg a struct! + v := vars.Copy() // make a copy since we make modifications to it... +Loop: + // TODO: simplify with Type.Underlying() + for { + switch x := v.Type().Kind; x { + case types.KindBool: + fallthrough + case types.KindStr: + fallthrough + case types.KindInt: + fallthrough + case types.KindFloat: + // standalone values can be used in templates with a dot + data = v.Value() + break Loop + + case types.KindList: + // TODO: can we improve on this to expose indexes? + data = v.Value() + break Loop + + case types.KindMap: + if v.Type().Key.Cmp(types.TypeStr) != nil { + return "", errwrap.Wrapf(err, "template: map keys must be str") + } + m := make(map[string]interface{}) + for k, v := range v.Map() { // map[Value]Value + m[k.Str()] = v.Value() + } + data = m + break Loop + + case types.KindStruct: + m := make(map[string]interface{}) + for k, v := range v.Struct() { // map[string]Value + m[k] = v.Value() + } + data = m + break Loop + + // TODO: should we allow functions here? + //case types.KindFunc: + + case types.KindVariant: + v = v.(*types.VariantValue).V // un-nest and recurse + continue Loop + + default: + return "", fmt.Errorf("can't use `%+v` as vars input", x) + } + } + + // run the template + if err := tmpl.Execute(buf, data); err != nil { + return "", errwrap.Wrapf(err, "template: execution error") + } + return buf.String(), nil +} + +// Stream returns the changing values that this func has over time. +func (obj *TemplateFunc) Stream() error { + defer close(obj.init.Output) // the sender closes + for { + select { + case input, ok := <-obj.init.Input: + if !ok { + return nil // can't output any more + } + //if err := input.Type().Cmp(obj.Info().Sig.Input); err != nil { + // return errwrap.Wrapf(err, "wrong function input") + //} + + if obj.last != nil && input.Cmp(obj.last) == nil { + continue // value didn't change, skip it + } + obj.last = input // store for next + + tmpl := input.Struct()["template"].Str() + vars := input.Struct()["vars"] + + result, err := obj.run(tmpl, vars) + if err != nil { + return err // no errwrap needed b/c helper func + } + + if obj.result == result { + continue // result didn't change + } + obj.result = result // store new result + + case <-obj.closeChan: + return nil + } + + select { + case obj.init.Output <- &types.StrValue{ + V: obj.result, + }: + case <-obj.closeChan: + return nil + } + } +} + +// Close runs some shutdown code for this fact and turns off the stream. +func (obj *TemplateFunc) Close() error { + close(obj.closeChan) + return nil +} diff --git a/lang/funcs/core/vumeterfunc.go b/lang/funcs/core/vumeterfunc.go new file mode 100644 index 00000000..3234e929 --- /dev/null +++ b/lang/funcs/core/vumeterfunc.go @@ -0,0 +1,213 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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 core // TODO: should this be in its own individual package? + +import ( + "fmt" + "math" + "os/exec" + "strconv" + "strings" + "syscall" + "time" + + "github.com/purpleidea/mgmt/lang/funcs" + "github.com/purpleidea/mgmt/lang/interfaces" + "github.com/purpleidea/mgmt/lang/types" + + errwrap "github.com/pkg/errors" +) + +func init() { + funcs.Register("vumeter", func() interfaces.Func { return &VUMeterFunc{} }) // must register the func and name +} + +// VUMeter is a gimmic function to display a vu meter from the microphone. +type VUMeterFunc struct { + init *interfaces.Init + last types.Value // last value received to use for diff + + symbol string + multiplier int64 + peak float64 + + result string // last calculated output + + closeChan chan struct{} +} + +// Validate makes sure we've built our struct properly. It is usually unused for +// normal functions that users can use directly. +func (obj *VUMeterFunc) Validate() error { + return nil +} + +// Info returns some static info about itself. +func (obj *VUMeterFunc) Info() *interfaces.Info { + return &interfaces.Info{ + Pure: true, + Memo: false, + Sig: types.NewType("func(symbol str, multiplier int, peak float) str"), + } +} + +// Init runs some startup code for this fact. +func (obj *VUMeterFunc) Init(init *interfaces.Init) error { + obj.init = init + obj.closeChan = make(chan struct{}) + return nil +} + +// Stream returns the changing values that this func has over time. +func (obj *VUMeterFunc) Stream() error { + defer close(obj.init.Output) // the sender closes + ticker := newTicker() + defer ticker.Stop() + // FIXME: this goChan seems to work better than the ticker :) + // this is because we have a ~1sec delay in capturing the value in exec + goChan := make(chan struct{}) + close(goChan) + for { + select { + case input, ok := <-obj.init.Input: + if !ok { + obj.init.Input = nil // don't infinite loop back + continue // no more inputs, but don't return! + } + //if err := input.Type().Cmp(obj.Info().Sig.Input); err != nil { + // return errwrap.Wrapf(err, "wrong function input") + //} + + if obj.last != nil && input.Cmp(obj.last) == nil { + continue // value didn't change, skip it + } + obj.last = input // store for next + + obj.symbol = input.Struct()["symbol"].Str() + obj.multiplier = input.Struct()["multiplier"].Int() + obj.peak = input.Struct()["peak"].Float() + + //case <-ticker.C: // received the timer event + case <-goChan: + + if obj.last == nil { + continue // still waiting for input values + } + + // arecord -d 1 /dev/shm/mgmt_rec.wav 2>/dev/null + args1 := []string{"-d", "1", "/dev/shm/mgmt_rec.wav"} + cmd1 := exec.Command("/usr/bin/arecord", args1...) + cmd1.SysProcAttr = &syscall.SysProcAttr{ + Setpgid: true, + Pgid: 0, + } + // start the command + if _, err := cmd1.Output(); err != nil { + return errwrap.Wrapf(err, "cmd failed to run") + } + + // sox -t .wav /dev/shm/mgmt_rec.wav -n stat 2>&1 | grep "Maximum amplitude" | cut -d ':' -f 2 + args2 := []string{"-t", ".wav", "/dev/shm/mgmt_rec.wav", "-n", "stat"} + cmd2 := exec.Command("/usr/bin/sox", args2...) + cmd2.SysProcAttr = &syscall.SysProcAttr{ + Setpgid: true, + Pgid: 0, + } + + // start the command + out, err := cmd2.CombinedOutput() // data comes on stderr + if err != nil { + return errwrap.Wrapf(err, "cmd failed to run") + } + + ratio, err := extract(out) + if err != nil { + return errwrap.Wrapf(err, "failed to extract") + } + + result, err := visual(obj.symbol, int(obj.multiplier), obj.peak, ratio) + if err != nil { + return errwrap.Wrapf(err, "could not generate visual") + } + + if obj.result == result { + continue // result didn't change + } + obj.result = result // store new result + + case <-obj.closeChan: + return nil + } + + select { + case obj.init.Output <- &types.StrValue{ + V: obj.result, + }: + case <-obj.closeChan: + return nil + } + } +} + +// Close runs some shutdown code for this fact and turns off the stream. +func (obj *VUMeterFunc) Close() error { + close(obj.closeChan) + return nil +} + +func newTicker() *time.Ticker { + return time.NewTicker(time.Duration(1) * time.Second) +} + +func extract(data []byte) (float64, error) { + const prefix = "Maximum amplitude:" + str := string(data) + lines := strings.Split(str, "\n") + for _, line := range lines { + if !strings.HasPrefix(line, prefix) { + continue + } + s := strings.TrimSpace(line[len(prefix):]) + f, err := strconv.ParseFloat(s, 64) + if err != nil { + return 0, err + } + return f, nil + } + return 0, fmt.Errorf("could not extract any data") +} + +func round(f float64) int { + return int(f + math.Copysign(0.5, f)) +} + +// TODO: make this fancier +func visual(symbol string, multiplier int, peak, ratio float64) (string, error) { + if ratio > 1 || ratio < 0 { + return "", fmt.Errorf("invalid ratio of %f", ratio) + } + + x := strings.Repeat(symbol, round(ratio*float64(multiplier))) + if x == "" { + x += symbol // add a minimum + } + if ratio > peak { + x += " PEAK!!!" + } + return fmt.Sprintf("(%f):\n%s\n%s", ratio, x, x), nil +} diff --git a/lang/funcs/engine.go b/lang/funcs/engine.go new file mode 100644 index 00000000..953f94f1 --- /dev/null +++ b/lang/funcs/engine.go @@ -0,0 +1,608 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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 funcs + +import ( + "fmt" + "strings" + "sync" + + "github.com/purpleidea/mgmt/lang/interfaces" + "github.com/purpleidea/mgmt/lang/types" + "github.com/purpleidea/mgmt/pgraph" + "github.com/purpleidea/mgmt/resources" + + multierr "github.com/hashicorp/go-multierror" + errwrap "github.com/pkg/errors" +) + +// State represents the state of a function vertex. This corresponds to an AST +// expr, which is the memory address (pointer) in the graph. +type State struct { + Expr interfaces.Expr // pointer to the expr vertex + + handle interfaces.Func // the function (if not nil, we've found it on init) + + init bool // have we run Init on our func? + ready bool // has it received all the args it needs at least once? + loaded bool // has the func run at least once ? + closed bool // did we close ourself down? + + notify chan struct{} // ping here when new input values exist + + input chan types.Value // the top level type must be a struct + output chan types.Value +} + +// Init creates the function state if it can be found in the registered list. +func (obj *State) Init() error { + handle, err := obj.Expr.Func() // build one and store it, don't re-gen + if err != nil { + return err + } + if err := handle.Validate(); err != nil { + return errwrap.Wrapf(err, "could not validate func") + } + obj.handle = handle + + sig := obj.handle.Info().Sig + if sig.Kind != types.KindFunc { + return fmt.Errorf("must be kind func") + } + if len(sig.Ord) > 0 { + // since we accept input, better get our notification chan built + obj.notify = make(chan struct{}) + } + + obj.input = make(chan types.Value) // we close this when we're done + obj.output = make(chan types.Value) // we create it, func closes it + + return nil +} + +// String satisfies fmt.Stringer so that these print nicely. +func (obj *State) String() string { + return obj.Expr.String() +} + +// Edge links an output vertex (value) to an input vertex with a named argument. +type Edge struct { + Args []string // list of named args that this edge sends to +} + +// String displays the list of arguments this edge satisfies. It is a required +// property to be a valid pgraph.Edge. +func (obj *Edge) String() string { + return strings.Join(obj.Args, ", ") +} + +// Engine represents the running time varying directed acyclic function graph. +type Engine struct { + Graph *pgraph.Graph + Hostname string + World resources.World + Debug bool + Logf func(format string, v ...interface{}) + + // Glitch: https://en.wikipedia.org/wiki/Reactive_programming#Glitches + Glitch bool // allow glitching? (more responsive, but less accurate) + + ag chan error // used to aggregate fact events without reflect + agLock *sync.Mutex + agCount int // last one turns out the light (closes the ag channel) + + topologicalSort []pgraph.Vertex // cached sorting of the graph for perf + + state map[pgraph.Vertex]*State // state associated with the vertex + + mutex *sync.RWMutex // concurrency guard for the table map + table map[pgraph.Vertex]types.Value // live table of output values + + loaded bool // are all of the funcs loaded? + loadedChan chan struct{} // funcs loaded signal + + streamChan chan error // signals a new graph can be created or problem + + closeChan chan struct{} // close signal + wg *sync.WaitGroup +} + +// Init initializes the struct. This is the first call you must make. Do not +// proceed with calls to other methods unless this succeeds first. This also +// loads all the functions by calling Init on each one in the graph. +// TODO: should Init take the graph as an input arg to keep it as a private field? +func (obj *Engine) Init() error { + obj.ag = make(chan error) + obj.agLock = &sync.Mutex{} + obj.state = make(map[pgraph.Vertex]*State) + obj.mutex = &sync.RWMutex{} + obj.table = make(map[pgraph.Vertex]types.Value) + obj.loadedChan = make(chan struct{}) + obj.streamChan = make(chan error) + obj.closeChan = make(chan struct{}) + obj.wg = &sync.WaitGroup{} + topologicalSort, err := obj.Graph.TopologicalSort() + if err != nil { + return errwrap.Wrapf(err, "topo sort failed") + } + obj.topologicalSort = topologicalSort // cache the result + + for _, vertex := range obj.Graph.Vertices() { + // is this an interface we can use? + if _, exists := obj.state[vertex]; exists { + return fmt.Errorf("vertex (%+v) is not unique in the graph", vertex) + } + + expr, ok := vertex.(interfaces.Expr) + if !ok { + return fmt.Errorf("vertex (%+v) was not an expr", vertex) + } + + obj.state[vertex] = &State{Expr: expr} // store some state! + + if e := obj.state[vertex].Init(); e != nil { + err = multierr.Append(err, e) // list of errors + } + + if obj.Debug { + obj.Logf("Loading func `%s`", vertex) + } + } + if err != nil { // usually due to `not found` errors + return errwrap.Wrapf(err, "could not load requested funcs") + } + return nil +} + +// Validate the graph type checks properly and other tests. Must run Init first. +// This should check that: (1) all vertices have the correct number of inputs, +// (2) that the *Info signatures all match correctly, (3) that the argument +// names match correctly, and that the whole graph is statically correct. +func (obj *Engine) Validate() error { + inList := func(needle interfaces.Func, haystack []interfaces.Func) bool { + if needle == nil { + panic("nil value passed to inList") // catch bugs! + } + for _, x := range haystack { + if needle == x { + return true + } + } + return false + } + var err error + ptrs := []interfaces.Func{} // Func is a ptr + for _, vertex := range obj.Graph.Vertices() { + node := obj.state[vertex] + // TODO: this doesn't work for facts because they're in the Func + // duplicate pointers would get closed twice, causing a panic... + if inList(node.handle, ptrs) { // check for duplicate ptrs! + e := fmt.Errorf("vertex `%s` has duplicate ptr", vertex) + err = multierr.Append(err, e) + } + ptrs = append(ptrs, node.handle) + } + for _, edge := range obj.Graph.Edges() { + if _, ok := edge.(*Edge); !ok { + e := fmt.Errorf("edge `%s` was not the correct type", edge) + err = multierr.Append(err, e) + } + } + if err != nil { + return err // stage the errors so the user can fix many at once! + } + + // check if vertices expecting inputs have them + for vertex, count := range obj.Graph.InDegree() { + node := obj.state[vertex] + if exp := len(node.handle.Info().Sig.Ord); exp != count { + e := fmt.Errorf("expected %d inputs to `%s`, got %d", exp, node, count) + err = multierr.Append(err, e) + } + } + + // expected vertex -> argName + expected := make(map[*State]map[string]int) // expected input fields + for vertex1 := range obj.Graph.Adjacency() { + // check for outputs that don't go anywhere? + //node1 := obj.state[vertex1] + //if len(obj.Graph.Adjacency()[vertex1]) == 0 { // no vertex1 -> vertex2 + // if node1.handle.Info().Sig.Output != nil { + // // an output value goes nowhere... + // } + //} + for vertex2 := range obj.Graph.Adjacency()[vertex1] { // populate + node2 := obj.state[vertex2] + expected[node2] = make(map[string]int) + for _, key := range node2.handle.Info().Sig.Ord { + expected[node2][key] = 1 + } + } + } + + for vertex1 := range obj.Graph.Adjacency() { + node1 := obj.state[vertex1] + for vertex2, edge := range obj.Graph.Adjacency()[vertex1] { + node2 := obj.state[vertex2] + edge := edge.(*Edge) + // check vertex1 -> vertex2 (with e) is valid + + for _, arg := range edge.Args { // loop over each arg + sig := node2.handle.Info().Sig + if len(sig.Ord) == 0 { + e := fmt.Errorf("no input expected from `%s` to `%s` with arg `%s`", node1, node2, arg) + err = multierr.Append(err, e) + continue + } + + if count, exists := expected[node2][arg]; !exists { + e := fmt.Errorf("wrong input name from `%s` to `%s` with arg `%s`", node1, node2, arg) + err = multierr.Append(err, e) + } else if count == 0 { + e := fmt.Errorf("duplicate input from `%s` to `%s` with arg `%s`", node1, node2, arg) + err = multierr.Append(err, e) + } + expected[node2][arg]-- // subtract one use + + out := node1.handle.Info().Sig.Out + if out == nil { + e := fmt.Errorf("no output possible from `%s` to `%s` with arg `%s`", node1, node2, arg) + err = multierr.Append(err, e) + continue + } + typ, exists := sig.Map[arg] // key in struct + if !exists { + // second check of this! + e := fmt.Errorf("wrong input name from `%s` to `%s` with arg `%s`", node1, node2, arg) + err = multierr.Append(err, errwrap.Wrapf(e, "programming error")) + continue + } + + if typ.Kind == types.KindVariant { // FIXME: hack for now + // pass (input arg variants) + } else if out.Kind == types.KindVariant { // FIXME: hack for now + // pass (output arg variants) + } else if typ.Cmp(out) != nil { + e := fmt.Errorf("type mismatch from `%s` (%s) to `%s` (%s) with arg `%s`", node1, out, node2, typ, arg) + err = multierr.Append(err, e) + } + } + } + } + + // check for leftover function inputs which weren't filled up by outputs + // (we're trying to call a function with fewer input args than required) + for node, m := range expected { // map[*State]map[string]int + for arg, count := range m { + if count != 0 { // count should be zero if all were used + e := fmt.Errorf("missing input to `%s` on arg `%s`", node, arg) + err = multierr.Append(err, e) + } + } + } + + if err != nil { + return err // stage the errors so the user can fix many at once! + } + + return nil +} + +// Run starts up this function engine and gets it all running. It errors if the +// startup failed for some reason. On success, use the Stream and Table methods +// for future interaction with the engine, and the Close method to shut it off. +func (obj *Engine) Run() error { + if len(obj.topologicalSort) == 0 { // no funcs to load! + close(obj.loadedChan) + close(obj.streamChan) + return nil + } + + // TODO: build a timer that runs while we wait for all funcs to startup. + // after some delay print a message to tell us which funcs we're waiting + // for to startup and that they are slow and blocking everyone, and then + // fail permanently after the timeout so that bad code can't block this! + + // loop through all funcs that we might need + obj.agAdd(len(obj.topologicalSort)) + for _, vertex := range obj.topologicalSort { + node := obj.state[vertex] + if obj.Debug { + obj.Logf("Startup func `%s`", node) + } + + incoming := obj.Graph.IncomingGraphVertices(vertex) // []Vertex + + init := &interfaces.Init{ + Hostname: obj.Hostname, + Input: node.input, + Output: node.output, + World: obj.World, + Debug: obj.Debug, + Logf: func(format string, v ...interface{}) { + obj.Logf("func: "+format, v...) + }, + } + if err := node.handle.Init(init); err != nil { + return errwrap.Wrapf(err, "could not init func `%s`", node) + } + node.init = true // we've successfully initialized + + // no incoming edges, so no incoming data + if len(incoming) == 0 { // TODO: do this here or earlier? + close(node.input) + } else { + // process function input data + obj.wg.Add(1) + go func(vertex pgraph.Vertex) { + node := obj.state[vertex] + defer obj.wg.Done() + defer close(node.input) + var ready bool + // the final closing output to this, closes this + for range node.notify { // new input values + // now build the struct if we can... + + ready = true // assume for now... + si := &types.Type{ + // input to functions are structs + Kind: types.KindStruct, + Map: node.handle.Info().Sig.Map, + Ord: node.handle.Info().Sig.Ord, + } + st := types.NewStruct(si) + for _, v := range incoming { + args := obj.Graph.Adjacency()[v][vertex].(*Edge).Args + from := obj.state[v] + obj.mutex.RLock() + value, exists := obj.table[v] + obj.mutex.RUnlock() + if !exists { + ready = false // nope! + break + } + + // set each arg, since one value + // could get used for multiple + // function inputs (shared edge) + for _, arg := range args { + err := st.Set(arg, value) // populate struct + if err != nil { + panic(fmt.Sprintf("struct set failure on `%s` from `%s`: %v", node, from, err)) + } + } + } + if !ready { + continue + } + + select { + case node.input <- st: // send to function + + case <-obj.closeChan: + return + } + } + }(vertex) + } + + obj.wg.Add(1) + go func(vertex pgraph.Vertex) { // run function + node := obj.state[vertex] + defer obj.wg.Done() + if obj.Debug { + obj.Logf("Running func `%s`", node) + } + err := node.handle.Stream() + if obj.Debug { + obj.Logf("Exiting func `%s`", node) + } + if err != nil { + // we closed with an error... + err := errwrap.Wrapf(err, "problem streaming func `%s`", node) + select { + case obj.ag <- err: // send to aggregate channel + + case <-obj.closeChan: + return + } + } + }(vertex) + + obj.wg.Add(1) + go func(vertex pgraph.Vertex) { // process function output data + node := obj.state[vertex] + defer obj.wg.Done() + defer obj.agDone(vertex) + outgoing := obj.Graph.OutgoingGraphVertices(vertex) // []Vertex + for value := range node.output { // read from channel + obj.mutex.RLock() + cached, exists := obj.table[vertex] + obj.mutex.RUnlock() + if !exists { // first value received + node.loaded = true + obj.Logf("func `%s` started", node) + } else if value.Cmp(cached) == nil { + // skip if new value is same as previous + // if this happens often, it *might* be + // a bug in the function implementation + // FIXME: do we need to disable engine + // caching when using hysteresis? + obj.Logf("func `%s` skipped", node) + continue + } + obj.mutex.Lock() + // XXX: maybe we can get rid of the table... + obj.table[vertex] = value // save the latest + if err := node.Expr.SetValue(value); err != nil { + panic(fmt.Sprintf("could not set value for `%s`: %+v", node, err)) + } + obj.mutex.Unlock() + obj.Logf("func `%s` changed", node) + + // FIXME: will this actually prevent glitching? + // if we only notify the aggregate channel when + // we're at the bottom of the topo sort (eg: no + // outgoing vertices to notify) then we'll have + // a glitch free subtree in the programs ast... + if obj.Glitch || len(outgoing) == 0 { + select { + case obj.ag <- nil: // send to aggregate channel + + case <-obj.closeChan: + return + } + } + + // notify the receiving vertices + for _, v := range outgoing { + node := obj.state[v] + select { + case node.notify <- struct{}{}: + + case <-obj.closeChan: + return + } + } + } + // no more output values are coming... + obj.Logf("func `%s` stopped", node) + }(vertex) + } + + // send event on streamChan when any of the (aggregated) facts change + obj.wg.Add(1) + go func() { + defer obj.wg.Done() + defer close(obj.streamChan) + Loop: + for { + var err error + var ok bool + select { + case err, ok = <-obj.ag: // aggregated channel + if !ok { + break Loop // channel shutdown + } + + if !obj.loaded { + // now check if we're ready + var loaded = true // initially assume true + for _, vertex := range obj.topologicalSort { + node := obj.state[vertex] + if !node.loaded { + loaded = false // we were wrong + break + } + } + obj.loaded = loaded + + if obj.loaded { + // this causes an initial start + // signal to be sent out below, + // since the stream sender runs + if obj.Debug { + obj.Logf("loaded") + } + close(obj.loadedChan) // signal + } else { + if err == nil { + continue // not ready to send signal + } // pass errors through... + } + } + + case <-obj.closeChan: + return + } + + // send stream signal + select { + // send events or errors on streamChan, eg: func failure + case obj.streamChan <- err: // send + if err != nil { + return + } + case <-obj.closeChan: + return + } + } + }() + + return nil +} + +// agAdd registers a user on the ag channel. +func (obj *Engine) agAdd(i int) { + defer obj.agLock.Unlock() + obj.agLock.Lock() + obj.agCount += i +} + +// agDone closes the channel if we're the last one using it. +func (obj *Engine) agDone(vertex pgraph.Vertex) { + defer obj.agLock.Unlock() + obj.agLock.Lock() + node := obj.state[vertex] + node.closed = true + + // FIXME: (perf) cache this into a table which we narrow down with each + // successive call. look at the outgoing vertices that I would affect... + for _, v := range obj.Graph.OutgoingGraphVertices(vertex) { // close for each one + // now determine who provides inputs to that vertex... + var closed = true + for _, vv := range obj.Graph.IncomingGraphVertices(v) { + // are they all closed? + if !obj.state[vv].closed { + closed = false + break + } + } + if closed { // if they're all closed, we can close the input + close(obj.state[v].notify) + } + } + + if obj.agCount == 0 { + close(obj.ag) + } +} + +// Stream returns a channel of engine events. Wait for nil events to know when +// the Table map has changed. An error event means this will shutdown shortly. +// Do not run the Table function before we've received one non-error event. +func (obj *Engine) Stream() chan error { + return obj.streamChan +} + +// Close shuts down the function engine. It waits till everything has finished. +func (obj *Engine) Close() error { + var err error + for _, vertex := range obj.topologicalSort { // FIXME: should we do this in reverse? + node := obj.state[vertex] + if node.init { // did we Init this func? + if e := node.handle.Close(); e != nil { + e := errwrap.Wrapf(e, "problem closing func `%s`", node) + err = multierr.Append(err, e) // list of errors + } + } + } + close(obj.closeChan) + obj.wg.Wait() // wait so that each func doesn't need to do this in close + return err +} diff --git a/lang/funcs/facts/core/answerfact.go b/lang/funcs/facts/core/answerfact.go new file mode 100644 index 00000000..005568e3 --- /dev/null +++ b/lang/funcs/facts/core/answerfact.go @@ -0,0 +1,76 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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 core // TODO: should this be in its own individual package? + +import ( + "github.com/purpleidea/mgmt/lang/funcs/facts" + "github.com/purpleidea/mgmt/lang/types" +) + +func init() { + facts.Register("answer", func() facts.Fact { return &AnswerFact{} }) // must register the fact and name +} + +// Answer is the Answer to Life, the Universe and Everything. +const Answer = 42 + +// AnswerFact returns the Answer to Life, the Universe and Everything. +type AnswerFact struct { + init *facts.Init + closeChan chan struct{} +} + +// Validate makes sure we've built our struct properly. It is usually unused for +// normal facts that users can use directly. +//func (obj *AnswerFact) Validate() error { +// return nil +//} + +// Info returns some static info about itself. +func (obj *AnswerFact) Info() *facts.Info { + return &facts.Info{ + Output: types.NewType("int"), + } +} + +// Init runs some startup code for this fact. +func (obj *AnswerFact) Init(init *facts.Init) error { + obj.init = init + obj.closeChan = make(chan struct{}) + return nil +} + +// Stream returns the changing values that this fact has over time. In this +// case, it returns a single value and then exits, since this is a static fact. +func (obj *AnswerFact) Stream() error { + select { + case obj.init.Output <- &types.IntValue{ + V: Answer, + }: + case <-obj.closeChan: + return nil + } + close(obj.init.Output) // signal that we're done sending + return nil +} + +// Close runs some shutdown code for this fact and turns off the stream. +func (obj *AnswerFact) Close() error { + close(obj.closeChan) + return nil +} diff --git a/lang/funcs/facts/core/datetimefact.go b/lang/funcs/facts/core/datetimefact.go new file mode 100644 index 00000000..2b60bccf --- /dev/null +++ b/lang/funcs/facts/core/datetimefact.go @@ -0,0 +1,96 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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 core // TODO: should this be in its own individual package? + +import ( + "time" + + "github.com/purpleidea/mgmt/lang/funcs/facts" + "github.com/purpleidea/mgmt/lang/types" +) + +func init() { + facts.Register("datetime", func() facts.Fact { return &DateTimeFact{} }) // must register the fact and name +} + +// DateTimeFact is a fact which returns the current date and time. +type DateTimeFact struct { + init *facts.Init + closeChan chan struct{} +} + +// Validate makes sure we've built our struct properly. It is usually unused for +// normal facts that users can use directly. +//func (obj *DateTimeFact) Validate() error { +// return nil +//} + +// Info returns some static info about itself. +func (obj *DateTimeFact) Info() *facts.Info { + return &facts.Info{ + Output: types.NewType("int"), + } +} + +// Init runs some startup code for this fact. +func (obj *DateTimeFact) Init(init *facts.Init) error { + obj.init = init + obj.closeChan = make(chan struct{}) + return nil +} + +// Stream returns the changing values that this fact has over time. +func (obj *DateTimeFact) Stream() error { + defer close(obj.init.Output) // always signal when we're done + // XXX: this might be an interesting fact to write because: + // 1) will the sleeps from the ticker be in sync with the second ticker? + // 2) if we care about a less precise interval (eg: minute changes) can + // we set this up so it doesn't tick as often? -- Yes (make this a function or create a limit function to wrap this) + // 3) is it best to have a delta timer that wakes up before it's needed + // and calculates how much longer to sleep for? + ticker := time.NewTicker(time.Duration(1) * time.Second) + + // streams must generate an initial event on startup + startChan := make(chan struct{}) // start signal + close(startChan) // kick it off! + defer ticker.Stop() + for { + select { + case <-startChan: // kick the loop once at start + startChan = nil // disable + case <-ticker.C: // received the timer event + // pass + case <-obj.closeChan: + return nil + } + + select { + case obj.init.Output <- &types.IntValue{ // seconds since 1970... + V: time.Now().Unix(), // .UTC() not necessary + }: + case <-obj.closeChan: + return nil + } + } +} + +// Close runs some shutdown code for this fact and turns off the stream. +func (obj *DateTimeFact) Close() error { + close(obj.closeChan) + return nil +} diff --git a/lang/funcs/facts/core/flipflopfact.go b/lang/funcs/facts/core/flipflopfact.go new file mode 100644 index 00000000..e2c7a70b --- /dev/null +++ b/lang/funcs/facts/core/flipflopfact.go @@ -0,0 +1,97 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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 core // TODO: should this be in its own individual package? + +import ( + "time" + + "github.com/purpleidea/mgmt/lang/funcs/facts" + "github.com/purpleidea/mgmt/lang/types" +) + +func init() { + // TODO: rename these `play` facts to start with a test_ prefix or similar + facts.Register("flipflop", func() facts.Fact { return &FlipFlopFact{} }) // must register the fact and name +} + +// FlipFlopFact is a fact which flips a bool repeatedly. This is an example fact +// and is not meant for serious computing. This would be better served by a flip +// function which you could specify an interval for. +type FlipFlopFact struct { + init *facts.Init + value bool + closeChan chan struct{} +} + +// Validate makes sure we've built our struct properly. It is usually unused for +// normal facts that users can use directly. +//func (obj *FlipFlopFact) Validate() error { +// return nil +//} + +// Info returns some static info about itself. +func (obj *FlipFlopFact) Info() *facts.Info { + return &facts.Info{ + Output: types.NewType("bool"), + } +} + +// Init runs some startup code for this fact. +func (obj *FlipFlopFact) Init(init *facts.Init) error { + obj.init = init + obj.closeChan = make(chan struct{}) + return nil +} + +// Stream returns the changing values that this fact has over time. +func (obj *FlipFlopFact) Stream() error { + defer close(obj.init.Output) // always signal when we're done + // TODO: don't hard code 5 sec interval + ticker := time.NewTicker(time.Duration(5) * time.Second) + + // streams must generate an initial event on startup + startChan := make(chan struct{}) // start signal + close(startChan) // kick it off! + defer ticker.Stop() + for { + select { + case <-startChan: // kick the loop once at start + startChan = nil // disable + case <-ticker.C: // received the timer event + // pass + case <-obj.closeChan: + return nil + } + + select { + case obj.init.Output <- &types.BoolValue{ // flip + V: obj.value, + }: + case <-obj.closeChan: + return nil + } + + obj.value = !obj.value // flip it + } +} + +// Close runs some shutdown code for this fact and turns off the stream. +func (obj *FlipFlopFact) Close() error { + close(obj.closeChan) + return nil +} diff --git a/lang/funcs/facts/core/hostnamefact.go b/lang/funcs/facts/core/hostnamefact.go new file mode 100644 index 00000000..1d0bd3c8 --- /dev/null +++ b/lang/funcs/facts/core/hostnamefact.go @@ -0,0 +1,74 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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 core // TODO: should this be in its own individual package? + +import ( + "github.com/purpleidea/mgmt/lang/funcs/facts" + "github.com/purpleidea/mgmt/lang/types" +) + +func init() { + facts.Register("hostname", func() facts.Fact { return &HostnameFact{} }) // must register the fact and name +} + +// HostnameFact is a function that returns the hostname. +// TODO: support hostnames that change in the future. +type HostnameFact struct { + init *facts.Init + closeChan chan struct{} +} + +// Validate makes sure we've built our struct properly. It is usually unused for +// normal facts that users can use directly. +//func (obj *HostnameFact) Validate() error { +// return nil +//} + +// Info returns some static info about itself. +func (obj *HostnameFact) Info() *facts.Info { + return &facts.Info{ + Output: types.NewType("str"), + } +} + +// Init runs some startup code for this const. +func (obj *HostnameFact) Init(init *facts.Init) error { + obj.init = init + obj.closeChan = make(chan struct{}) + return nil +} + +// Stream returns the single value that this const has, and then closes. +func (obj *HostnameFact) Stream() error { + select { + case obj.init.Output <- &types.StrValue{ + V: obj.init.Hostname, + }: + // pass + case <-obj.closeChan: + return nil + } + close(obj.init.Output) // signal that we're done sending + return nil +} + +// Close runs some shutdown code for this const and turns off the stream. +func (obj *HostnameFact) Close() error { + close(obj.closeChan) + return nil +} diff --git a/lang/funcs/facts/core/loadfact.go b/lang/funcs/facts/core/loadfact.go new file mode 100644 index 00000000..a7e8e42e --- /dev/null +++ b/lang/funcs/facts/core/loadfact.go @@ -0,0 +1,131 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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 core // TODO: should this be in its own individual package? + +import ( + "syscall" + "time" + + "github.com/purpleidea/mgmt/lang/funcs/facts" + "github.com/purpleidea/mgmt/lang/types" + + errwrap "github.com/pkg/errors" +) + +const ( + // LoadScale factor scales the output from sysinfo to the correct float + // value. + LoadScale = 65536 // XXX: is this correct or should it be 65535? + + loadSignature = "struct{x1 float; x5 float; x15 float}" +) + +func init() { + facts.Register("load", func() facts.Fact { return &LoadFact{} }) // must register the fact and name +} + +// LoadFact is a fact which returns the current system load. +type LoadFact struct { + init *facts.Init + closeChan chan struct{} +} + +// Validate makes sure we've built our struct properly. It is usually unused for +// normal facts that users can use directly. +//func (obj *LoadFact) Validate() error { +// return nil +//} + +// Info returns some static info about itself. +func (obj *LoadFact) Info() *facts.Info { + return &facts.Info{ + Output: types.NewType(loadSignature), + } +} + +// Init runs some startup code for this fact. +func (obj *LoadFact) Init(init *facts.Init) error { + obj.init = init + obj.closeChan = make(chan struct{}) + return nil +} + +// Stream returns the changing values that this fact has over time. +func (obj *LoadFact) Stream() error { + defer close(obj.init.Output) // always signal when we're done + + // it seems the different values only update once every 5 + // seconds, so that's as often as we need to refresh this! + // TODO: lookup this value if it's something configurable + ticker := time.NewTicker(time.Duration(5) * time.Second) + + // streams must generate an initial event on startup + startChan := make(chan struct{}) // start signal + close(startChan) // kick it off! + defer ticker.Stop() + for { + select { + case <-startChan: // kick the loop once at start + startChan = nil // disable + case <-ticker.C: // received the timer event + // pass + case <-obj.closeChan: + return nil + } + + x1, x5, x15, err := load() + if err != nil { + return errwrap.Wrapf(err, "could not read load values") + } + + st := types.NewStruct(types.NewType(loadSignature)) + for k, v := range map[string]float64{"x1": x1, "x5": x5, "x15": x15} { + if err := st.Set(k, &types.FloatValue{V: v}); err != nil { + return errwrap.Wrapf(err, "struct could not set key: `%s`", k) + } + } + + select { + case obj.init.Output <- st: + // send + case <-obj.closeChan: + return nil + } + } +} + +// Close runs some shutdown code for this fact and turns off the stream. +func (obj *LoadFact) Close() error { + close(obj.closeChan) + return nil +} + +// load returns the system load averages for the last minute, five minutes and +// fifteen minutes. Calling this more often than once every five seconds seems +// to be unnecessary, since the kernel only updates these values that often. +// TODO: is the kernel update interval configurable? +func load() (one, five, fifteen float64, err error) { + var sysinfo syscall.Sysinfo_t + if err = syscall.Sysinfo(&sysinfo); err != nil { + return + } + one = float64(sysinfo.Loads[0]) / LoadScale + five = float64(sysinfo.Loads[1]) / LoadScale + fifteen = float64(sysinfo.Loads[2]) / LoadScale + return +} diff --git a/lang/funcs/facts/facts.go b/lang/funcs/facts/facts.go new file mode 100644 index 00000000..fbdf53a3 --- /dev/null +++ b/lang/funcs/facts/facts.go @@ -0,0 +1,77 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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 facts provides a framework for language values that change over time. +package facts + +import ( + "fmt" + + "github.com/purpleidea/mgmt/lang/funcs" + "github.com/purpleidea/mgmt/lang/interfaces" + "github.com/purpleidea/mgmt/lang/types" + "github.com/purpleidea/mgmt/resources" +) + +// RegisteredFacts is a global map of all possible facts which can be used. You +// should never touch this map directly. Use methods like Register instead. +var RegisteredFacts = make(map[string]func() Fact) // must initialize + +// Register takes a fact and its name and makes it available for use. It is +// commonly called in the init() method of the fact at program startup. There is +// no matching Unregister function. +func Register(name string, fn func() Fact) { + if _, ok := RegisteredFacts[name]; ok { + panic(fmt.Sprintf("a fact named %s is already registered", name)) + } + //gob.Register(fn()) + funcs.Register(name, func() interfaces.Func { // implement in terms of func interface + return &FactFunc{ + Fact: fn(), + } + }) + RegisteredFacts[name] = fn +} + +// Info is a static representation of some information about the fact. It is +// used for static analysis and type checking. If you break this contract, you +// might cause a panic. +type Info struct { + Output *types.Type // output value type (must not change over time!) + Err error // did this fact validate? +} + +type Init struct { + Hostname string // uuid for the host + //Noop bool + Output chan types.Value // Stream must close `output` chan + World resources.World +} + +// Fact is the interface that any valid fact must fulfill. It is very simple, +// but still event driven. Facts should attempt to only send values when they +// have changed. +// TODO: should we support a static version of this interface for facts that +// never change to avoid the overhead of the goroutine and channel listener? +// TODO: should we move this to the interface package? +type Fact interface { + //Validate() error // currently not needed since no facts are internal + Info() *Info + Init(*Init) error + Stream() error + Close() error +} diff --git a/lang/funcs/facts/func.go b/lang/funcs/facts/func.go new file mode 100644 index 00000000..6fdf5012 --- /dev/null +++ b/lang/funcs/facts/func.go @@ -0,0 +1,77 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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 facts + +import ( + "fmt" + + "github.com/purpleidea/mgmt/lang/interfaces" + "github.com/purpleidea/mgmt/lang/types" +) + +// FactFunc is a wrapper for the fact interface. It implements the fact +// interface in terms of Func to reduce the two down to a single mechanism. +type FactFunc struct { // implements `interfaces.Func` + Fact Fact +} + +// Validate makes sure we've built our struct properly. +func (obj *FactFunc) Validate() error { + if obj.Fact == nil { + return fmt.Errorf("must specify a Fact in struct") + } + //return obj.Fact.Validate() // currently unused + return nil +} + +// Info returns some static info about itself. +func (obj *FactFunc) Info() *interfaces.Info { + return &interfaces.Info{ + Pure: false, + Memo: false, + Sig: &types.Type{ + Kind: types.KindFunc, + // if Ord or Map are nil, this will panic things! + Ord: []string{}, + Map: make(map[string]*types.Type), + Out: obj.Fact.Info().Output, + }, + Err: obj.Fact.Info().Err, + } +} + +// Init runs some startup code for this fact. +func (obj *FactFunc) Init(init *interfaces.Init) error { + return obj.Fact.Init( + &Init{ + Hostname: init.Hostname, + Output: init.Output, + World: init.World, + }, + ) +} + +// Stream returns the changing values that this fact has over time. +func (obj *FactFunc) Stream() error { + return obj.Fact.Stream() +} + +// Close runs some shutdown code for this fact and turns off the stream. +func (obj *FactFunc) Close() error { + return obj.Fact.Close() +} diff --git a/lang/funcs/facts/func_test.go b/lang/funcs/facts/func_test.go new file mode 100644 index 00000000..35e0657f --- /dev/null +++ b/lang/funcs/facts/func_test.go @@ -0,0 +1,97 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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 facts + +import ( + "log" + "testing" + "time" + + "github.com/purpleidea/mgmt/lang/funcs" + "github.com/purpleidea/mgmt/pgraph" +) + +const Debug = false // switch on for more interactive log messages when testing! + +// logf switches messages to use realtime logging when debugging tests, and the +// quiet logging which is not shown until test failures, when debug mode is off. +func logf(t *testing.T, format string, args ...interface{}) { + if Debug { + log.Printf(format, args...) + } else { + t.Logf(format, args...) + } +} + +func TestFuncGraph0(t *testing.T) { + logf(t, "Hello!") + g, _ := pgraph.NewGraph("empty") // empty graph + + obj := &funcs.Engine{ + Graph: g, + } + + logf(t, "Init...") + if err := obj.Init(); err != nil { + t.Errorf("could not init: %+v", err) + return + } + + logf(t, "Validate...") + if err := obj.Validate(); err != nil { + t.Errorf("could not validate: %+v", err) + return + } + + logf(t, "Run...") + if err := obj.Run(); err != nil { + t.Errorf("could not run: %+v", err) + return + } + + // wait for some activity + logf(t, "Stream...") + stream := obj.Stream() + logf(t, "Loop...") + br := time.After(time.Duration(5) * time.Second) +Loop: + for { + select { + case err, ok := <-stream: + if !ok { + logf(t, "Stream break...") + break Loop + } + if err != nil { + logf(t, "Error: %+v", err) + continue + } + + case <-br: + logf(t, "Break...") + t.Errorf("empty graph should have closed stream") + break Loop + } + } + + logf(t, "Closing...") + if err := obj.Close(); err != nil { + t.Errorf("could not close: %+v", err) + return + } +} diff --git a/lang/funcs/funcs.go b/lang/funcs/funcs.go new file mode 100644 index 00000000..18a396df --- /dev/null +++ b/lang/funcs/funcs.go @@ -0,0 +1,52 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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 funcs provides a framework for functions that change over time. +package funcs + +import ( + "fmt" + + "github.com/purpleidea/mgmt/lang/interfaces" +) + +// RegisteredFuncs is a global map of all possible funcs which can be used. You +// should never touch this map directly. Use methods like Register instead. It +// includes implementations which also satisfy PolyFunc as well. +var RegisteredFuncs = make(map[string]func() interfaces.Func) // must initialize + +// Register takes a func and its name and makes it available for use. It is +// commonly called in the init() method of the func at program startup. There is +// no matching Unregister function. You may also register functions which +// satisfy the PolyFunc interface. +func Register(name string, fn func() interfaces.Func) { + if _, exists := RegisteredFuncs[name]; exists { + panic(fmt.Sprintf("a func named %s is already registered", name)) + } + //gob.Register(fn()) + RegisteredFuncs[name] = fn +} + +// Lookup returns a pointer to the function's struct. It may be convertible to a +// PolyFunc if the particular function implements those additional methods. +func Lookup(name string) (interfaces.Func, error) { + f, exists := RegisteredFuncs[name] + if !exists { + return nil, fmt.Errorf("not found") + } + return f(), nil +} diff --git a/lang/funcs/funcs_test.go b/lang/funcs/funcs_test.go new file mode 100644 index 00000000..fbe7cfab --- /dev/null +++ b/lang/funcs/funcs_test.go @@ -0,0 +1,21 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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 funcs + +// most of the testing of this package is inside of the adjacent `facts` package +// because it imports this package and thus lets us use those constructs to test diff --git a/lang/funcs/history_polyfunc.go b/lang/funcs/history_polyfunc.go new file mode 100644 index 00000000..6b5605e9 --- /dev/null +++ b/lang/funcs/history_polyfunc.go @@ -0,0 +1,234 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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 funcs // TODO: should this be in its own individual package? + +import ( + "fmt" + + "github.com/purpleidea/mgmt/lang/interfaces" + "github.com/purpleidea/mgmt/lang/types" +) + +const ( + // HistoryFuncName is the name this function is registered as. This + // starts with an underscore so that it cannot be used from the lexer. + HistoryFuncName = "_history" +) + +func init() { + Register(HistoryFuncName, func() interfaces.Func { return &HistoryFunc{} }) // must register the func and name +} + +// HistoryFunc is special function which returns the Nth oldest value seen. It +// must store up incoming values until it gets enough to return the desired one. +// A restart of the program, will expunge the stored state. This obviously takes +// more memory, the further back you wish to index. A change in the index var is +// generally not useful, but it is permitted. Moving it to a smaller value will +// cause older index values to be expunged. If this is undesirable, a max count +// could be added. This was not implemented with efficiency in mind. Since some +// functions might not send out un-changed values, it might also make sense to +// implement a *time* based hysteresis, since this only looks at the last N +// changed values. A time based hysteresis would tick every precision-width, and +// store whatever the latest value at that time is. +type HistoryFunc struct { + Type *types.Type // type of input value (same as output type) + + init *interfaces.Init + + history []types.Value // goes from newest (index->0) to oldest (len()-1) + + result types.Value // last calculated output + + closeChan chan struct{} +} + +// Polymorphisms returns the possible type signature for this function. In this +// case, since the number of possible types for the first arg can be infinite, +// it returns the final precise type only if it can be gleamed statically. If +// not, it returns that unknown as a variant, which is hopefully solved during +// unification. +func (obj *HistoryFunc) Polymorphisms(partialType *types.Type, partialValues []types.Value) ([]*types.Type, error) { + // TODO: return `variant` as first & return arg for now -- maybe there's a better way? + variant := []*types.Type{types.NewType("func(value variant, index int) variant")} + + if partialType == nil { + return variant, nil + } + + var typ *types.Type // = nil is implied + + ord := partialType.Ord + if partialType.Map != nil { + if len(ord) != 2 { + return nil, fmt.Errorf("must have at exactly two args in history func") + } + if t, exists := partialType.Map[ord[1]]; exists && t != nil { + if t.Cmp(types.TypeInt) != nil { + return nil, fmt.Errorf("second arg for history must be an int") + } + } + + if t, exists := partialType.Map[ord[0]]; exists && t != nil && partialType.Out != nil { + if t.Cmp(partialType.Out) != nil { + return nil, fmt.Errorf("type of first arg for history must match return type") + } + typ = t // it has been found :) + } + } + + if partialType.Out != nil { + typ = partialType.Out // it has been found :) + } + + if typ == nil { + return variant, nil + } + + t := types.NewType(fmt.Sprintf("func(value %s, index int) %s", typ.String(), typ.String())) + + return []*types.Type{t}, nil // return a list with a single possibility +} + +// Build takes the now known function signature and stores it so that this +// function can appear to be static. That type is used to build our function +// statically. +func (obj *HistoryFunc) Build(typ *types.Type) error { + if typ.Kind != types.KindFunc { + return fmt.Errorf("input type must be of kind func") + } + if len(typ.Ord) != 2 { + return fmt.Errorf("the history function needs exactly two args") + } + if typ.Out == nil { + return fmt.Errorf("return type of function must be specified") + } + if typ.Map == nil { + return fmt.Errorf("invalid input type") + } + + t1, exists := typ.Map[typ.Ord[1]] + if !exists || t1 == nil { + return fmt.Errorf("second arg must be specified") + } + if t1.Cmp(types.TypeInt) != nil { + return fmt.Errorf("second arg for history must be an int") + } + + t0, exists := typ.Map[typ.Ord[0]] + if !exists || t0 == nil { + return fmt.Errorf("first arg must be specified") + } + obj.Type = t0 // type of historical value is now known! + + return nil +} + +// Validate makes sure we've built our struct properly. It is usually unused for +// normal functions that users can use directly. +func (obj *HistoryFunc) Validate() error { + if obj.Type == nil { // build must be run first + return fmt.Errorf("type is still unspecified") + } + return nil +} + +// Info returns some static info about itself. +func (obj *HistoryFunc) Info() *interfaces.Info { + return &interfaces.Info{ + Pure: false, // definitely false + Memo: false, + Sig: types.NewType(fmt.Sprintf("func(value %s, index int) %s", obj.Type.String(), obj.Type.String())), + Err: obj.Validate(), + } +} + +// Init runs some startup code for this fact. +func (obj *HistoryFunc) Init(init *interfaces.Init) error { + obj.init = init + obj.closeChan = make(chan struct{}) + return nil +} + +// Stream returns the changing values that this func has over time. +func (obj *HistoryFunc) Stream() error { + defer close(obj.init.Output) // the sender closes + for { + select { + case input, ok := <-obj.init.Input: + if !ok { + return nil // can't output any more + } + //if err := input.Type().Cmp(obj.Info().Sig.Input); err != nil { + // return errwrap.Wrapf(err, "wrong function input") + //} + + //if obj.last != nil && input.Cmp(obj.last) == nil { + // continue // value didn't change, skip it + //} + //obj.last = input // store for next + + index := int(input.Struct()["index"].Int()) + value := input.Struct()["value"] + var result types.Value + + if index < 0 { + return fmt.Errorf("can't use a negative index of %d", index) + } + + // 1) truncate history so length equals index + if len(obj.history) > index { + // remove all but first N elements, where N == index + obj.history = obj.history[:index] + } + + // 2) (un)shift (add our new value to the beginning) + obj.history = append([]types.Value{value}, obj.history...) + + // 3) are we ready to output a sufficiently old value? + if index >= len(obj.history) { + continue // not enough history is stored yet... + } + + // 4) read one off the back + result = obj.history[len(obj.history)-1] + + // TODO: do we want to do this? + // if the result is still the same, skip sending an update... + if obj.result != nil && result.Cmp(obj.result) == nil { + continue // result didn't change + } + obj.result = result // store new result + + case <-obj.closeChan: + return nil + } + + select { + case obj.init.Output <- obj.result: // send + // pass + case <-obj.closeChan: + return nil + } + } +} + +// Close runs some shutdown code for this fact and turns off the stream. +func (obj *HistoryFunc) Close() error { + close(obj.closeChan) + return nil +} diff --git a/lang/funcs/maplookup_polyfunc.go b/lang/funcs/maplookup_polyfunc.go new file mode 100644 index 00000000..9ff2cab9 --- /dev/null +++ b/lang/funcs/maplookup_polyfunc.go @@ -0,0 +1,285 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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 funcs + +import ( + "fmt" + + "github.com/purpleidea/mgmt/lang/interfaces" + "github.com/purpleidea/mgmt/lang/types" + + errwrap "github.com/pkg/errors" +) + +const ( + // MapLookupFuncName is the name this function is registered as. This + // starts with an underscore so that it cannot be used from the lexer. + // XXX: change to _maplookup and add syntax in the lexer/parser + MapLookupFuncName = "maplookup" +) + +func init() { + Register(MapLookupFuncName, func() interfaces.Func { return &MapLookupPolyFunc{} }) // must register the func and name +} + +// MapLookupPolyFunc is a key map lookup function. +type MapLookupPolyFunc struct { + Type *types.Type // Kind == Map, that is used as the map we lookup + + init *interfaces.Init + last types.Value // last value received to use for diff + + result types.Value // last calculated output + + closeChan chan struct{} +} + +// Polymorphisms returns the list of possible function signatures available for +// this static polymorphic function. It relies on type and value hints to limit +// the number of returned possibilities. +func (obj *MapLookupPolyFunc) Polymorphisms(partialType *types.Type, partialValues []types.Value) ([]*types.Type, error) { + // TODO: return `variant` as arg for now -- maybe there's a better way? + variant := []*types.Type{types.NewType("func(map variant, key variant, default variant) variant")} + + if partialType == nil { + return variant, nil + } + + // what's the map type of the first argument? + typ := &types.Type{ + Kind: types.KindMap, + //Key: ???, + //Val: ???, + } + + ord := partialType.Ord + if partialType.Map != nil { + if len(ord) != 3 { + return nil, fmt.Errorf("must have exactly three args in maplookup func") + } + if tMap, exists := partialType.Map[ord[0]]; exists && tMap != nil { + if tMap.Kind != types.KindMap { + return nil, fmt.Errorf("first arg for maplookup must be a map") + } + typ.Key = tMap.Key + typ.Val = tMap.Val + } + if tKey, exists := partialType.Map[ord[1]]; exists && tKey != nil { + if typ.Key != nil && typ.Key.Cmp(tKey) != nil { + return nil, fmt.Errorf("second arg for maplookup must match map's key type") + } + typ.Key = tKey + } + if tDef, exists := partialType.Map[ord[2]]; exists && tDef != nil { + if typ.Val != nil && typ.Val.Cmp(tDef) != nil { + return nil, fmt.Errorf("third arg for maplookup must match map's val type") + } + typ.Val = tDef + + // add this for better error messages + if tOut := partialType.Out; tOut != nil { + if tDef.Cmp(tOut) != nil { + return nil, fmt.Errorf("third arg for maplookup must match return type") + } + } + } + if tOut := partialType.Out; tOut != nil { + if typ.Val != nil && typ.Val.Cmp(tOut) != nil { + return nil, fmt.Errorf("return type for maplookup must match map's val type") + } + typ.Val = tOut + } + } + + // TODO: are we okay adding just the map val type and not the map key type? + //if tOut := partialType.Out; tOut != nil { + // if typ.Val != nil && typ.Val.Cmp(tOut) != nil { + // return nil, fmt.Errorf("return type for maplookup must match map's val type") + // } + // typ.Val = tOut + //} + + typFunc := &types.Type{ + Kind: types.KindFunc, // function type + Map: make(map[string]*types.Type), + Ord: []string{"map", "key", "default"}, + Out: nil, + } + typFunc.Map["map"] = typ + typFunc.Map["key"] = typ.Key + typFunc.Map["default"] = typ.Val + typFunc.Out = typ.Val + + // TODO: don't include partial internal func map's for now, allow in future? + if typ.Key == nil || typ.Val == nil { + typFunc.Map = make(map[string]*types.Type) // erase partial + typFunc.Map["map"] = types.TypeVariant + typFunc.Map["key"] = types.TypeVariant + typFunc.Map["default"] = types.TypeVariant + } + if typ.Val == nil { + typFunc.Out = types.TypeVariant + } + + // just returning nothing for now, in case we can't detect a partial map + if typ.Key == nil || typ.Val == nil { + return []*types.Type{typFunc}, nil + } + + // TODO: type check that the partialValues are compatible + + return []*types.Type{typFunc}, nil // solved! +} + +// Build is run to turn the polymorphic, undeterminted function, into the +// specific statically type version. It is usually run after Unify completes, +// and must be run before Info() and any of the other Func interface methods are +// used. This function is idempotent, as long as the arg isn't changed between +// runs. +func (obj *MapLookupPolyFunc) Build(typ *types.Type) error { + // typ is the KindFunc signature we're trying to build... + if typ.Kind != types.KindFunc { + return fmt.Errorf("input type must be of kind func") + } + + if len(typ.Ord) != 3 { + return fmt.Errorf("the maplookup function needs exactly three args") + } + if typ.Out == nil { + return fmt.Errorf("return type of function must be specified") + } + if typ.Map == nil { + return fmt.Errorf("invalid input type") + } + + tMap, exists := typ.Map[typ.Ord[0]] + if !exists || tMap == nil { + return fmt.Errorf("first arg must be specified") + } + + tKey, exists := typ.Map[typ.Ord[1]] + if !exists || tKey == nil { + return fmt.Errorf("second arg must be specified") + } + + tDef, exists := typ.Map[typ.Ord[2]] + if !exists || tDef == nil { + return fmt.Errorf("third arg must be specified") + } + + if err := tMap.Key.Cmp(tKey); err != nil { + return errwrap.Wrapf(err, "key must match map key type") + } + + if err := tMap.Val.Cmp(tDef); err != nil { + return errwrap.Wrapf(err, "default must match map val type") + } + + if err := tMap.Val.Cmp(typ.Out); err != nil { + return errwrap.Wrapf(err, "return type must match map val type") + } + + obj.Type = tMap // map type + return nil +} + +// Validate tells us if the input struct takes a valid form. +func (obj *MapLookupPolyFunc) Validate() error { + if obj.Type == nil { // build must be run first + return fmt.Errorf("type is still unspecified") + } + if obj.Type.Kind != types.KindMap { + return fmt.Errorf("type must be a kind of map") + } + return nil +} + +// Info returns some static info about itself. Build must be called before this +// will return correct data. +func (obj *MapLookupPolyFunc) Info() *interfaces.Info { + typ := types.NewType(fmt.Sprintf("func(map %s, key %s, default %s) %s", obj.Type.String(), obj.Type.Key.String(), obj.Type.Val.String(), obj.Type.Val.String())) + return &interfaces.Info{ + Pure: true, + Memo: false, + Sig: typ, // func kind + Err: obj.Validate(), + } +} + +// Init runs some startup code for this fact. +func (obj *MapLookupPolyFunc) Init(init *interfaces.Init) error { + obj.init = init + obj.closeChan = make(chan struct{}) + return nil +} + +// Stream returns the changing values that this func has over time. +func (obj *MapLookupPolyFunc) Stream() error { + defer close(obj.init.Output) // the sender closes + for { + select { + case input, ok := <-obj.init.Input: + if !ok { + return nil // can't output any more + } + //if err := input.Type().Cmp(obj.Info().Sig.Input); err != nil { + // return errwrap.Wrapf(err, "wrong function input") + //} + + if obj.last != nil && input.Cmp(obj.last) == nil { + continue // value didn't change, skip it + } + obj.last = input // store for next + + m := (input.Struct()["map"]).(*types.MapValue) + key := input.Struct()["key"] + def := input.Struct()["default"] + + var result types.Value + val, exists := m.Lookup(key) + if exists { + result = val + } else { + result = def + } + + // if previous input was `2 + 4`, but now it + // changed to `1 + 5`, the result is still the + // same, so we can skip sending an update... + if obj.result != nil && result.Cmp(obj.result) == nil { + continue // result didn't change + } + obj.result = result // store new result + + case <-obj.closeChan: + return nil + } + + select { + case obj.init.Output <- obj.result: // send + case <-obj.closeChan: + return nil + } + } +} + +// Close runs some shutdown code for this fact and turns off the stream. +func (obj *MapLookupPolyFunc) Close() error { + close(obj.closeChan) + return nil +} diff --git a/lang/funcs/operator_polyfunc.go b/lang/funcs/operator_polyfunc.go new file mode 100644 index 00000000..bf93cbdc --- /dev/null +++ b/lang/funcs/operator_polyfunc.go @@ -0,0 +1,737 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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 funcs // this is here, in case we allow others to register operators... + +import ( + "fmt" + "math" + + "github.com/purpleidea/mgmt/lang/interfaces" + "github.com/purpleidea/mgmt/lang/types" + "github.com/purpleidea/mgmt/util" + + errwrap "github.com/pkg/errors" +) + +const ( + // OperatorFuncName is the name this function is registered as. This + // starts with an underscore so that it cannot be used from the lexer. + OperatorFuncName = "_operator" + + // operatorArgName is the edge and arg name used for the function's operator. + operatorArgName = "x" // something short and arbitrary +) + +func init() { + // concatenation + RegisterOperator("+", &types.FuncValue{ + T: types.NewType("func(a str, b str) str"), + V: func(input []types.Value) (types.Value, error) { + return &types.StrValue{ + V: input[0].Str() + input[1].Str(), + }, nil + }, + }) + // addition + RegisterOperator("+", &types.FuncValue{ + T: types.NewType("func(a int, b int) int"), + V: func(input []types.Value) (types.Value, error) { + //if l := len(input); l != 2 { + // return nil, fmt.Errorf("expected two inputs, got: %d", l) + //} + // FIXME: check for overflow? + return &types.IntValue{ + V: input[0].Int() + input[1].Int(), + }, nil + }, + }) + // floating-point addition + RegisterOperator("+", &types.FuncValue{ + T: types.NewType("func(a float, b float) float"), + V: func(input []types.Value) (types.Value, error) { + return &types.FloatValue{ + V: input[0].Float() + input[1].Float(), + }, nil + }, + }) + + // subtraction + RegisterOperator("-", &types.FuncValue{ + T: types.NewType("func(a int, b int) int"), + V: func(input []types.Value) (types.Value, error) { + return &types.IntValue{ + V: input[0].Int() - input[1].Int(), + }, nil + }, + }) + // floating-point subtraction + RegisterOperator("-", &types.FuncValue{ + T: types.NewType("func(a float, b float) float"), + V: func(input []types.Value) (types.Value, error) { + return &types.FloatValue{ + V: input[0].Float() - input[1].Float(), + }, nil + }, + }) + + // multiplication + RegisterOperator("*", &types.FuncValue{ + T: types.NewType("func(a int, b int) int"), + V: func(input []types.Value) (types.Value, error) { + // FIXME: check for overflow? + return &types.IntValue{ + V: input[0].Int() * input[1].Int(), + }, nil + }, + }) + // floating-point multiplication + RegisterOperator("*", &types.FuncValue{ + T: types.NewType("func(a float, b float) float"), + V: func(input []types.Value) (types.Value, error) { + return &types.FloatValue{ + V: input[0].Float() * input[1].Float(), + }, nil + }, + }) + + // don't add: `func(int, float) float` or: `func(float, int) float` + // division + RegisterOperator("/", &types.FuncValue{ + T: types.NewType("func(a int, b int) float"), + V: func(input []types.Value) (types.Value, error) { + divisor := input[1].Int() + if divisor == 0 { + return nil, fmt.Errorf("can't divide by zero") + } + return &types.FloatValue{ + V: float64(input[0].Int()) / float64(divisor), + }, nil + }, + }) + // floating-point division + RegisterOperator("/", &types.FuncValue{ + T: types.NewType("func(a float, b float) float"), + V: func(input []types.Value) (types.Value, error) { + divisor := input[1].Float() + if divisor == 0.0 { + return nil, fmt.Errorf("can't divide by zero") + } + return &types.FloatValue{ + V: input[0].Float() / divisor, + }, nil + }, + }) + + // string equality + RegisterOperator("==", &types.FuncValue{ + T: types.NewType("func(a str, b str) bool"), + V: func(input []types.Value) (types.Value, error) { + return &types.BoolValue{ + V: input[0].Str() == input[1].Str(), + }, nil + }, + }) + // bool equality + RegisterOperator("==", &types.FuncValue{ + T: types.NewType("func(a bool, b bool) bool"), + V: func(input []types.Value) (types.Value, error) { + return &types.BoolValue{ + V: input[0].Bool() == input[1].Bool(), + }, nil + }, + }) + // int equality + RegisterOperator("==", &types.FuncValue{ + T: types.NewType("func(a int, b int) bool"), + V: func(input []types.Value) (types.Value, error) { + return &types.BoolValue{ + V: input[0].Int() == input[1].Int(), + }, nil + }, + }) + // floating-point equality + RegisterOperator("==", &types.FuncValue{ + T: types.NewType("func(a float, b float) bool"), + V: func(input []types.Value) (types.Value, error) { + // TODO: should we do an epsilon check? + return &types.BoolValue{ + V: input[0].Float() == input[1].Float(), + }, nil + }, + }) + + // string in-equality + RegisterOperator("!=", &types.FuncValue{ + T: types.NewType("func(a str, b str) bool"), + V: func(input []types.Value) (types.Value, error) { + return &types.BoolValue{ + V: input[0].Str() == input[1].Str(), + }, nil + }, + }) + // bool in-equality + RegisterOperator("!=", &types.FuncValue{ + T: types.NewType("func(a bool, b bool) bool"), + V: func(input []types.Value) (types.Value, error) { + return &types.BoolValue{ + V: input[0].Bool() != input[1].Bool(), + }, nil + }, + }) + // int in-equality + RegisterOperator("!=", &types.FuncValue{ + T: types.NewType("func(a int, b int) bool"), + V: func(input []types.Value) (types.Value, error) { + return &types.BoolValue{ + V: input[0].Int() != input[1].Int(), + }, nil + }, + }) + // floating-point in-equality + RegisterOperator("!=", &types.FuncValue{ + T: types.NewType("func(a float, b float) bool"), + V: func(input []types.Value) (types.Value, error) { + // TODO: should we do an epsilon check? + return &types.BoolValue{ + V: input[0].Float() != input[1].Float(), + }, nil + }, + }) + + // less-than + RegisterOperator("<", &types.FuncValue{ + T: types.NewType("func(a int, b int) bool"), + V: func(input []types.Value) (types.Value, error) { + return &types.BoolValue{ + V: input[0].Int() < input[1].Int(), + }, nil + }, + }) + // floating-point less-than + RegisterOperator("<", &types.FuncValue{ + T: types.NewType("func(a float, b float) bool"), + V: func(input []types.Value) (types.Value, error) { + // TODO: should we do an epsilon check? + return &types.BoolValue{ + V: input[0].Float() < input[1].Float(), + }, nil + }, + }) + // greater-than + RegisterOperator(">", &types.FuncValue{ + T: types.NewType("func(a int, b int) bool"), + V: func(input []types.Value) (types.Value, error) { + return &types.BoolValue{ + V: input[0].Int() > input[1].Int(), + }, nil + }, + }) + // floating-point greater-than + RegisterOperator(">", &types.FuncValue{ + T: types.NewType("func(a float, b float) bool"), + V: func(input []types.Value) (types.Value, error) { + // TODO: should we do an epsilon check? + return &types.BoolValue{ + V: input[0].Float() > input[1].Float(), + }, nil + }, + }) + // less-than-equal + RegisterOperator("<=", &types.FuncValue{ + T: types.NewType("func(a int, b int) bool"), + V: func(input []types.Value) (types.Value, error) { + return &types.BoolValue{ + V: input[0].Int() <= input[1].Int(), + }, nil + }, + }) + // floating-point less-than-equal + RegisterOperator("<=", &types.FuncValue{ + T: types.NewType("func(a float, b float) bool"), + V: func(input []types.Value) (types.Value, error) { + // TODO: should we do an epsilon check? + return &types.BoolValue{ + V: input[0].Float() <= input[1].Float(), + }, nil + }, + }) + // greater-than-equal + RegisterOperator(">=", &types.FuncValue{ + T: types.NewType("func(a int, b int) bool"), + V: func(input []types.Value) (types.Value, error) { + return &types.BoolValue{ + V: input[0].Int() >= input[1].Int(), + }, nil + }, + }) + // floating-point greater-than-equal + RegisterOperator(">=", &types.FuncValue{ + T: types.NewType("func(a float, b float) bool"), + V: func(input []types.Value) (types.Value, error) { + // TODO: should we do an epsilon check? + return &types.BoolValue{ + V: input[0].Float() >= input[1].Float(), + }, nil + }, + }) + + // logical and + // TODO: is there a way for the engine to have + // short-circuit operators, and does it matter? + RegisterOperator("&&", &types.FuncValue{ + T: types.NewType("func(a bool, b bool) bool"), + V: func(input []types.Value) (types.Value, error) { + return &types.BoolValue{ + V: input[0].Bool() && input[1].Bool(), + }, nil + }, + }) + // logical or + RegisterOperator("||", &types.FuncValue{ + T: types.NewType("func(a bool, b bool) bool"), + V: func(input []types.Value) (types.Value, error) { + return &types.BoolValue{ + V: input[0].Bool() || input[1].Bool(), + }, nil + }, + }) + + // logical not (unary operator) + RegisterOperator("!", &types.FuncValue{ + T: types.NewType("func(a bool) bool"), + V: func(input []types.Value) (types.Value, error) { + return &types.BoolValue{ + V: !input[0].Bool(), + }, nil + }, + }) + + // pi operator (this is an easter egg to demo a zero arg operator) + RegisterOperator("π", &types.FuncValue{ + T: types.NewType("func() float"), + V: func(input []types.Value) (types.Value, error) { + return &types.FloatValue{ + V: math.Pi, + }, nil + }, + }) + + Register(OperatorFuncName, func() interfaces.Func { return &OperatorPolyFunc{} }) // must register the func and name +} + +// OperatorFuncs maps an operator to a list of callable function values. +var OperatorFuncs = make(map[string][]*types.FuncValue) // must initialize + +// RegisterOperator registers the given string operator and function value +// implementation with the mini-database for this generalized, static, +// polymorphic operator implementation. +func RegisterOperator(operator string, fn *types.FuncValue) { + if _, exists := OperatorFuncs[operator]; !exists { + OperatorFuncs[operator] = []*types.FuncValue{} // init + } + + for _, f := range OperatorFuncs[operator] { + if err := f.T.Cmp(fn.T); err == nil { + panic(fmt.Sprintf("operator %s already has an implementation for %+v", operator, f.T)) + } + } + + for i, x := range fn.T.Ord { + if x == operatorArgName { + panic(fmt.Sprintf("can't use `%s` as an argName for operator `%s` with type `%+v`", x, operator, fn.T)) + } + // yes this limits the arg max to 24 (`x`) including operator + if s := util.NumToAlpha(i); x != s { + panic(fmt.Sprintf("arg for operator `%s` (index `%d`) should be named `%s`, not `%s`", operator, i, s, x)) + } + } + + OperatorFuncs[operator] = append(OperatorFuncs[operator], fn) +} + +// LookupOperator returns a list of type strings for each operator. An empty +// operator string means return everything. If you specify a size that is less +// than zero, we don't filter by arg length, otherwise we only return signatures +// which have an arg length equal to size. +func LookupOperator(operator string, size int) ([]*types.Type, error) { + fns, exists := OperatorFuncs[operator] + if !exists && operator != "" { + return nil, fmt.Errorf("operator not found") + } + results := []*types.Type{} + + if operator == "" { + // FIXME: loop this in sorted order... + for _, a := range OperatorFuncs { + fns = append(fns, a...) + } + } + + for _, fn := range fns { + typ := addOperatorArg(fn.T) // add in the `operatorArgName` arg + typ = unlabelOperatorArgNames(typ) // label in standard a..b..c + + if size >= 0 && len(typ.Ord) != size { + continue + } + results = append(results, typ) + } + + return results, nil +} + +// OperatorPolyFunc is an operator function that performs an operation on N +// values. +type OperatorPolyFunc struct { + Type *types.Type // Kind == Function, including operator arg + + init *interfaces.Init + last types.Value // last value received to use for diff + + result types.Value // last calculated output + + closeChan chan struct{} +} + +// argNames returns the maximum list of possible argNames. This can be truncated +// if needed. The first arg name is the operator. +func (obj *OperatorPolyFunc) argNames() []string { + // we could just do this statically, but i did it dynamically so that I + // wouldn't ever have to remember to update this list... + max := 0 + for _, fns := range OperatorFuncs { + for _, fn := range fns { + l := len(fn.T.Ord) + if l > max { + max = l + } + } + } + //if length >= 0 && length < max { + // max = length + //} + + args := []string{operatorArgName} + for i := 0; i < max; i++ { + s := util.NumToAlpha(i) + if s == operatorArgName { + panic(fmt.Sprintf("can't use `%s` as arg name", operatorArgName)) + } + args = append(args, s) + } + + return args +} + +// findFunc tries to find the first available registered operator function that +// matches the Operator/Type pattern requested. If none is found it returns nil. +func (obj *OperatorPolyFunc) findFunc(operator string) *types.FuncValue { + fns, exists := OperatorFuncs[operator] + if !exists { + return nil + } + typ := removeOperatorArg(obj.Type) // remove operator so we can match... + for _, fn := range fns { + if err := fn.Type().Cmp(typ); err == nil { // found one! + return fn + } + } + return nil +} + +// Polymorphisms returns the list of possible function signatures available for +// this static polymorphic function. It relies on type and value hints to limit +// the number of returned possibilities. +func (obj *OperatorPolyFunc) Polymorphisms(partialType *types.Type, partialValues []types.Value) ([]*types.Type, error) { + var op string + var size = -1 + + // optimization: if operator happens to already be known statically, + // then we can return a much smaller subset of possible signatures... + if partialType != nil && partialType.Ord != nil { + ord := partialType.Ord + if len(ord) == 0 { + return nil, fmt.Errorf("must have at least one arg in operator func") + } + // optimization: since we know arg length, we can limit the + // signatures that we return... + size = len(ord) // we know size! + if partialType.Map != nil { + if t, exists := partialType.Map[ord[0]]; exists && t != nil { + if t.Cmp(types.TypeStr) != nil { + return nil, fmt.Errorf("first arg for operator func must be an str") + } + if len(partialValues) > 0 && partialValues[0] != nil { + op = partialValues[0].Str() // known str + } + } + } + } + + // since built-in functions have their functions explicitly defined, we + // can add easy invariants between in/out args and their expected types. + results, err := LookupOperator(op, size) + if err != nil { + return nil, errwrap.Wrapf(err, "error findings signatures for operator `%s`", op) + } + + // TODO: we can add additional results filtering here if we'd like... + + if len(results) == 0 { + return nil, fmt.Errorf("no matching signatures for operator `%s` could be found", op) + } + + return results, nil +} + +// Build is run to turn the polymorphic, undeterminted function, into the +// specific statically type version. It is usually run after Unify completes, +// and must be run before Info() and any of the other Func interface methods are +// used. This function is idempotent, as long as the arg isn't changed between +// runs. It typically re-labels the input arg names to match what is actually +// used. +func (obj *OperatorPolyFunc) Build(typ *types.Type) error { + // typ is the KindFunc signature we're trying to build... + + if len(typ.Ord) < 1 { + return fmt.Errorf("the operator function needs at least 1 arg") + } + if typ.Out == nil { + return fmt.Errorf("return type of function must be specified") + } + + t, err := obj.relabelOperatorArgNames(typ) + if err != nil { + return fmt.Errorf("could not build function from type: %+v", typ) + } + obj.Type = t // func type + return nil +} + +// Validate tells us if the input struct takes a valid form. +func (obj *OperatorPolyFunc) Validate() error { + if obj.Type == nil { // build must be run first + return fmt.Errorf("type is still unspecified") + } + if obj.Type.Kind != types.KindFunc { + return fmt.Errorf("type must be a kind of func") + } + return nil +} + +// Info returns some static info about itself. Build must be called before this +// will return correct data. +func (obj *OperatorPolyFunc) Info() *interfaces.Info { + return &interfaces.Info{ + Pure: true, + Memo: false, + Sig: obj.Type, // func kind, which includes operator arg as input + Err: obj.Validate(), + } +} + +// Init runs some startup code for this fact. +func (obj *OperatorPolyFunc) Init(init *interfaces.Init) error { + obj.init = init + obj.closeChan = make(chan struct{}) + return nil +} + +// Stream returns the changing values that this func has over time. +func (obj *OperatorPolyFunc) Stream() error { + var op, lastOp string + var fn *types.FuncValue + defer close(obj.init.Output) // the sender closes + for { + select { + case input, ok := <-obj.init.Input: + if !ok { + return nil // can't output any more + } + //if err := input.Type().Cmp(obj.Info().Sig.Input); err != nil { + // return errwrap.Wrapf(err, "wrong function input") + //} + + if obj.last != nil && input.Cmp(obj.last) == nil { + continue // value didn't change, skip it + } + obj.last = input // store for next + + // build up arg list + args := []types.Value{} + for _, name := range obj.Type.Ord { + v := input.Struct()[name] + if name == operatorArgName { + op = v.Str() + continue // skip over the operator arg + } + args = append(args, v) + } + + if op == "" { + return fmt.Errorf("operator cannot be empty") + } + // operator selection is dynamic now, although mostly it + // should not change... to do so is probably uncommon... + if fn == nil || op != lastOp { + fn = obj.findFunc(op) + } + if fn == nil { + return fmt.Errorf("func not found for operator `%s` with sig: `%+v`", op, obj.Type) + } + lastOp = op + + var result types.Value + result, err := fn.Call(args) // run the function + if err != nil { + return errwrap.Wrapf(err, "problem running function") + } + if result == nil { + return fmt.Errorf("computed function output was nil") + } + + // if previous input was `2 + 4`, but now it + // changed to `1 + 5`, the result is still the + // same, so we can skip sending an update... + if obj.result != nil && result.Cmp(obj.result) == nil { + continue // result didn't change + } + obj.result = result // store new result + + case <-obj.closeChan: + return nil + } + + select { + case obj.init.Output <- obj.result: // send + case <-obj.closeChan: + return nil + } + } +} + +// Close runs some shutdown code for this fact and turns off the stream. +func (obj *OperatorPolyFunc) Close() error { + close(obj.closeChan) + return nil +} + +// relabelOperatorArgNames relabels the input type of kind func with arg names +// that match the expected ones for this operator (which are all standardized). +func (obj *OperatorPolyFunc) relabelOperatorArgNames(typ *types.Type) (*types.Type, error) { + if typ == nil { + return nil, fmt.Errorf("cannot re-label missing type") + } + if typ.Kind != types.KindFunc { + return nil, fmt.Errorf("specified type must be a func kind") + } + + argNames := obj.argNames() // correct arg names... + + if l := len(argNames); len(typ.Ord) > l { + return nil, fmt.Errorf("did not expect more than %d args", l) + } + + m := make(map[string]*types.Type) + ord := []string{} + for pos, x := range typ.Ord { // function args in order + name := argNames[pos] // new arg name + m[name] = typ.Map[x] // n-th type stored with new arg name + ord = append(ord, name) + } + return &types.Type{ + Kind: types.KindFunc, + Map: m, + Ord: ord, + Out: typ.Out, + }, nil +} + +// unlabelOperatorArgNames unlabels the input type of kind func with arg names +// that match the default ones for all functions (which are all standardized). +func unlabelOperatorArgNames(typ *types.Type) *types.Type { + if typ == nil { + return nil + } + + m := make(map[string]*types.Type) + ord := []string{} + for pos, x := range typ.Ord { // function args in order + name := util.NumToAlpha(pos) // default (unspecified) naming + m[name] = typ.Map[x] // n-th type stored with new arg name + ord = append(ord, name) + } + return &types.Type{ + Kind: types.KindFunc, + Map: m, + Ord: ord, + Out: typ.Out, + } +} + +// removeOperatorArg returns a copy of the input KindFunc type, without the +// operator arg which specifies which operator we're using. It *is* idempotent. +func removeOperatorArg(typ *types.Type) *types.Type { + if typ == nil { + return nil + } + if _, exists := typ.Map[operatorArgName]; !exists { + return typ // pass through + } + + m := make(map[string]*types.Type) + ord := []string{} + for _, s := range typ.Ord { + if s == operatorArgName { + continue // remove the operator + } + m[s] = typ.Map[s] + ord = append(ord, s) + } + return &types.Type{ + Kind: types.KindFunc, + Map: m, + Ord: ord, + Out: typ.Out, + } +} + +// addOperatorArg returns a copy of the input KindFunc type, with the operator +// arg which specifies which operator we're using added. This is idempotent. +func addOperatorArg(typ *types.Type) *types.Type { + if typ == nil { + return nil + } + if _, exists := typ.Map[operatorArgName]; exists { + return typ // pass through + } + + m := make(map[string]*types.Type) + m[operatorArgName] = types.TypeStr // add the operator + ord := []string{operatorArgName} // add the operator + for _, s := range typ.Ord { + m[s] = typ.Map[s] + ord = append(ord, s) + } + return &types.Type{ + Kind: types.KindFunc, + Map: m, + Ord: ord, + Out: typ.Out, + } +} diff --git a/lang/funcs/structlookup_polyfunc.go b/lang/funcs/structlookup_polyfunc.go new file mode 100644 index 00000000..112039aa --- /dev/null +++ b/lang/funcs/structlookup_polyfunc.go @@ -0,0 +1,282 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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 funcs + +import ( + "fmt" + + "github.com/purpleidea/mgmt/lang/interfaces" + "github.com/purpleidea/mgmt/lang/types" + + errwrap "github.com/pkg/errors" +) + +const ( + // StructLookupFuncName is the name this function is registered as. This + // starts with an underscore so that it cannot be used from the lexer. + // XXX: change to _structlookup and add syntax in the lexer/parser + StructLookupFuncName = "structlookup" +) + +func init() { + Register(StructLookupFuncName, func() interfaces.Func { return &StructLookupPolyFunc{} }) // must register the func and name +} + +// StructLookupPolyFunc is a key map lookup function. +type StructLookupPolyFunc struct { + Type *types.Type // Kind == Struct, that is used as the struct we lookup + Out *types.Type // type of field we're extracting + + init *interfaces.Init + last types.Value // last value received to use for diff + field string + + result types.Value // last calculated output + + closeChan chan struct{} +} + +// Polymorphisms returns the list of possible function signatures available for +// this static polymorphic function. It relies on type and value hints to limit +// the number of returned possibilities. +func (obj *StructLookupPolyFunc) Polymorphisms(partialType *types.Type, partialValues []types.Value) ([]*types.Type, error) { + // TODO: return `variant` as arg for now -- maybe there's a better way? + variant := []*types.Type{types.NewType("func(struct variant, field str) variant")} + + if partialType == nil { + return variant, nil + } + + var typ *types.Type // struct type of the first argument + var out *types.Type // type of the field + + // TODO: if partialValue[0] exists, check it matches the type we expect + ord := partialType.Ord + if partialType.Map != nil { + if len(ord) != 2 { + return nil, fmt.Errorf("must have exactly two args in structlookup func") + } + if tStruct, exists := partialType.Map[ord[0]]; exists && tStruct != nil { + if tStruct.Kind != types.KindStruct { + return nil, fmt.Errorf("first arg for structlookup must be a struct") + } + if !tStruct.HasVariant() { + typ = tStruct // found + } + } + if tField, exists := partialType.Map[ord[1]]; exists && tField != nil { + if tField.Cmp(types.TypeStr) != nil { + return nil, fmt.Errorf("second arg for structlookup must be a string") + } + } + + if len(partialValues) == 2 && partialValues[1] != nil { + if types.TypeStr.Cmp(partialValues[1].Type()) != nil { + return nil, fmt.Errorf("second value must be an str") + } + structType, exists := partialType.Map[ord[0]] + if !exists { + return nil, fmt.Errorf("missing struct field") + } + if structType != nil { + field := partialValues[1].Str() + fieldType, exists := structType.Map[field] + if !exists { + return nil, fmt.Errorf("field: `%s` does not exist in struct", field) + } + if fieldType != nil { + if partialType.Out != nil && fieldType.Cmp(partialType.Out) != nil { + return nil, fmt.Errorf("field `%s` must have same type as return type", field) + } + + out = fieldType // found! + } + } + } + + if tOut := partialType.Out; tOut != nil { + // TODO: we could check that at least one of the types + // in struct.Map was our type, but not very useful... + } + } + + typFunc := &types.Type{ + Kind: types.KindFunc, // function type + Map: make(map[string]*types.Type), + Ord: []string{"struct", "field"}, + Out: out, + } + typFunc.Map["struct"] = typ + typFunc.Map["field"] = types.TypeStr + + // set variant instead of nil + if typFunc.Map["struct"] == nil { + typFunc.Map["struct"] = types.TypeVariant + } + if out == nil { + typFunc.Out = types.TypeVariant + } + + return []*types.Type{typFunc}, nil +} + +// Build is run to turn the polymorphic, undeterminted function, into the +// specific statically type version. It is usually run after Unify completes, +// and must be run before Info() and any of the other Func interface methods are +// used. This function is idempotent, as long as the arg isn't changed between +// runs. +func (obj *StructLookupPolyFunc) Build(typ *types.Type) error { + // typ is the KindFunc signature we're trying to build... + if typ.Kind != types.KindFunc { + return fmt.Errorf("input type must be of kind func") + } + + if len(typ.Ord) != 2 { + return fmt.Errorf("the structlookup function needs exactly two args") + } + if typ.Out == nil { + return fmt.Errorf("return type of function must be specified") + } + if typ.Map == nil { + return fmt.Errorf("invalid input type") + } + + tStruct, exists := typ.Map[typ.Ord[0]] + if !exists || tStruct == nil { + return fmt.Errorf("first arg must be specified") + } + + tField, exists := typ.Map[typ.Ord[1]] + if !exists || tField == nil { + return fmt.Errorf("second arg must be specified") + } + if err := tField.Cmp(types.TypeStr); err != nil { + return errwrap.Wrapf(err, "field must be an str") + } + + // NOTE: We actually don't know which field this is, only its type! we + // could have cached the discovered field during Polymorphisms(), but it + // turns out it's not actually necessary for us to know it to build the + // struct. + obj.Type = tStruct // struct type + obj.Out = typ.Out // type of return value + return nil +} + +// Validate tells us if the input struct takes a valid form. +func (obj *StructLookupPolyFunc) Validate() error { + if obj.Type == nil { // build must be run first + return fmt.Errorf("type is still unspecified") + } + if obj.Type.Kind != types.KindStruct { + return fmt.Errorf("type must be a kind of struct") + } + if obj.Out == nil { + return fmt.Errorf("return type must be specified") + } + + for _, t := range obj.Type.Map { + if obj.Out.Cmp(t) == nil { + return nil // found at least one match + } + } + return fmt.Errorf("return type is not in the list of available struct fields") +} + +// Info returns some static info about itself. Build must be called before this +// will return correct data. +func (obj *StructLookupPolyFunc) Info() *interfaces.Info { + typ := types.NewType(fmt.Sprintf("func(struct %s, field str) %s", obj.Type.String(), obj.Out.String())) + return &interfaces.Info{ + Pure: true, + Memo: false, + Sig: typ, // func kind + Err: obj.Validate(), + } +} + +// Init runs some startup code for this fact. +func (obj *StructLookupPolyFunc) Init(init *interfaces.Init) error { + obj.init = init + obj.closeChan = make(chan struct{}) + return nil +} + +// Stream returns the changing values that this func has over time. +func (obj *StructLookupPolyFunc) Stream() error { + defer close(obj.init.Output) // the sender closes + for { + select { + case input, ok := <-obj.init.Input: + if !ok { + return nil // can't output any more + } + //if err := input.Type().Cmp(obj.Info().Sig.Input); err != nil { + // return errwrap.Wrapf(err, "wrong function input") + //} + + if obj.last != nil && input.Cmp(obj.last) == nil { + continue // value didn't change, skip it + } + obj.last = input // store for next + + st := (input.Struct()["struct"]).(*types.StructValue) + field := input.Struct()["field"].Str() + + if field == "" { + return fmt.Errorf("received empty field") + } + + result, exists := st.Lookup(field) + if !exists { + return fmt.Errorf("could not lookup field: `%s` in struct", field) + } + + if obj.field == "" { + obj.field = field // store first field + } + + if field != obj.field { + return fmt.Errorf("input field changed from: `%s`, to: `%s`", obj.field, field) + } + + // if previous input was `2 + 4`, but now it + // changed to `1 + 5`, the result is still the + // same, so we can skip sending an update... + if obj.result != nil && result.Cmp(obj.result) == nil { + continue // result didn't change + } + obj.result = result // store new result + + case <-obj.closeChan: + return nil + } + + select { + case obj.init.Output <- obj.result: // send + case <-obj.closeChan: + return nil + } + } +} + +// Close runs some shutdown code for this fact and turns off the stream. +func (obj *StructLookupPolyFunc) Close() error { + close(obj.closeChan) + return nil +} diff --git a/lang/funcs/structs/composite.go b/lang/funcs/structs/composite.go new file mode 100644 index 00000000..ad9ab5b1 --- /dev/null +++ b/lang/funcs/structs/composite.go @@ -0,0 +1,203 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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 structs + +import ( + "fmt" + + "github.com/purpleidea/mgmt/lang/interfaces" + "github.com/purpleidea/mgmt/lang/types" + + errwrap "github.com/pkg/errors" +) + +// CompositeFunc is a function that passes through the value it receives. It is +// used to take a series of inputs to a list, map or struct, and return that +// value as a stream that depends on those inputs. It helps the list, map, and +// struct's that fulfill the Expr interface but expressing a Func method. +type CompositeFunc struct { + Type *types.Type // this is the type of the composite value we hold + Len int // length of list or map (if used) + + init *interfaces.Init + last types.Value // last value received to use for diff + result types.Value // last calculated output + + closeChan chan struct{} +} + +// Validate makes sure we've built our struct properly. +func (obj *CompositeFunc) Validate() error { + if obj.Type == nil { + return fmt.Errorf("must specify a type") + } + switch obj.Type.Kind { + case types.KindList: + fallthrough + case types.KindMap: + fallthrough + case types.KindStruct: + return nil + } + + return fmt.Errorf("can't compose type `%s`", obj.Type.String()) +} + +// Info returns some static info about itself. +func (obj *CompositeFunc) Info() *interfaces.Info { + typ := &types.Type{ + Kind: types.KindFunc, // function type + Map: make(map[string]*types.Type), + Ord: []string{}, + Out: obj.Type, // this is the output type for the expression + } + + switch obj.Type.Kind { + case types.KindList: // wrapped in a struct with `length` many keys + for i := 0; i < obj.Len; i++ { + // FIXME: should we .Title the fields or add a prefix? + key := fmt.Sprintf("%d", i) + typ.Map[key] = obj.Type.Val // type of each list element + typ.Ord = append(typ.Ord, key) + } + + case types.KindMap: // wrapped in a struct with named keys + for i := 0; i < obj.Len; i++ { + // each key and val has a value to pass in, and we have + // a known number of kv pairs, so we pass each in with + // the index of the kv pair as found in the parse order + key1 := fmt.Sprintf("key:%d", i) + typ.Map[key1] = obj.Type.Key // type of each map key + typ.Ord = append(typ.Ord, key1) + + key2 := fmt.Sprintf("val:%d", i) + typ.Map[key2] = obj.Type.Val // type of each map val + typ.Ord = append(typ.Ord, key2) + } + + case types.KindStruct: + // map it directly, each key is the right input! + typ.Map = obj.Type.Map + typ.Ord = obj.Type.Ord + } + + return &interfaces.Info{ + Pure: true, + Memo: false, // TODO: ??? + Sig: typ, + Err: obj.Validate(), + } +} + +// Init runs some startup code for this composite function. +func (obj *CompositeFunc) Init(init *interfaces.Init) error { + obj.init = init + obj.closeChan = make(chan struct{}) + return nil +} + +// Stream takes an input struct in the format as described in the Func and Graph +// methods of the Expr, and returns the actual expected value as a stream based +// on the changing inputs to that value. +func (obj *CompositeFunc) Stream() error { + defer close(obj.init.Output) // the sender closes + for { + select { + case input, ok := <-obj.init.Input: + if !ok { + return nil // can't output any more + } + //if err := input.Type().Cmp(obj.Info().Sig.Input); err != nil { + // return errwrap.Wrapf(err, "wrong function input") + //} + if obj.last != nil && input.Cmp(obj.last) == nil { + continue // value didn't change, skip it + } + obj.last = input // store for next + + var result types.Value + switch obj.Type.Kind { + case types.KindList: + // XXX: this duplicates the same logic that exists in Value() as implemented on *ExprList + // XXX: have this call that function to get the result? + result = obj.Type.New() // new list + input := input.(*types.StructValue) // must be! + for i := 0; i < obj.Len; i++ { // build it + value, exists := input.Lookup(fmt.Sprintf("%d", i)) // argNames as integers! + if !exists { + return fmt.Errorf("missing input index `%d`", i) + } + if err := result.(*types.ListValue).Add(value); err != nil { + return errwrap.Wrapf(err, "can't build list index `%d`", i) + } + } + + case types.KindMap: + result = obj.Type.New() // new map + input := (input.(*types.StructValue)).Struct() // must be! + l := len(input) + if l%2 != 0 { + return fmt.Errorf("expected even number of inputs for a map, got: %d", l) + } + + // each key should be named `key:0`, `val:0`, `key:1`, `val:1`, + // and so on for as many key pairs as we have... remember that + // the number of keys pairs is known statically in this case! + for i := 0; i < l/2; i++ { // build it + key, exists := input[fmt.Sprintf("key:%d", i)] + if !exists { + return fmt.Errorf("missing input key `key:%d`", i) + } + val, exists := input[fmt.Sprintf("val:%d", i)] + if !exists { + return fmt.Errorf("missing input val `val:%d`", i) + } + + if err := result.(*types.MapValue).Add(key, val); err != nil { + return errwrap.Wrapf(err, "can't build map key with index `%d`", i) + } + } + + case types.KindStruct: + result = input + } + + // skip sending an update... + if obj.result != nil && result.Cmp(obj.result) == nil { + continue // result didn't change + } + obj.result = result // store new result + + case <-obj.closeChan: + return nil + } + + select { + case obj.init.Output <- obj.result: // send + // pass + case <-obj.closeChan: + return nil + } + } +} + +// Close runs some shutdown code for this function and turns off the stream. +func (obj *CompositeFunc) Close() error { + close(obj.closeChan) + return nil +} diff --git a/lang/funcs/structs/const.go b/lang/funcs/structs/const.go new file mode 100644 index 00000000..a5e096c7 --- /dev/null +++ b/lang/funcs/structs/const.go @@ -0,0 +1,76 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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 structs + +import ( + "fmt" + + "github.com/purpleidea/mgmt/lang/interfaces" + "github.com/purpleidea/mgmt/lang/types" +) + +// ConstFunc is a function that returns the constant value passed to Value. +type ConstFunc struct { + Value types.Value + + init *interfaces.Init + closeChan chan struct{} +} + +// Validate makes sure we've built our struct properly. +func (obj *ConstFunc) Validate() error { + if obj.Value == nil { + return fmt.Errorf("must specify `Value` input") + } + return nil +} + +// Info returns some static info about itself. +func (obj *ConstFunc) Info() *interfaces.Info { + return &interfaces.Info{ + Pure: true, + Memo: false, // TODO: ??? + Sig: types.NewType(fmt.Sprintf("func() %s", obj.Value.Type().String())), + Err: obj.Validate(), // XXX: implement this and check .Err in engine! + } +} + +// Init runs some startup code for this const. +func (obj *ConstFunc) Init(init *interfaces.Init) error { + obj.init = init + obj.closeChan = make(chan struct{}) + return nil +} + +// Stream returns the single value that this const has, and then closes. +func (obj *ConstFunc) Stream() error { + select { + case obj.init.Output <- obj.Value: + // pass + case <-obj.closeChan: + return nil + } + close(obj.init.Output) // signal that we're done sending + return nil +} + +// Close runs some shutdown code for this const and turns off the stream. +func (obj *ConstFunc) Close() error { + close(obj.closeChan) + return nil +} diff --git a/lang/funcs/structs/if.go b/lang/funcs/structs/if.go new file mode 100644 index 00000000..153568f1 --- /dev/null +++ b/lang/funcs/structs/if.go @@ -0,0 +1,125 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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 structs + +import ( + "fmt" + + "github.com/purpleidea/mgmt/lang/interfaces" + "github.com/purpleidea/mgmt/lang/types" +) + +// IfFunc is a function that passes through the value of the correct branch +// based on the conditional value it gets. +type IfFunc struct { + Type *types.Type // this is the type of the if expression output we hold + + init *interfaces.Init + last types.Value // last value received to use for diff + result types.Value // last calculated output + + closeChan chan struct{} +} + +// Validate tells us if the input struct takes a valid form. +func (obj *IfFunc) Validate() error { + if obj.Type == nil { + return fmt.Errorf("must specify a type") + } + return nil +} + +// Info returns some static info about itself. +func (obj *IfFunc) Info() *interfaces.Info { + typ := &types.Type{ + Kind: types.KindFunc, // function type + Map: map[string]*types.Type{ + "c": types.TypeBool, // conditional must be a boolean + "a": obj.Type, // true branch must be this type + "b": obj.Type, // false branch must be this type too + }, + Ord: []string{"c", "a", "b"}, // conditional, and two branches + Out: obj.Type, // result type must match + } + + return &interfaces.Info{ + Pure: true, + Memo: false, // TODO: ??? + Sig: typ, + Err: obj.Validate(), + } +} + +// Init runs some startup code for this if expression function. +func (obj *IfFunc) Init(init *interfaces.Init) error { + obj.init = init + obj.closeChan = make(chan struct{}) + return nil +} + +// Stream takes an input struct in the format as described in the Func and Graph +// methods of the Expr, and returns the actual expected value as a stream based +// on the changing inputs to that value. +func (obj *IfFunc) Stream() error { + defer close(obj.init.Output) // the sender closes + for { + select { + case input, ok := <-obj.init.Input: + if !ok { + return nil // can't output any more + } + //if err := input.Type().Cmp(obj.Info().Sig.Input); err != nil { + // return errwrap.Wrapf(err, "wrong function input") + //} + if obj.last != nil && input.Cmp(obj.last) == nil { + continue // value didn't change, skip it + } + obj.last = input // store for next + + var result types.Value + + if input.Struct()["c"].Bool() { + result = input.Struct()["a"] // true branch + } else { + result = input.Struct()["b"] // false branch + } + + // skip sending an update... + if obj.result != nil && result.Cmp(obj.result) == nil { + continue // result didn't change + } + obj.result = result // store new result + + case <-obj.closeChan: + return nil + } + + select { + case obj.init.Output <- obj.result: // send + // pass + case <-obj.closeChan: + return nil + } + } +} + +// Close runs some shutdown code for this function and turns off the stream. +func (obj *IfFunc) Close() error { + close(obj.closeChan) + return nil +} diff --git a/lang/funcs/structs/var.go b/lang/funcs/structs/var.go new file mode 100644 index 00000000..a3995892 --- /dev/null +++ b/lang/funcs/structs/var.go @@ -0,0 +1,141 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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 structs + +import ( + "fmt" + + "github.com/purpleidea/mgmt/lang/interfaces" + "github.com/purpleidea/mgmt/lang/types" + + errwrap "github.com/pkg/errors" +) + +// VarFunc is a function that passes through a function that came from a bind +// lookup. It exists so that the reactive function engine type checks correctly. +type VarFunc struct { + Type *types.Type // this is the type of the var's value that we hold + Func interfaces.Func + Edge string // name of the edge used + + init *interfaces.Init + last types.Value // last value received to use for diff + result types.Value // last calculated output + + closeChan chan struct{} +} + +// Validate makes sure we've built our struct properly. +func (obj *VarFunc) Validate() error { + if obj.Type == nil { + return fmt.Errorf("must specify a type") + } + if obj.Func == nil { + return fmt.Errorf("must specify a func") + } + if obj.Edge == "" { + return fmt.Errorf("must specify an edge name") + } + + // we're supposed to call Validate() before we ever call Info() + if err := obj.Func.Validate(); err != nil { + return errwrap.Wrapf(err, "func did not validate") + } + + typ := obj.Func.Info().Sig + if err := obj.Type.Cmp(typ.Out); err != nil { + return errwrap.Wrapf(err, "expr type must match func out type") + } + return nil +} + +// Info returns some static info about itself. +func (obj *VarFunc) Info() *interfaces.Info { + typ := &types.Type{ + Kind: types.KindFunc, // function type + Map: map[string]*types.Type{obj.Edge: obj.Type}, + Ord: []string{obj.Edge}, + Out: obj.Type, // this is the output type for the expression + } + + return &interfaces.Info{ + Pure: true, + Memo: false, // TODO: ??? + Sig: typ, + Err: obj.Validate(), + } +} + +// Init runs some startup code for this composite function. +func (obj *VarFunc) Init(init *interfaces.Init) error { + obj.init = init + obj.closeChan = make(chan struct{}) + return nil +} + +// Stream takes an input struct in the format as described in the Func and Graph +// methods of the Expr, and returns the actual expected value as a stream based +// on the changing inputs to that value. +func (obj *VarFunc) Stream() error { + defer close(obj.init.Output) // the sender closes + for { + select { + case input, ok := <-obj.init.Input: + if !ok { + return nil // can't output any more + } + //if err := input.Type().Cmp(obj.Info().Sig.Input); err != nil { + // return errwrap.Wrapf(err, "wrong function input") + //} + if obj.last != nil && input.Cmp(obj.last) == nil { + continue // value didn't change, skip it + } + obj.last = input // store for next + + var result types.Value + st := input.(*types.StructValue) // must be! + value, exists := st.Lookup(obj.Edge) + if !exists { + return fmt.Errorf("missing expected input argument `%s`", obj.Edge) + } + result = value + + // skip sending an update... + if obj.result != nil && result.Cmp(obj.result) == nil { + continue // result didn't change + } + obj.result = result // store new result + + case <-obj.closeChan: + return nil + } + + select { + case obj.init.Output <- obj.result: // send + // pass + case <-obj.closeChan: + return nil + } + } +} + +// Close runs some shutdown code for this function and turns off the stream. +func (obj *VarFunc) Close() error { + close(obj.closeChan) + return nil +} diff --git a/lang/gapi.go b/lang/gapi.go new file mode 100644 index 00000000..3487957a --- /dev/null +++ b/lang/gapi.go @@ -0,0 +1,271 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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 lang + +import ( + "fmt" + "log" + "strings" + "sync" + + "github.com/purpleidea/mgmt/gapi" + "github.com/purpleidea/mgmt/pgraph" + "github.com/purpleidea/mgmt/recwatch" + "github.com/purpleidea/mgmt/resources" + + multierr "github.com/hashicorp/go-multierror" + errwrap "github.com/pkg/errors" + "github.com/urfave/cli" +) + +const ( + // Name is the name of this frontend. + Name = "lang" + // Start is the entry point filename that we use. It is arbitrary. + Start = "/start." + FileNameExtension // FIXME: replace with a proper code entry point schema (directory schema) +) + +func init() { + gapi.Register(Name, func() gapi.GAPI { return &GAPI{} }) // register +} + +// GAPI implements the main lang GAPI interface. +type GAPI struct { + InputURI string // input URI of code file system to run + + lang *Lang // lang struct + + data gapi.Data + initialized bool + closeChan chan struct{} + wg *sync.WaitGroup // sync group for tunnel go routines + configWatcher *recwatch.ConfigWatcher +} + +// Cli takes a cli.Context, and returns our GAPI if activated. All arguments +// should take the prefix of the registered name. On activation, if there are +// any validation problems, you should return an error. If this was not +// activated, then you should return a nil GAPI and a nil error. +func (obj *GAPI) Cli(c *cli.Context, fs resources.Fs) (*gapi.Deploy, error) { + if s := c.String(Name); c.IsSet(Name) { + if s == "" { + return nil, fmt.Errorf("input code is empty") + } + + // read through this local path, and store it in our file system + // since our deploy should work anywhere in the cluster, let the + // engine ensure that this file system is replicated everywhere! + + // TODO: single file input for now + if err := gapi.CopyFileToFs(fs, s, Start); err != nil { + return nil, errwrap.Wrapf(err, "can't copy code from `%s` to `%s`", s, Start) + } + + return &gapi.Deploy{ + Name: Name, + Noop: c.GlobalBool("noop"), + Sema: c.GlobalInt("sema"), + GAPI: &GAPI{ + InputURI: fs.URI(), + // TODO: add properties here... + }, + }, nil + } + return nil, nil // we weren't activated! +} + +// CliFlags returns a list of flags used by this deploy subcommand. +func (obj *GAPI) CliFlags() []cli.Flag { + return []cli.Flag{ + cli.StringFlag{ + Name: fmt.Sprintf("%s, %s", Name, Name[0:1]), + Value: "", + Usage: "language code path to deploy", + }, + } +} + +// Init initializes the lang GAPI struct. +func (obj *GAPI) Init(data gapi.Data) error { + if obj.initialized { + return fmt.Errorf("already initialized") + } + if obj.InputURI == "" { + return fmt.Errorf("the InputURI param must be specified") + } + obj.data = data // store for later + obj.closeChan = make(chan struct{}) + obj.wg = &sync.WaitGroup{} + obj.initialized = true + obj.configWatcher = recwatch.NewConfigWatcher() + return nil +} + +// LangInit is a wrapper around the lang Init method. +func (obj *GAPI) LangInit() error { + if obj.lang != nil { + return nil // already ran init, close first! + } + + fs, err := obj.data.World.Fs(obj.InputURI) // open the remote file system + if err != nil { + return errwrap.Wrapf(err, "can't load code from file system `%s`", obj.InputURI) + } + + b, err := fs.ReadFile(Start) // read the single file out of it + if err != nil { + return errwrap.Wrapf(err, "can't read code from file `%s`", Start) + } + + code := strings.NewReader(string(b)) + obj.lang = &Lang{ + Input: code, // string as an interface that satisfies io.Reader + Hostname: obj.data.Hostname, + World: obj.data.World, + Debug: obj.data.Debug, + } + if err := obj.lang.Init(); err != nil { + return errwrap.Wrapf(err, "can't init the lang") + } + return nil +} + +// LangClose is a wrapper around the lang Close method. +func (obj *GAPI) LangClose() error { + if obj.lang != nil { + err := obj.lang.Close() + obj.lang = nil // clear it to avoid double closing + return errwrap.Wrapf(err, "can't close the lang") // nil passthrough + } + return nil +} + +// Graph returns a current Graph. +func (obj *GAPI) Graph() (*pgraph.Graph, error) { + if !obj.initialized { + return nil, fmt.Errorf("%s: GAPI is not initialized", Name) + } + + g, err := obj.lang.Interpret() + if err != nil { + return nil, errwrap.Wrapf(err, "%s: interpret error", Name) + } + + return g, nil +} + +// Next returns nil errors every time there could be a new graph. +func (obj *GAPI) Next() chan gapi.Next { + ch := make(chan gapi.Next) + obj.wg.Add(1) + go func() { + defer obj.wg.Done() + defer close(ch) // this will run before the obj.wg.Done() + if !obj.initialized { + next := gapi.Next{ + Err: fmt.Errorf("%s: GAPI is not initialized", Name), + Exit: true, // exit, b/c programming error? + } + ch <- next + return + } + startChan := make(chan struct{}) // start signal + close(startChan) // kick it off! + + streamChan := make(chan error) + //defer obj.LangClose() // close any old lang + + var ok bool + for { + var err error + var langSwap bool // do we need to swap the lang object? + select { + // TODO: this should happen in ConfigWatch instead :) + case <-startChan: // kick the loop once at start + startChan = nil // disable + err = nil // set nil as the message to send + langSwap = true + + case err, ok = <-streamChan: // a variable changed + if !ok { // the channel closed! + return + } + + case <-obj.closeChan: + return + } + log.Printf("%s: Generating new graph...", Name) + + // skip this to pass through the err if present + if langSwap && err == nil { + log.Printf("%s: Swap!", Name) + // run up to these three but fail on err + if e := obj.LangClose(); e != nil { // close any old lang + err = e // pass through the err + } else if e := obj.LangInit(); e != nil { // init the new one! + err = e // pass through the err + + // Always run LangClose after LangInit + // when done. This is currently needed + // because we should tell the lang obj + // to shut down all the running facts. + if e := obj.LangClose(); e != nil { + err = multierr.Append(err, e) // list of errors + } + } else { + + if obj.data.NoStreamWatch { // TODO: do we want to allow this for the lang? + streamChan = nil + } else { + // stream for lang events + streamChan = obj.lang.Stream() // update stream + } + continue // wait for stream to trigger + } + } + + next := gapi.Next{ + Exit: err != nil, // TODO: do we want to shutdown? + Err: err, + } + select { + case ch <- next: // trigger a run (send a msg) + if err != nil { + return + } + // unblock if we exit while waiting to send! + case <-obj.closeChan: + return + } + } + }() + return ch +} + +// Close shuts down the lang GAPI. +func (obj *GAPI) Close() error { + if !obj.initialized { + return fmt.Errorf("%s: GAPI is not initialized", Name) + } + obj.configWatcher.Close() + obj.LangClose() // close lang, esp. if blocked in Stream() wait + close(obj.closeChan) + obj.wg.Wait() + obj.initialized = false // closed = true + return nil +} diff --git a/lang/interfaces/ast.go b/lang/interfaces/ast.go new file mode 100644 index 00000000..9862cd78 --- /dev/null +++ b/lang/interfaces/ast.go @@ -0,0 +1,126 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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" + "github.com/purpleidea/mgmt/pgraph" + "github.com/purpleidea/mgmt/resources" +) + +const ( + // Debug enables debugging for some commonly used debug information. + Debug = false +) + +// Stmt represents a statement node in the language. A stmt could be a resource, +// a `bind` statement, or even an `if` statement. (Different from an `if` +// expression.) +type Stmt interface { + Interpolate() (Stmt, error) // return expanded form of AST as a new AST + SetScope(*Scope) error // set the scope here and propagate it downwards + Unify() ([]Invariant, error) // TODO: is this named correctly? + Graph() (*pgraph.Graph, error) + Output() (*Output, error) +} + +// Expr represents an expression in the language. Expr implementations must have +// their method receivers implemented as pointer receivers so that they can be +// easily copied and moved around. Expr also implements pgraph.Vertex so that +// these can be stored as pointers in our graph data structure. +type Expr interface { + pgraph.Vertex // must implement this since we store these in our graphs + Interpolate() (Expr, error) // return expanded form of AST as a new AST + SetScope(*Scope) error // set the scope here and propagate it downwards + SetType(*types.Type) error // sets the type definitively, errors if incompatible + Type() (*types.Type, error) + Unify() ([]Invariant, error) // TODO: is this named correctly? + Graph() (*pgraph.Graph, error) + Func() (Func, error) // a function that represents this reactively + SetValue(types.Value) error + Value() (types.Value, error) +} + +// Scope represents a mapping between a variables identifier and the +// corresponding expression it is bound to. Local scopes in this language exist +// and are formed by nesting within if statements. Child scopes can shadow +// variables in parent scopes, which is another way of saying they can redefine +// previously used variables as long as the new binding happens within a child +// scope. This is useful so that someone in the top scope can't prevent a child +// module from ever using that variable name again. It might be worth revisiting +// this point in the future if we find it adds even greater code safety. Please +// report any bugs you have written that would have been prevented by this. +type Scope struct { + Variables map[string]Expr + //Functions map[string]??? // TODO: do we want a separate namespace for user defined functions? +} + +// Empty returns the zero, empty value for the scope, with all the internal +// lists initialized appropriately. +func (obj *Scope) Empty() *Scope { + return &Scope{ + Variables: make(map[string]Expr), + //Functions: ???, + } +} + +// Copy makes a copy of the Scope struct. This ensures that if the internal map +// is changed, it doesn't affect other copies of the Scope. It does *not* copy +// or change the Expr pointers contained within, since these are references, and +// we need those to be consistently pointing to the same things after copying. +func (obj *Scope) Copy() *Scope { + variables := make(map[string]Expr) + if obj != nil { // allow copying nil scopes + for k, v := range obj.Variables { // copy + variables[k] = v // we don't copy the expr's! + } + } + return &Scope{ + Variables: variables, + } +} + +// Edge is the data structure representing a compiled edge that is used in the +// lang to express a dependency between two resources and optionally send/recv. +type Edge struct { + Kind1 string // kind of resource + Name1 string // name of resource + Send string // name of field used for send/recv (optional) + + Kind2 string // kind of resource + Name2 string // name of resource + Recv string // name of field used for send/recv (optional) + + Notify bool // is there a notification being sent? +} + +// Output is a collection of data returned by a Stmt. +type Output struct { // returned by Stmt + Resources []resources.Res + Edges []*Edge + //Exported []*Exports // TODO: add exported resources +} + +// Empty returns the zero, empty value for the output, with all the internal +// lists initialized appropriately. +func (obj *Output) Empty() *Output { + return &Output{ + Resources: []resources.Res{}, + Edges: []*Edge{}, + } +} diff --git a/lang/interfaces/error.go b/lang/interfaces/error.go new file mode 100644 index 00000000..e9dff0f6 --- /dev/null +++ b/lang/interfaces/error.go @@ -0,0 +1,30 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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 + +// Error is a constant error type that implements error. +type Error string + +// Error fulfills the error interface of this type. +func (e Error) Error() string { return string(e) } + +const ( + // ErrTypeCurrentlyUnknown is returned from the Type() call on Expr if + // unification didn't run successfully and the type isn't obvious yet. + ErrTypeCurrentlyUnknown = Error("type is currently unknown") +) diff --git a/lang/interfaces/func.go b/lang/interfaces/func.go new file mode 100644 index 00000000..b48feeed --- /dev/null +++ b/lang/interfaces/func.go @@ -0,0 +1,90 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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" + "github.com/purpleidea/mgmt/resources" +) + +// Info is a static representation of some information about the function. It is +// used for static analysis and type checking. If you break this contract, you +// might cause a panic. +type Info struct { + Pure bool // is the function pure? (can it be memoized?) + Memo bool // should the function be memoized? (false if too much output) + Sig *types.Type // the signature of the function, must be KindFunc + Err error // is this a valid function, or was it created improperly? +} + +// Init is the structure of values and references which is passed into all +// functions on initialization. +type Init struct { + Hostname string // uuid for the host + //Noop bool + Input chan types.Value // Engine will close `input` chan + Output chan types.Value // Stream must close `output` chan + World resources.World + Debug bool + Logf func(format string, v ...interface{}) +} + +// Func is the interface that any valid func must fulfill. It is very simple, +// but still event driven. Funcs should attempt to only send values when they +// have changed. +// TODO: should we support a static version of this interface for funcs that +// never change to avoid the overhead of the goroutine and channel listener? +type Func interface { + Validate() error // FIXME: this is only needed for PolyFunc. Get it moved and used! + Info() *Info + Init(*Init) error + Stream() error + Close() error +} + +// PolyFunc is an interface for functions which are statically polymorphic. In +// other words, they are functions which before compile time are polymorphic, +// but after a successful compilation have a fixed static signature. This makes +// implementing what would appear to be generic or polymorphic instead something +// that is actually static and that still has the language safety properties. +type PolyFunc interface { + Func // implement everything in Func but add the additional requirements + + // Polymorphisms returns a list of possible function type signatures. It + // takes as input a list of partial "hints" as to limit the number of + // possible results it returns. These partial hints take the form of a + // function type signature (with as many types in it specified and the + // rest set to nil) and any known static values for the input args. If + // the partial type is not nil, then the Ord parameter must be of the + // correct arg length. If any types are specified, then the array must + // be of that length as well, with the known ones filled in. Some + // static polymorphic functions require a minimal amount of hinting or + // they will be unable to return any possible result that is not + // infinite in length. If you expect to need to return an infinite (or + // very large) amount of results, then you should return an error + // instead. The arg names in your returned func type signatures should + // be in the standardized "a..b..c" format. Use util.NumToAlpha if you + // want to convert easily. + Polymorphisms(*types.Type, []types.Value) ([]*types.Type, error) + + // Build takes the known type signature for this function and finalizes + // this structure so that it is now determined, and ready to function as + // a normal function would. (The normal methods in the Func interface + // are all that should be needed or used after this point.) + Build(*types.Type) error // then, you can get argNames from Info() +} diff --git a/lang/interfaces/unification.go b/lang/interfaces/unification.go new file mode 100644 index 00000000..0e3cceb5 --- /dev/null +++ b/lang/interfaces/unification.go @@ -0,0 +1,30 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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 ( + "fmt" +) + +// Invariant represents a constraint that is described by the Expr's and Stmt's, +// and which is passed into the unification solver to describe what is known +// by the AST. +type Invariant interface { + // TODO: should we add any other methods to this type? + fmt.Stringer +} diff --git a/lang/interpolate.go b/lang/interpolate.go new file mode 100644 index 00000000..494dd444 --- /dev/null +++ b/lang/interpolate.go @@ -0,0 +1,238 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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 lang // TODO: move this into a sub package of lang/$name? + +import ( + "fmt" + "log" + + "github.com/purpleidea/mgmt/lang/interfaces" + + "github.com/hashicorp/hil" + hilast "github.com/hashicorp/hil/ast" + errwrap "github.com/pkg/errors" +) + +// Pos represents a position in the code. +// TODO: consider expanding with range characteristics. +type Pos struct { + Line int // line number starting at 1 + Column int // column number starting at 1 + Filename string // optional source filename, if known +} + +// InterpolateStr interpolates a string and returns the representative AST. This +// particular implementation uses the hashicorp hil library and syntax to do so. +func InterpolateStr(str string, pos *Pos) (interfaces.Expr, error) { + if interfaces.Debug { + log.Printf("%s: interpolate: %s", Name, str) + } + var line, column int = -1, -1 + var filename string + if pos != nil { + line = pos.Line + column = pos.Column + filename = pos.Filename + } + hilPos := hilast.Pos{ + Line: line, + Column: column, + Filename: filename, + } + // should not error on plain strings + tree, err := hil.ParseWithPosition(str, hilPos) + if err != nil { + return nil, errwrap.Wrapf(err, "can't parse string interpolation: `%s`", str) + } + if interfaces.Debug { + log.Printf("%s: interpolate: tree: %+v", Name, tree) + } + + result, err := hilTransform(tree) + if err != nil { + return nil, errwrap.Wrapf(err, "error running AST map: `%s`", str) + } + if interfaces.Debug { + log.Printf("%s: interpolate: transform: %+v", Name, result) + } + return result, err +} + +// hilTransform returns the AST equivalent of the hil AST. +func hilTransform(root hilast.Node) (interfaces.Expr, error) { + switch node := root.(type) { + case *hilast.Output: // common root node + if interfaces.Debug { + log.Printf("%s: interpolate: transform: got output type: %+v", Name, node) + } + + if len(node.Exprs) == 0 { + return nil, fmt.Errorf("no expressions found") + } + if len(node.Exprs) == 1 { + return hilTransform(node.Exprs[0]) + } + + // assumes len > 1 + args := []interfaces.Expr{} + for _, n := range node.Exprs { + expr, err := hilTransform(n) + if err != nil { + return nil, errwrap.Wrapf(err, "root failed") + } + args = append(args, expr) + } + + // XXX: i think we should be adding these args together, instead + // of grouping for example... + result, err := concatExprListIntoCall(args) + if err != nil { + return nil, errwrap.Wrapf(err, "function grouping failed") + } + return result, nil + + case *hilast.Call: + if interfaces.Debug { + log.Printf("%s: interpolate: transform: got function type: %+v", Name, node) + } + args := []interfaces.Expr{} + for _, n := range node.Args { + arg, err := hilTransform(n) + if err != nil { + return nil, fmt.Errorf("call failed: %+v", err) + } + args = append(args, arg) + } + + return &ExprCall{ + Name: node.Func, // name + Args: args, + }, nil + + case *hilast.LiteralNode: // string, int, etc... + if interfaces.Debug { + log.Printf("%s: interpolate: transform: got literal type: %+v", Name, node) + } + + switch node.Typex { + case hilast.TypeBool: + return &ExprBool{ + V: node.Value.(bool), + }, nil + + case hilast.TypeString: + return &ExprStr{ + V: node.Value.(string), + }, nil + + case hilast.TypeInt: + return &ExprInt{ + // node.Value is an int stored as an interface + V: int64(node.Value.(int)), + }, nil + + case hilast.TypeFloat: + return &ExprFloat{ + V: node.Value.(float64), + }, nil + + // TODO: should we handle these too? + //case hilast.TypeList: + //case hilast.TypeMap: + + default: + return nil, fmt.Errorf("unmatched type: %T", node) + } + + case *hilast.VariableAccess: // variable lookup + if interfaces.Debug { + log.Printf("%s: interpolate: transform: got variable access type: %+v", Name, node) + } + return &ExprVar{ + Name: node.Name, + }, nil + + //case *hilast.Index: + // if va, ok := node.Target.(*hilast.VariableAccess); ok { + // v, err := NewInterpolatedVariable(va.Name) + // if err != nil { + // resultErr = err + // return n + // } + // result = append(result, v) + // } + // if va, ok := node.Key.(*hilast.VariableAccess); ok { + // v, err := NewInterpolatedVariable(va.Name) + // if err != nil { + // resultErr = err + // return n + // } + // result = append(result, v) + // } + + default: + return nil, fmt.Errorf("unmatched type: %+v", node) + } +} + +// concatExprListIntoCall takes a list of expressions, and combines them into an +// expression which ultimately concatenates them all together with a + operator. +// TODO: this assumes they're all strings, do we need to watch out for int's? +func concatExprListIntoCall(exprs []interfaces.Expr) (interfaces.Expr, error) { + if len(exprs) == 0 { + return nil, fmt.Errorf("empty list") + } + + operator := &ExprStr{ + V: "+", // for PLUS this is a `+` character + } + + if len(exprs) == 1 { + return exprs[0], nil // just return self + } + //if len(exprs) == 1 { + // arg := exprs[0] + // emptyStr := &ExprStr{ + // V: "", // empty str + // } + // return &ExprCall{ + // Name: operatorFuncName, // concatenate the two strings with + operator + // Args: []interfaces.Expr{ + // operator, // operator first + // arg, // string arg + // emptyStr, + // }, + // }, nil + //} + + head, tail := exprs[0], exprs[1:] + + grouped, err := concatExprListIntoCall(tail) + if err != nil { + return nil, err + } + + return &ExprCall{ + Name: operatorFuncName, // concatenate the two strings with + operator + Args: []interfaces.Expr{ + operator, // operator first + head, // string arg + grouped, // nested function call which returns a string + }, + }, nil +} diff --git a/lang/interpolate_test.go b/lang/interpolate_test.go new file mode 100644 index 00000000..5d984b30 --- /dev/null +++ b/lang/interpolate_test.go @@ -0,0 +1,662 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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 lang + +import ( + "fmt" + "reflect" + "strings" + "testing" + + "github.com/purpleidea/mgmt/lang/interfaces" + + "github.com/davecgh/go-spew/spew" + "github.com/kylelemons/godebug/pretty" +) + +func TestInterpolate0(t *testing.T) { + type test struct { // an individual test + name string + code string + fail bool + ast interfaces.Stmt + } + testCases := []test{} + // NOTE: to run an individual test, first run: `go test -v` to list the + // names, and then run `go test -run ` with the name(s) to run. + + { + ast := &StmtProg{ + Prog: []interfaces.Stmt{}, + } + testCases = append(testCases, test{ // 0 + "nil", + ``, + false, + ast, + }) + } + { + ast := &StmtProg{ + Prog: []interfaces.Stmt{ + &StmtRes{ + Kind: "test", + Name: &ExprStr{ + V: "t1", + }, + Fields: []*StmtResField{ + { + Field: "stringptr", + Value: &ExprStr{ + V: "foo", + }, + }, + }, + }, + }, + } + testCases = append(testCases, test{ + name: "basic string", + code: ` + test "t1" { + stringptr => "foo", + } + `, + fail: false, + ast: ast, + }) + } + { + fieldName := &ExprCall{ + Name: operatorFuncName, + Args: []interfaces.Expr{ + &ExprStr{ + V: "+", + }, + &ExprStr{ + V: "foo-", + }, + &ExprVar{ + Name: "x", + }, + }, + } + ast := &StmtProg{ + Prog: []interfaces.Stmt{ + &StmtRes{ + Kind: "test", + Name: &ExprStr{ + V: "t1", + }, + Fields: []*StmtResField{ + { + Field: "stringptr", + Value: fieldName, + }, + }, + }, + }, + } + testCases = append(testCases, test{ + name: "basic expansion", + code: ` + #$x = "hello" # not actually needed to test interpolation + test "t1" { + stringptr => "foo-${x}", + } + `, + fail: false, + ast: ast, + }) + } + + for index, tc := range testCases { // run all the tests + t.Run(fmt.Sprintf("test #%d (%s)", index, tc.name), func(t *testing.T) { + code, fail, exp := tc.code, tc.fail, tc.ast + + str := strings.NewReader(code) + ast, err := LexParse(str) + if err != nil { + t.Errorf("test #%d: FAIL", index) + t.Errorf("test #%d: lex/parse failed with: %+v", index, err) + return + } + t.Logf("test #%d: AST: %+v", index, ast) + + iast, err := ast.Interpolate() + if !fail && err != nil { + t.Errorf("test #%d: FAIL", index) + t.Errorf("test #%d: interpolate failed with: %+v", index, err) + return + } + if fail && err == nil { + t.Errorf("test #%d: FAIL", index) + t.Errorf("test #%d: interpolate expected error, not nil", index) + return + } + + if !reflect.DeepEqual(iast, exp) { + t.Errorf("test #%d: AST did not match expected", index) + // TODO: consider making our own recursive print function + t.Logf("test #%d: actual: \n%s", index, spew.Sdump(iast)) + t.Logf("test #%d: expected: \n%s", index, spew.Sdump(exp)) + if diff := pretty.Compare(iast, exp); diff != "" { // bonus + t.Logf("test #%d: diff:\n%s", index, diff) + } + return + } + }) + } +} + +func TestInterpolateBasicStmt(t *testing.T) { + type test struct { // an individual test + name string + ast interfaces.Stmt + fail bool + exp interfaces.Stmt + } + testCases := []test{} + + // this causes a panic, so it can't be used + //{ + // testCases = append(testCases, test{ + // "nil", + // nil, + // false, + // nil, + // }) + //} + { + ast := &StmtProg{ + Prog: []interfaces.Stmt{ + &StmtRes{ + Kind: "test", + Name: &ExprStr{ + V: "t1", + }, + Fields: []*StmtResField{ + { + Field: "stringptr", + Value: &ExprStr{ + V: "foo", + }, + }, + }, + }, + }, + } + exp := &StmtProg{ + Prog: []interfaces.Stmt{ + &StmtRes{ + Kind: "test", + Name: &ExprStr{ + V: "t1", + }, + Fields: []*StmtResField{ + { + Field: "stringptr", + Value: &ExprStr{ + V: "foo", + }, + }, + }, + }, + }, + } + testCases = append(testCases, test{ + name: "basic resource", + ast: ast, + fail: false, + exp: exp, + }) + } + { + ast := &StmtProg{ + Prog: []interfaces.Stmt{ + &StmtRes{ + Kind: "test", + Name: &ExprStr{ + V: "t${blah}", + }, + Fields: []*StmtResField{ + { + Field: "stringptr", + Value: &ExprStr{ + V: "foo", + }, + }, + }, + }, + }, + } + resName := &ExprCall{ + Name: operatorFuncName, + Args: []interfaces.Expr{ + &ExprStr{ + V: "+", + }, + &ExprStr{ + V: "t", + }, + &ExprVar{ + Name: "blah", + }, + }, + } + exp := &StmtProg{ + Prog: []interfaces.Stmt{ + &StmtRes{ + Kind: "test", + Name: resName, + Fields: []*StmtResField{ + { + Field: "stringptr", + Value: &ExprStr{ + V: "foo", + }, + }, + }, + }, + }, + } + testCases = append(testCases, test{ + name: "expanded resource", + ast: ast, + fail: false, + exp: exp, + }) + } + { + ast := &StmtProg{ + Prog: []interfaces.Stmt{ + &StmtRes{ + Kind: "test", + Name: &ExprStr{ + V: "t${42}", // incorrect type + }, + Fields: []*StmtResField{ + { + Field: "stringptr", + Value: &ExprStr{ + V: "foo", + }, + }, + }, + }, + }, + } + resName := &ExprCall{ + Name: operatorFuncName, + // incorrect sig for this function, but correct interpolation + Args: []interfaces.Expr{ + &ExprStr{ + V: "+", + }, + &ExprStr{ + V: "t", + }, + &ExprInt{ + V: 42, + }, + }, + } + exp := &StmtProg{ + Prog: []interfaces.Stmt{ + &StmtRes{ + Kind: "test", + Name: resName, + Fields: []*StmtResField{ + { + Field: "stringptr", + Value: &ExprStr{ + V: "foo", + }, + }, + }, + }, + }, + } + testCases = append(testCases, test{ + name: "expanded invalid resource name", + ast: ast, + fail: false, + exp: exp, + }) + } + + for index, tc := range testCases { // run all the tests + t.Run(fmt.Sprintf("test #%d (%s)", index, tc.name), func(t *testing.T) { + ast, fail, exp := tc.ast, tc.fail, tc.exp + + iast, err := ast.Interpolate() + if !fail && err != nil { + t.Errorf("test #%d: FAIL", index) + t.Errorf("test #%d: interpolate failed with: %+v", index, err) + return + } + if fail && err == nil { + t.Errorf("test #%d: FAIL", index) + t.Errorf("test #%d: interpolate expected error, not nil", index) + return + } + + if !reflect.DeepEqual(iast, exp) { + t.Errorf("test #%d: AST did not match expected", index) + // TODO: consider making our own recursive print function + t.Logf("test #%d: actual: \n%s", index, spew.Sdump(iast)) + t.Logf("test #%d: expected: \n%s", index, spew.Sdump(exp)) + if diff := pretty.Compare(iast, exp); diff != "" { // bonus + t.Logf("test #%d: diff:\n%s", index, diff) + } + return + } + }) + } +} + +func TestInterpolateBasicExpr(t *testing.T) { + type test struct { // an individual test + name string + ast interfaces.Expr + fail bool + exp interfaces.Expr + } + testCases := []test{} + + // this causes a panic, so it can't be used + //{ + // testCases = append(testCases, test{ + // "nil", + // nil, + // false, + // nil, + // }) + //} + { + ast := &ExprStr{ + V: "hello", + } + exp := &ExprStr{ + V: "hello", + } + testCases = append(testCases, test{ + name: "basic string", + ast: ast, + fail: false, + exp: exp, + }) + } + { + ast := &ExprStr{ + V: "hello ${person_name}", + } + exp := &ExprCall{ + Name: operatorFuncName, + Args: []interfaces.Expr{ + &ExprStr{ + V: "+", + }, + &ExprStr{ + V: "hello ", + }, + &ExprVar{ + Name: "person_name", + }, + }, + } + testCases = append(testCases, test{ + name: "basic expansion", + ast: ast, + fail: false, + exp: exp, + }) + } + { + ast := &ExprStr{ + V: "hello ${x ${y} z}", + } + testCases = append(testCases, test{ + name: "invalid expansion", + ast: ast, + fail: true, + }) + } + // TODO: patterns like what are shown below are supported by the `hil` + // library, but are not yet supported by our translation layer, nor do + // they necessarily work or make much sense at this point in time... + //{ + // ast := &ExprStr{ + // V: `hello ${func("hello ${var.foo}")}`, + // } + // exp := nil // TODO: add this + // testCases = append(testCases, test{ + // name: "double expansion", + // ast: ast, + // fail: false, + // exp: exp, + // }) + //} + { + ast := &ExprStr{ + V: "sweetie${3.14159}", // invalid but only at type check + } + exp := &ExprCall{ + Name: operatorFuncName, + Args: []interfaces.Expr{ + &ExprStr{ + V: "+", + }, + &ExprStr{ + V: "sweetie", + }, + &ExprFloat{ + V: 3.14159, + }, + }, + } + testCases = append(testCases, test{ + name: "float expansion", + ast: ast, + fail: false, + exp: exp, + }) + } + { + ast := &ExprStr{ + V: "i am: ${hostname()}", + } + exp := &ExprCall{ + Name: operatorFuncName, + Args: []interfaces.Expr{ + &ExprStr{ + V: "+", + }, + &ExprStr{ + V: "i am: ", + }, + &ExprCall{ + Name: "hostname", + Args: []interfaces.Expr{}, + }, + }, + } + testCases = append(testCases, test{ + name: "function expansion", + ast: ast, + fail: false, + exp: exp, + }) + } + { + ast := &ExprStr{ + V: "i am: ${blah(21, 12.3)}", + } + exp := &ExprCall{ + Name: operatorFuncName, + Args: []interfaces.Expr{ + &ExprStr{ + V: "+", + }, + &ExprStr{ + V: "i am: ", + }, + &ExprCall{ + Name: "blah", + Args: []interfaces.Expr{ + &ExprInt{ + V: 21, + }, + &ExprFloat{ + V: 12.3, + }, + }, + }, + }, + } + testCases = append(testCases, test{ + name: "function expansion arg", + ast: ast, + fail: false, + exp: exp, + }) + } + // FIXME: i am broken, i don't deal well with negatives for some reason + //{ + // ast := &ExprStr{ + // V: "i am: ${blah(21, -12.3)}", + // } + // exp := &ExprCall{ + // Name: operatorFuncName, + // Args: []interfaces.Expr{ + // &ExprStr{ + // V: "+", + // }, + // &ExprStr{ + // V: "i am: ", + // }, + // &ExprCall{ + // Name: "blah", + // Args: []interfaces.Expr{ + // &ExprInt{ + // V: 21, + // }, + // &ExprFloat{ + // V: -12.3, + // }, + // }, + // }, + // }, + // } + // testCases = append(testCases, test{ + // name: "function expansion arg negative", + // ast: ast, + // fail: false, + // exp: exp, + // }) + //} + // FIXME: i am broken :( + //{ + // ast := &ExprStr{ + // V: "sweetie${-3.14159}", // FIXME: only the negative breaks this + // } + // exp := &ExprCall{ + // Name: operatorFuncName, + // Args: []interfaces.Expr{ + // &ExprStr{ + // V: "+", + // }, + // &ExprStr{ + // V: "sweetie", + // }, + // &ExprFloat{ + // V: -3.14159, + // }, + // }, + // } + // testCases = append(testCases, test{ + // name: "negative float expansion", + // ast: ast, + // fail: false, + // exp: exp, + // }) + //} + // FIXME: i am also broken, but less important + //{ + // ast := &ExprStr{ + // V: `i am: ${blah(42, "${foo}")}`, + // } + // exp := &ExprCall{ + // Name: operatorFuncName, + // Args: []interfaces.Expr{ + // &ExprStr{ + // V: "+", + // }, + // &ExprStr{ + // V: "i am: ", + // }, + // &ExprCall{ + // Name: "blah", + // Args: []interfaces.Expr{ + // &ExprInt{ + // V: 42, + // }, + // &ExprVar{ + // Name: "foo", + // }, + // }, + // }, + // }, + // } + // testCases = append(testCases, test{ + // name: "function expansion arg with var", + // ast: ast, + // fail: false, + // exp: exp, + // }) + //} + + for index, tc := range testCases { // run all the tests + t.Run(fmt.Sprintf("test #%d (%s)", index, tc.name), func(t *testing.T) { + ast, fail, exp := tc.ast, tc.fail, tc.exp + + iast, err := ast.Interpolate() + if !fail && err != nil { + t.Errorf("test #%d: FAIL", index) + t.Errorf("test #%d: interpolate failed with: %+v", index, err) + return + } + if fail && err == nil { + t.Errorf("test #%d: FAIL", index) + t.Errorf("test #%d: interpolate expected error, not nil", index) + return + } + + if !reflect.DeepEqual(iast, exp) { + t.Errorf("test #%d: AST did not match expected", index) + // TODO: consider making our own recursive print function + t.Logf("test #%d: actual: \n%s", index, spew.Sdump(iast)) + t.Logf("test #%d: expected: \n%s", index, spew.Sdump(exp)) + if diff := pretty.Compare(iast, exp); diff != "" { // bonus + t.Logf("test #%d: diff:\n%s", index, diff) + } + return + } + }) + } +} diff --git a/lang/interpret.go b/lang/interpret.go new file mode 100644 index 00000000..dbfded62 --- /dev/null +++ b/lang/interpret.go @@ -0,0 +1,144 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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 lang // TODO: move this into a sub package of lang/$name? + +import ( + "fmt" + + "github.com/purpleidea/mgmt/lang/interfaces" + "github.com/purpleidea/mgmt/pgraph" + "github.com/purpleidea/mgmt/resources" + + errwrap "github.com/pkg/errors" +) + +// interpret runs the program and causes a graph generation as a side effect. +// You should not run this on the AST if you haven't previously run the function +// graph engine so that output values have been produced! Type unification is +// another important aspect which needs to have been completed. +func interpret(ast interfaces.Stmt) (*pgraph.Graph, error) { + output, err := ast.Output() // contains resList, edgeList, etc... + if err != nil { + return nil, err + } + + graph, err := pgraph.NewGraph("interpret") // give graph a default name + if err != nil { + return nil, errwrap.Wrapf(err, "could not create new graph") + } + + var lookup = make(map[string]map[string]resources.Res) // map[kind]map[name]Res + // build the send/recv mapping; format: map[kind]map[name]map[field]*Send + var receive = make(map[string]map[string]map[string]*resources.Send) + + for _, res := range output.Resources { + graph.AddVertex(res) + kind := res.GetKind() + name := res.GetName() + if _, exists := lookup[kind]; !exists { + lookup[kind] = make(map[string]resources.Res) + receive[kind] = make(map[string]map[string]*resources.Send) + } + if _, exists := receive[kind][name]; !exists { + receive[kind][name] = make(map[string]*resources.Send) + } + + if r, exists := lookup[kind][name]; exists { // found same name + if !r.Compare(res) { + // TODO: print a diff of the two resources + return nil, fmt.Errorf("incompatible duplicate resource `%s` found", res) + } + // more than one compatible resource exists... we allow + // duplicates, if they're going to not conflict... + // XXX: does it matter which one we add to the graph? + } + lookup[kind][name] = res // add to temporary lookup table + } + + for _, e := range output.Edges { + var v1, v2 resources.Res + var exists = true + var m map[string]resources.Res + var notify = e.Notify + + if m, exists = lookup[e.Kind1]; exists { + v1, exists = m[e.Name1] + } + if !exists { + return nil, fmt.Errorf("edge cannot find resource kind: %s named: `%s`", e.Kind1, e.Name1) + } + if m, exists = lookup[e.Kind2]; exists { + v2, exists = m[e.Name2] + } + if !exists { + return nil, fmt.Errorf("edge cannot find resource kind: %s named: `%s`", e.Kind2, e.Name2) + } + + if existingEdge := graph.FindEdge(v1, v2); existingEdge != nil { + // collate previous Notify signals to this edge with OR + notify = notify || (existingEdge.(*resources.Edge)).Notify + } + + edge := &resources.Edge{ + Name: fmt.Sprintf("%s -> %s", v1, v2), + Notify: notify, + } + graph.AddEdge(v1, v2, edge) // identical duplicates are ignored + + // send recv + if (e.Send == "") != (e.Recv == "") { // xor + return nil, fmt.Errorf("you must specify both send/recv fields or neither") + } + if e.Send == "" || e.Recv == "" { // is there send/recv to do or not? + continue + } + + // check for pre-existing send/recv at this key + if existingSend, exists := receive[e.Kind2][e.Name2][e.Recv]; exists { + // ignore identical duplicates + // TODO: does this safe ignore work with duplicate compatible resources? + if existingSend.Res != v1 || existingSend.Key != e.Send { + return nil, fmt.Errorf("resource kind: %s named: `%s` already receives on `%s`", e.Kind2, e.Name2, e.Recv) + } + } + + // store mapping for later + receive[e.Kind2][e.Name2][e.Recv] = &resources.Send{Res: v1, Key: e.Send} + } + + // we need to first build up a map of all the resources handles, because + // we don't know which order send/recv pairs will arrive in, and we need + // to ensure the right pointer exists before we reference it... finally, + // we build up a list of send/recv mappings to ensure we don't overwrite + // pre-existing mappings, so we can now set them all at once at the end! + + // TODO: do this in a deterministic order + for kind, x := range receive { + for name, recv := range x { + lookup[kind][name].SetRecv(recv) // set it! + } + } + + // ensure that we have a DAG! + if _, err := graph.TopologicalSort(); err != nil { + // TODO: print information on the cycles + return nil, errwrap.Wrapf(err, "resource graph has cycles") + } + + return graph, nil +} diff --git a/lang/interpret_test.go b/lang/interpret_test.go new file mode 100644 index 00000000..dc0d0740 --- /dev/null +++ b/lang/interpret_test.go @@ -0,0 +1,602 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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 lang + +import ( + "fmt" + "strings" + "testing" + + "github.com/purpleidea/mgmt/lang/interfaces" + "github.com/purpleidea/mgmt/lang/unification" + "github.com/purpleidea/mgmt/pgraph" + "github.com/purpleidea/mgmt/resources" +) + +func vertexAstCmpFn(v1, v2 pgraph.Vertex) (bool, error) { + //fmt.Printf("V1: %T %+v\n", v1, v1) + //node := v1.(*funcs.Node) + //fmt.Printf("node: %T %+v\n", node, node) + //fmt.Printf("V2: %T %+v\n", v2, v2) + if v1.String() == "" || v2.String() == "" { + return false, fmt.Errorf("oops, empty vertex") + } + return v1.String() == v2.String(), nil +} + +func edgeAstCmpFn(e1, e2 pgraph.Edge) (bool, error) { + if e1.String() == "" || e2.String() == "" { + return false, fmt.Errorf("oops, empty edge") + } + return e1.String() == e2.String(), nil +} + +type vtex string + +func (obj *vtex) String() string { + return string(*obj) +} + +type edge string + +func (obj *edge) String() string { + return string(*obj) +} + +func TestAstFunc0(t *testing.T) { + scope := &interfaces.Scope{ // global scope + Variables: map[string]interfaces.Expr{ + "hello": &ExprStr{V: "world"}, + "answer": &ExprInt{V: 42}, + }, + } + + type test struct { // an individual test + name string + code string + fail bool + scope *interfaces.Scope + graph *pgraph.Graph + } + values := []test{} + + { + graph, _ := pgraph.NewGraph("g") + values = append(values, test{ // 0 + "nil", + ``, + false, + nil, + graph, + }) + } + { + graph, _ := pgraph.NewGraph("g") + values = append(values, test{ + name: "scope only", + code: ``, + fail: false, + scope: scope, // use the scope defined above + graph: graph, + }) + } + { + graph, _ := pgraph.NewGraph("g") + v1, v2 := vtex("int(42)"), vtex("var(x)") + e1 := edge("x") + graph.AddVertex(&v1, &v2) + graph.AddEdge(&v1, &v2, &e1) + values = append(values, test{ + name: "two vars", + code: ` + $x = 42 + $y = $x + `, + // TODO: this should fail with an unused variable error! + fail: false, + graph: graph, + }) + } + { + graph, _ := pgraph.NewGraph("g") + values = append(values, test{ + name: "self-referential vars", + code: ` + $x = $y + $y = $x + `, + fail: true, + graph: graph, + }) + } + { + graph, _ := pgraph.NewGraph("g") + v1, v2, v3, v4, v5 := vtex("int(42)"), vtex("var(a)"), vtex("var(b)"), vtex("var(c)"), vtex("str(t)") + e1, e2, e3 := edge("a"), edge("b"), edge("c") + graph.AddVertex(&v1, &v2, &v3, &v4, &v5) + graph.AddEdge(&v1, &v2, &e1) + graph.AddEdge(&v2, &v3, &e2) + graph.AddEdge(&v3, &v4, &e3) + values = append(values, test{ + name: "chained vars", + code: ` + test "t" { + int64ptr => $c, + } + $c = $b + $b = $a + $a = 42 + `, + fail: false, + graph: graph, + }) + } + { + graph, _ := pgraph.NewGraph("g") + v1, v2 := vtex("bool(true)"), vtex("var(b)") + graph.AddVertex(&v1, &v2) + e1 := edge("b") + graph.AddEdge(&v1, &v2, &e1) + values = append(values, test{ + name: "simple bool", + code: ` + if $b { + } + $b = true + `, + fail: false, + graph: graph, + }) + } + { + graph, _ := pgraph.NewGraph("g") + v1, v2, v3, v4, v5 := vtex("str(t)"), vtex("str(+)"), vtex("int(42)"), vtex("int(13)"), vtex(fmt.Sprintf("call:%s(str(+), int(42), int(13))", operatorFuncName)) + graph.AddVertex(&v1, &v2, &v3, &v4, &v5) + e1, e2, e3 := edge("x"), edge("a"), edge("b") + graph.AddEdge(&v2, &v5, &e1) + graph.AddEdge(&v3, &v5, &e2) + graph.AddEdge(&v4, &v5, &e3) + values = append(values, test{ + name: "simple operator", + code: ` + test "t" { + int64ptr => 42 + 13, + } + `, + fail: false, + graph: graph, + }) + } + { + graph, _ := pgraph.NewGraph("g") + v1, v2, v3 := vtex("str(t)"), vtex("str(-)"), vtex("str(+)") + v4, v5, v6 := vtex("int(42)"), vtex("int(13)"), vtex("int(99)") + v7 := vtex(fmt.Sprintf("call:%s(str(+), int(42), int(13))", operatorFuncName)) + v8 := vtex(fmt.Sprintf("call:%s(str(-), call:%s(str(+), int(42), int(13)), int(99))", operatorFuncName, operatorFuncName)) + + graph.AddVertex(&v1, &v2, &v3, &v4, &v5, &v6, &v7, &v8) + e1, e2, e3 := edge("x"), edge("a"), edge("b") + graph.AddEdge(&v3, &v7, &e1) + graph.AddEdge(&v4, &v7, &e2) + graph.AddEdge(&v5, &v7, &e3) + + e4, e5, e6 := edge("x"), edge("a"), edge("b") + graph.AddEdge(&v2, &v8, &e4) + graph.AddEdge(&v7, &v8, &e5) + graph.AddEdge(&v6, &v8, &e6) + values = append(values, test{ + name: "simple operators", + code: ` + test "t" { + int64ptr => 42 + 13 - 99, + } + `, + fail: false, + graph: graph, + }) + } + { + graph, _ := pgraph.NewGraph("g") + v1, v2 := vtex("bool(true)"), vtex("str(t)") + v3, v4 := vtex("int(13)"), vtex("int(42)") + v5, v6 := vtex("var(i)"), vtex("var(x)") + v7, v8 := vtex("str(+)"), vtex(fmt.Sprintf("call:%s(str(+), int(42), var(i))", operatorFuncName)) + + e1, e2, e3, e4, e5 := edge("x"), edge("a"), edge("b"), edge("i"), edge("x") + graph.AddVertex(&v1, &v2, &v3, &v4, &v5, &v6, &v7, &v8) + graph.AddEdge(&v3, &v5, &e4) + + graph.AddEdge(&v7, &v8, &e1) + graph.AddEdge(&v4, &v8, &e2) + graph.AddEdge(&v5, &v8, &e3) + + graph.AddEdge(&v8, &v6, &e5) + values = append(values, test{ + name: "nested resource and scoped var", + code: ` + if true { + test "t" { + int64ptr => $x, + } + $x = 42 + $i + } + $i = 13 + `, + fail: false, + graph: graph, + }) + } + { + values = append(values, test{ + name: "out of scope error", + code: ` + # should be out of scope, and a compile error! + if $b { + } + if true { + $b = true + } + `, + fail: true, + }) + } + { + values = append(values, test{ + name: "variable re-declaration error", + code: ` + # this should fail b/c of variable re-declaration + $x = "hello" + $x = "world" # woops + `, + fail: true, + }) + } + { + graph, _ := pgraph.NewGraph("g") + v1, v2, v3 := vtex("str(hello)"), vtex("str(world)"), vtex("bool(true)") + v4, v5 := vtex("var(x)"), vtex("str(t)") + + graph.AddVertex(&v1, &v2, &v3, &v4, &v5) + e1 := edge("x") + // only one edge! (cool) + graph.AddEdge(&v1, &v4, &e1) + + values = append(values, test{ + name: "variable shadowing", + code: ` + # this should be okay, because var is shadowed + $x = "hello" + if true { + $x = "world" # shadowed + } + test "t" { + stringptr => $x, + } + `, + fail: false, + graph: graph, + }) + } + { + graph, _ := pgraph.NewGraph("g") + v1, v2, v3 := vtex("str(hello)"), vtex("str(world)"), vtex("bool(true)") + v4, v5 := vtex("var(x)"), vtex("str(t)") + + graph.AddVertex(&v1, &v2, &v3, &v4, &v5) + e1 := edge("x") + // only one edge! (cool) + graph.AddEdge(&v2, &v4, &e1) + + values = append(values, test{ + name: "variable shadowing inner", + code: ` + # this should be okay, because var is shadowed + $x = "hello" + if true { + $x = "world" # shadowed + test "t" { + stringptr => $x, + } + } + `, + fail: false, + graph: graph, + }) + } + // // FIXME: blocked by: https://github.com/purpleidea/mgmt/issues/199 + //{ + // graph, _ := pgraph.NewGraph("g") + // v0 := vtex("bool(true)") + // v1, v2 := vtex("str(hello)"), vtex("str(world)") + // v3, v4 := vtex("var(x)"), vtex("var(x)") // different vertices! + // v5, v6 := vtex("str(t1)"), vtex("str(t2)") + // + // graph.AddVertex(&v0, &v1, &v2, &v3, &v4, &v5, &v6) + // e1, e2 := edge("x"), edge("x") + // graph.AddEdge(&v1, &v3, &e1) + // graph.AddEdge(&v2, &v4, &e2) + // + // values = append(values, test{ + // name: "variable shadowing both", + // code: ` + // # this should be okay, because var is shadowed + // $x = "hello" + // if true { + // $x = "world" # shadowed + // test "t2" { + // stringptr => $x, + // } + // } + // test "t1" { + // stringptr => $x, + // } + // `, + // fail: false, + // graph: graph, + // }) + //} + { + values = append(values, test{ + name: "variable re-declaration and type change error", + code: ` + # this should fail b/c of variable re-declaration + $x = "wow" + $x = 99 # woops, but also a change of type :P + `, + fail: true, + }) + } + + for index, test := range values { // run all the tests + name, code, fail, scope, exp := test.name, test.code, test.fail, test.scope, test.graph + + if name == "" { + name = "" + } + + //if index != 3 { // hack to run a subset (useful for debugging) + //if test.name != "simple operators" { + // continue + //} + + t.Logf("\n\ntest #%d (%s) ----------------\n\n", index, name) + str := strings.NewReader(code) + ast, err := LexParse(str) + if err != nil { + t.Errorf("test #%d: FAIL", index) + t.Errorf("test #%d: lex/parse failed with: %+v", index, err) + continue + } + t.Logf("test #%d: AST: %+v", index, ast) + + iast, err := ast.Interpolate() + if err != nil { + t.Errorf("test #%d: FAIL", index) + t.Errorf("test #%d: interpolate failed with: %+v", index, err) + continue + } + + // propagate the scope down through the AST... + err = iast.SetScope(scope) + if !fail && err != nil { + t.Errorf("test #%d: FAIL", index) + t.Errorf("test #%d: could not set scope: %+v", index, err) + continue + } + if fail && err != nil { + continue // fail happened during set scope, don't run unification! + } + + // apply type unification + logf := func(format string, v ...interface{}) { + t.Logf(fmt.Sprintf("test #%d", index)+": unification: "+format, v...) + } + err = unification.Unify(iast, unification.SimpleInvariantSolverLogger(logf)) + if !fail && err != nil { + t.Errorf("test #%d: FAIL", index) + t.Errorf("test #%d: could not unify types: %+v", index, err) + continue + } + // maybe it will fail during graph below instead? + //if fail && err == nil { + // t.Errorf("test #%d: FAIL", index) + // t.Errorf("test #%d: unification passed, expected fail", index) + // continue + //} + if fail && err != nil { + continue // fail happened during unification, don't run Graph! + } + + // build the function graph + graph, err := iast.Graph() + + if !fail && err != nil { + t.Errorf("test #%d: FAIL", index) + t.Errorf("test #%d: functions failed with: %+v", index, err) + continue + } + if fail && err == nil { + t.Errorf("test #%d: FAIL", index) + t.Errorf("test #%d: functions passed, expected fail", index) + continue + } + + if fail { // can't process graph if it's nil + // TODO: match against expected error + t.Logf("test #%d: error: %+v", index, err) + continue + } + + t.Logf("test #%d: graph: %+v", index, graph) + // TODO: improve: https://github.com/purpleidea/mgmt/issues/199 + if err := graph.GraphCmp(exp, vertexAstCmpFn, edgeAstCmpFn); err != nil { + t.Errorf("test #%d: FAIL\n\n", index) + t.Logf("test #%d: actual (g1): %v%s\n\n", index, graph, fullPrint(graph)) + t.Logf("test #%d: expected (g2): %v%s\n\n", index, exp, fullPrint(exp)) + t.Errorf("test #%d: cmp error:\n%v", index, err) + continue + } + + for i, v := range graph.Vertices() { + t.Logf("test #%d: vertex(%d): %+v", index, i, v) + } + for v1 := range graph.Adjacency() { + for v2, e := range graph.Adjacency()[v1] { + t.Logf("test #%d: edge(%+v): %+v -> %+v", index, e, v1, v2) + } + } + } +} + +// TestAstInterpret0 should only be run in limited circumstances. Read the code +// comments below to see how it is run. +func TestAstInterpret0(t *testing.T) { + type test struct { // an individual test + name string + code string + fail bool + graph *pgraph.Graph + } + values := []test{} + + { + graph, _ := pgraph.NewGraph("g") + values = append(values, test{ // 0 + "nil", + ``, + false, + graph, + }) + } + { + values = append(values, test{ + name: "wrong res field type", + code: ` + test "t1" { + stringptr => 42, # int, not str + } + `, + fail: true, + }) + } + { + graph, _ := pgraph.NewGraph("g") + t1, _ := resources.NewNamedResource("test", "t1") + x := t1.(*resources.TestRes) + int64ptr := int64(42) + x.Int64Ptr = &int64ptr + str := "okay cool" + x.StringPtr = &str + int8ptr := int8(127) + int8ptrptr := &int8ptr + int8ptrptrptr := &int8ptrptr + x.Int8PtrPtrPtr = &int8ptrptrptr + graph.AddVertex(t1) + values = append(values, test{ + name: "resource with three pointer fields", + code: ` + test "t1" { + int64ptr => 42, + stringptr => "okay cool", + int8ptrptrptr => 127, # super nested + } + `, + fail: false, + graph: graph, + }) + } + { + graph, _ := pgraph.NewGraph("g") + t1, _ := resources.NewNamedResource("test", "t1") + x := t1.(*resources.TestRes) + stringptr := "wow" + x.StringPtr = &stringptr + graph.AddVertex(t1) + values = append(values, test{ + name: "resource with simple string pointer field", + code: ` + test "t1" { + stringptr => "wow", + } + `, + graph: graph, + }) + } + + for index, test := range values { // run all the tests + name, code, fail, exp := test.name, test.code, test.fail, test.graph + + if name == "" { + name = "" + } + + //if index != 3 { // hack to run a subset (useful for debugging) + //if test.name != "nil" { + // continue + //} + + t.Logf("\n\ntest #%d (%s) ----------------\n\n", index, name) + + str := strings.NewReader(code) + ast, err := LexParse(str) + if err != nil { + t.Errorf("test #%d: lex/parse failed with: %+v", index, err) + continue + } + t.Logf("test #%d: AST: %+v", index, ast) + + // these tests only work in certain cases, since this does not + // perform type unification, run the function graph engine, and + // only gives you limited results... don't expect normal code to + // run and produce meaningful things in this test... + graph, err := interpret(ast) + + if !fail && err != nil { + t.Errorf("test #%d: interpret failed with: %+v", index, err) + continue + } + if fail && err == nil { + t.Errorf("test #%d: interpret passed, expected fail", index) + continue + } + + if fail { // can't process graph if it's nil + // TODO: match against expected error + t.Logf("test #%d: expected fail, error: %+v", index, err) + continue + } + + t.Logf("test #%d: graph: %+v", index, graph) + // TODO: improve: https://github.com/purpleidea/mgmt/issues/199 + if err := graph.GraphCmp(exp, vertexCmpFn, edgeCmpFn); err != nil { + t.Logf("test #%d: actual (g1): %v%s", index, graph, fullPrint(graph)) + t.Logf("test #%d: expected (g2): %v%s", index, exp, fullPrint(exp)) + t.Errorf("test #%d: cmp error:\n%v", index, err) + continue + } + + for i, v := range graph.Vertices() { + t.Logf("test #%d: vertex(%d): %+v", index, i, v) + } + for v1 := range graph.Adjacency() { + for v2, e := range graph.Adjacency()[v1] { + t.Logf("test #%d: edge(%+v): %+v -> %+v", index, e, v1, v2) + } + } + } +} diff --git a/lang/lang.go b/lang/lang.go new file mode 100644 index 00000000..f9e1fa1c --- /dev/null +++ b/lang/lang.go @@ -0,0 +1,267 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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 lang // TODO: move this into a sub package of lang/$name? + +import ( + "fmt" + "io" + "log" + "sync" + + "github.com/purpleidea/mgmt/lang/funcs" + _ "github.com/purpleidea/mgmt/lang/funcs/core" // import so the funcs register + _ "github.com/purpleidea/mgmt/lang/funcs/facts/core" // import so the facts register + "github.com/purpleidea/mgmt/lang/interfaces" + "github.com/purpleidea/mgmt/lang/unification" + "github.com/purpleidea/mgmt/pgraph" + "github.com/purpleidea/mgmt/resources" + + errwrap "github.com/pkg/errors" +) + +const ( + // FileNameExtension is the filename extension used for languages files. + FileNameExtension = "mcl" // alternate suggestions welcome! + + // make these available internally without requiring the import + operatorFuncName = funcs.OperatorFuncName + historyFuncName = funcs.HistoryFuncName + containsFuncName = funcs.ContainsFuncName +) + +// Lang is the main language lexer/parser object. +type Lang struct { + Input io.Reader // os.Stdin or anything that satisfies this interface + Hostname string + World resources.World + Debug bool + + ast interfaces.Stmt // store main prog AST here + funcs *funcs.Engine // function event engine + + loadedChan chan struct{} // loaded signal + + streamChan chan error // signals a new graph can be created or problem + //streamBurst bool // should we try and be bursty with the stream events? + + closeChan chan struct{} // close signal + wg *sync.WaitGroup +} + +// Init initializes the lang struct, and starts up the initial data sources. +// NOTE: The trick is that we need to get the list of funcs to watch AND start +// watching them, *before* we pull their values, that way we'll know if they +// changed from the values we wanted. +func (obj *Lang) Init() error { + obj.loadedChan = make(chan struct{}) + obj.streamChan = make(chan error) + obj.closeChan = make(chan struct{}) + obj.wg = &sync.WaitGroup{} + + once := &sync.Once{} + loadedSignal := func() { close(obj.loadedChan) } // only run once! + + // run the lexer/parser and build an AST + log.Printf("%s: Lexing/Parsing...", Name) + ast, err := LexParse(obj.Input) + if err != nil { + return errwrap.Wrapf(err, "could not generate AST") + } + if obj.Debug { + log.Printf("%s: behold, the AST: %+v", Name, ast) + } + + // TODO: should we validate the structure of the AST? + // TODO: should we do this *after* interpolate, or trust it to behave? + //if err := ast.Validate(); err != nil { + // return errwrap.Wrapf(err, "could not validate AST") + //} + + log.Printf("%s: Interpolating...", Name) + // interpolate strings and other expansionable nodes in AST + interpolated, err := ast.Interpolate() + if err != nil { + return errwrap.Wrapf(err, "could not interpolate AST") + } + obj.ast = interpolated + + // 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}, + }, + } + + log.Printf("%s: Building Scope...", Name) + // propagate the scope down through the AST... + if err := obj.ast.SetScope(scope); err != nil { + return errwrap.Wrapf(err, "could not set scope") + } + + // apply type unification + logf := func(format string, v ...interface{}) { + if obj.Debug { // unification only has debug messages... + log.Printf(Name+": unification: "+format, v...) + } + } + log.Printf("%s: Running Type Unification...", Name) + if err := unification.Unify(obj.ast, unification.SimpleInvariantSolverLogger(logf)); err != nil { + return errwrap.Wrapf(err, "could not unify types") + } + + log.Printf("%s: Building Function Graph...", Name) + // we assume that for some given code, the list of funcs doesn't change + // iow, we don't support variable, variables or absurd things like that + graph, err := obj.ast.Graph() // build the graph of functions + if err != nil { + return errwrap.Wrapf(err, "could not generate function graph") + } + + if obj.Debug { + log.Printf("%s: function graph: %+v", Name, graph) + graph.Logf("%s: ", Name) // log graph with this printf prefix... + } + + if graph.NumVertices() == 0 { // no funcs to load! + // send only one signal since we won't ever send after this! + log.Printf("%s: Static graph found", Name) + obj.wg.Add(1) + go func() { + defer obj.wg.Done() + defer close(obj.streamChan) // no more events are coming! + select { + case obj.streamChan <- nil: // send one signal + // pass + case <-obj.closeChan: + return + } + close(obj.loadedChan) // signal + }() + return nil // exit early, no funcs to load! + } + + obj.funcs = &funcs.Engine{ + Graph: graph, // not the same as the output graph! + Hostname: obj.Hostname, + World: obj.World, + Debug: obj.Debug, + Logf: func(format string, v ...interface{}) { + log.Printf(Name+": funcs: "+format, v...) + }, + Glitch: false, // FIXME: verify this functionality is perfect! + } + + log.Printf("%s: Function Engine Initializing...", Name) + if err := obj.funcs.Init(); err != nil { + return errwrap.Wrapf(err, "init error with func engine") + } + + log.Printf("%s: Function Engine Validating...", Name) + if err := obj.funcs.Validate(); err != nil { + return errwrap.Wrapf(err, "validate error with func engine") + } + + log.Printf("%s: Function Engine Starting...", Name) + // On failure, we expect the caller to run Close() to shutdown all of + // the currently initialized (and running) funcs... This is needed if + // we successfully ran `Run` but isn't needed only for Init/Validate. + if err := obj.funcs.Run(); err != nil { + return errwrap.Wrapf(err, "run error with func engine") + } + + // wait for some activity + log.Printf("%s: Stream...", Name) + stream := obj.funcs.Stream() + obj.wg.Add(1) + go func() { + log.Printf("%s: Loop...", Name) + defer obj.wg.Done() + defer close(obj.streamChan) // no more events are coming! + for { + var err error + var ok bool + select { + case err, ok = <-stream: + if !ok { + log.Printf("%s: Stream closed", Name) + return + } + if err == nil { + // only do this once, on the first event + once.Do(loadedSignal) // signal + } + + case <-obj.closeChan: + return + } + + select { + case obj.streamChan <- err: // send + if err != nil { + log.Printf("%s: Stream error: %+v", Name, err) + return + } + + case <-obj.closeChan: + return + } + } + }() + return nil +} + +// Stream returns a channel of graph change requests or errors. These are +// usually sent when a func output changes. +func (obj *Lang) Stream() chan error { + return obj.streamChan +} + +// Interpret runs the interpreter and returns a graph and corresponding error. +func (obj *Lang) Interpret() (*pgraph.Graph, error) { + select { + case <-obj.loadedChan: // funcs are now loaded! + // pass + default: + // if this is hit, someone probably called this too early! + // it should only be called in response to a stream event! + return nil, fmt.Errorf("funcs aren't loaded yet") + } + + log.Printf("%s: Running interpret...", Name) + // this call returns the graph + graph, err := interpret(obj.ast) + if err != nil { + return nil, errwrap.Wrapf(err, "could not interpret") + } + + return graph, nil // return a graph +} + +// Close shuts down the lang struct and causes all the funcs to shutdown. It +// must be called when finished after any successful Init ran. +func (obj *Lang) Close() error { + var err error + if obj.funcs != nil { + err = obj.funcs.Close() + } + close(obj.closeChan) + obj.wg.Wait() + return err +} diff --git a/lang/lang_test.go b/lang/lang_test.go new file mode 100644 index 00000000..74869e9f --- /dev/null +++ b/lang/lang_test.go @@ -0,0 +1,414 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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 lang + +import ( + "fmt" + "strings" + "testing" + + _ "github.com/purpleidea/mgmt/lang/funcs/facts/core" // load facts + "github.com/purpleidea/mgmt/pgraph" + "github.com/purpleidea/mgmt/resources" + + multierr "github.com/hashicorp/go-multierror" + errwrap "github.com/pkg/errors" +) + +// TODO: unify with the other function like this... +// TODO: where should we put our test helpers? +func runGraphCmp(t *testing.T, g1, g2 *pgraph.Graph) { + err := g1.GraphCmp(g2, vertexCmpFn, edgeCmpFn) + if err != nil { + t.Logf(" actual (g1): %v%s", g1, fullPrint(g1)) + t.Logf("expected (g2): %v%s", g2, fullPrint(g2)) + t.Errorf("Cmp error:\n%v", err) + } +} + +// TODO: unify with the other function like this... +func fullPrint(g *pgraph.Graph) (str string) { + if g == nil { + return "" + } + str += "\n" + for v := range g.Adjacency() { + str += fmt.Sprintf("* v: %s\n", v) + } + for v1 := range g.Adjacency() { + for v2, e := range g.Adjacency()[v1] { + str += fmt.Sprintf("* e: %s -> %s # %s\n", v1, v2, e) + } + } + return +} + +func vertexCmpFn(v1, v2 pgraph.Vertex) (bool, error) { + if v1.String() == "" || v2.String() == "" { + return false, fmt.Errorf("oops, empty vertex") + } + + r1, r2 := v1.(resources.Res), v2.(resources.Res) + if !r1.Compare(r2) { + //fmt.Printf("r1: %+v\n", *(r1.(*resources.TestRes).Int64Ptr)) + //fmt.Printf("r2: %+v\n", *(r2.(*resources.TestRes).Int64Ptr)) + return false, nil + } + + return v1.String() == v2.String(), nil +} + +func edgeCmpFn(e1, e2 pgraph.Edge) (bool, error) { + if e1.String() == "" || e2.String() == "" { + return false, fmt.Errorf("oops, empty edge") + } + return e1.String() == e2.String(), nil +} + +func runInterpret(code string) (*pgraph.Graph, error) { + str := strings.NewReader(code) + lang := &Lang{ + Input: str, // string as an interface that satisfies io.Reader + Debug: true, + } + if err := lang.Init(); err != nil { + return nil, errwrap.Wrapf(err, "init failed") + } + closeFn := func() error { + return errwrap.Wrapf(lang.Close(), "close failed") + } + + select { + case err, ok := <-lang.Stream(): + if !ok { + return nil, errwrap.Wrapf(closeFn(), "stream closed without event") + } + if err != nil { + return nil, errwrap.Wrapf(err, "stream failed, close: %+v", closeFn()) + } + } + + // run artificially without the entire engine + graph, err := lang.Interpret() + if err != nil { + err := errwrap.Wrapf(err, "interpret failed") + if e := closeFn(); e != nil { + err = multierr.Append(err, e) // list of errors + } + return nil, err + } + + return graph, closeFn() +} + +func TestInterpret0(t *testing.T) { + code := `` + graph, err := runInterpret(code) + if err != nil { + t.Errorf("runInterpret failed: %+v", err) + return + } + + expected := &pgraph.Graph{} + + runGraphCmp(t, graph, expected) +} + +func TestInterpret1(t *testing.T) { + code := `noop "n1" {}` + graph, err := runInterpret(code) + if err != nil { + t.Errorf("runInterpret failed: %+v", err) + return + } + + n1, _ := resources.NewNamedResource("noop", "n1") + + expected := &pgraph.Graph{} + expected.AddVertex(n1) + + runGraphCmp(t, graph, expected) +} + +func TestInterpret2(t *testing.T) { + code := ` + noop "n1" {} + noop "n2" {} + ` + graph, err := runInterpret(code) + if err != nil { + t.Errorf("runInterpret failed: %+v", err) + return + } + + n1, _ := resources.NewNamedResource("noop", "n1") + n2, _ := resources.NewNamedResource("noop", "n2") + + expected := &pgraph.Graph{} + expected.AddVertex(n1) + expected.AddVertex(n2) + + runGraphCmp(t, graph, expected) +} + +func TestInterpret3(t *testing.T) { + // should overflow int8 + code := ` + test "t1" { + int8 => 88888888, + } + ` + _, err := runInterpret(code) + if err == nil { + t.Errorf("expected overflow failure, but it passed") + } +} + +func TestInterpret4(t *testing.T) { + // str => " !#$%&'()*+,-./0123456790:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_abcdefghijklmnopqrstuvwxyz{|}~", + code := ` + # comment 1 + test "t1" { # comment 2 + stringptr => " !\"#$%&'()*+,-./0123456790:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_abcdefghijklmnopqrstuvwxyz{|}~", + int64 => 42, + boolptr => true, + # comment 3 + stringptr => "the actual field name is: StringPtr", # comment 4 + int8ptr => 99, # comment 5 + comment => "☺\thello\u263a\nwo\"rld\\2\u263a", # must escape these + } + ` + graph, err := runInterpret(code) + if err != nil { + t.Errorf("runInterpret failed: %+v", err) + return + } + + t1, _ := resources.NewNamedResource("test", "t1") + x := t1.(*resources.TestRes) + str := " !\"#$%&'()*+,-./0123456790:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_abcdefghijklmnopqrstuvwxyz{|}~" + x.StringPtr = &str + x.Int64 = 42 + b := true + x.BoolPtr = &b + stringptr := "the actual field name is: StringPtr" + x.StringPtr = &stringptr + int8ptr := int8(99) + x.Int8Ptr = &int8ptr + x.Comment = "☺\thello☺\nwo\"rld\\2\u263a" // must escape the escaped chars + + expected := &pgraph.Graph{} + expected.AddVertex(x) + + runGraphCmp(t, graph, expected) +} + +func TestInterpret5(t *testing.T) { + code := ` + if true { + test "t1" { + int64 => 42, + stringptr => "hello!", + } + } + ` + graph, err := runInterpret(code) + if err != nil { + t.Errorf("runInterpret failed: %+v", err) + return + } + + t1, _ := resources.NewNamedResource("test", "t1") + x := t1.(*resources.TestRes) + x.Int64 = 42 + str := "hello!" + x.StringPtr = &str + + expected := &pgraph.Graph{} + expected.AddVertex(x) + + runGraphCmp(t, graph, expected) +} + +func TestInterpret6(t *testing.T) { + code := ` + $b = true + if $b { + test "t1" { + int64 => 42, + stringptr => "hello", + } + } + if $b { + test "t2" { + int64 => 13, + stringptr => "world", + } + } + ` + graph, err := runInterpret(code) + if err != nil { + t.Errorf("runInterpret failed: %+v", err) + return + } + + expected := &pgraph.Graph{} + + { + r, _ := resources.NewNamedResource("test", "t1") + x := r.(*resources.TestRes) + x.Int64 = 42 + str := "hello" + x.StringPtr = &str + expected.AddVertex(x) + } + { + r, _ := resources.NewNamedResource("test", "t2") + x := r.(*resources.TestRes) + x.Int64 = 13 + str := "world" + x.StringPtr = &str + expected.AddVertex(x) + } + + runGraphCmp(t, graph, expected) +} + +func TestInterpretMany(t *testing.T) { + type test struct { // an individual test + name string + code string + fail bool + graph *pgraph.Graph + } + values := []test{} + + { + graph, _ := pgraph.NewGraph("g") + values = append(values, test{ // 0 + "nil", + ``, + false, + graph, + }) + } + { + graph, _ := pgraph.NewGraph("g") + values = append(values, test{ // 1 + name: "empty", + code: ``, + fail: false, + graph: graph, + }) + } + { + graph, _ := pgraph.NewGraph("g") + r, _ := resources.NewNamedResource("test", "t") + x := r.(*resources.TestRes) + i := int64(42 + 13) + x.Int64Ptr = &i + graph.AddVertex(x) + values = append(values, test{ + name: "simple addition", + code: ` + test "t" { + int64ptr => 42 + 13, + } + `, + fail: false, + graph: graph, + }) + } + { + graph, _ := pgraph.NewGraph("g") + r, _ := resources.NewNamedResource("test", "t") + x := r.(*resources.TestRes) + i := int64(42 + 13 + 99) + x.Int64Ptr = &i + graph.AddVertex(x) + values = append(values, test{ + name: "triple addition", + code: ` + test "t" { + int64ptr => 42 + 13 + 99, + } + `, + fail: false, + graph: graph, + }) + } + { + graph, _ := pgraph.NewGraph("g") + r, _ := resources.NewNamedResource("test", "t") + x := r.(*resources.TestRes) + i := int64(42 + 13 - 99) + x.Int64Ptr = &i + graph.AddVertex(x) + values = append(values, test{ + name: "triple addition/subtraction", + code: ` + test "t" { + int64ptr => 42 + 13 - 99, + } + `, + fail: false, + graph: graph, + }) + } + + for index, test := range values { // run all the tests + name, code, fail, exp := test.name, test.code, test.fail, test.graph + + if name == "" { + name = "" + } + + //if index != 3 { // hack to run a subset (useful for debugging) + //if test.name != "nil" { + // continue + //} + + t.Logf("\n\ntest #%d (%s) ----------------\n\n", index, name) + + graph, err := runInterpret(code) + if !fail && err != nil { + t.Errorf("test #%d: FAIL", index) + t.Errorf("test #%d: runInterpret failed with: %+v", index, err) + continue + } + if fail && err == nil { + t.Errorf("test #%d: FAIL", index) + t.Errorf("test #%d: runInterpret passed, expected fail", index) + continue + } + + if fail { // can't process graph if it's nil + continue + } + + t.Logf("test #%d: graph: %+v", index, graph) + // TODO: improve: https://github.com/purpleidea/mgmt/issues/199 + if err := graph.GraphCmp(exp, vertexCmpFn, edgeCmpFn); err != nil { + t.Errorf("test #%d: FAIL", index) + t.Logf("test #%d: actual: %v%s", index, graph, fullPrint(graph)) + t.Logf("test #%d: expected: %v%s", index, exp, fullPrint(exp)) + t.Errorf("test #%d: cmp error:\n%v", index, err) + continue + } + } +} diff --git a/lang/lexer.nex b/lang/lexer.nex new file mode 100644 index 00000000..13ca8268 --- /dev/null +++ b/lang/lexer.nex @@ -0,0 +1,351 @@ +/[ \t\n]/ { /* skip over whitespace */ } +/{/ { + yylex.pos(lval) // our pos + lval.str = yylex.Text() + return OPEN_CURLY + } +/}/ { + yylex.pos(lval) // our pos + lval.str = yylex.Text() + return CLOSE_CURLY + } +/\(/ { + yylex.pos(lval) // our pos + lval.str = yylex.Text() + return OPEN_PAREN + } +/\)/ { + yylex.pos(lval) // our pos + lval.str = yylex.Text() + return CLOSE_PAREN + } +/\[/ { + yylex.pos(lval) // our pos + lval.str = yylex.Text() + return OPEN_BRACK + } +/\]/ { + yylex.pos(lval) // our pos + lval.str = yylex.Text() + return CLOSE_BRACK + } +/if/ { + yylex.pos(lval) // our pos + lval.str = yylex.Text() + return IF + } +/else/ { + yylex.pos(lval) // our pos + lval.str = yylex.Text() + return ELSE + } +/=>/ { + yylex.pos(lval) // our pos + lval.str = yylex.Text() + return ROCKET + } +/,/ { + yylex.pos(lval) // our pos + lval.str = yylex.Text() + return COMMA + } +/:/ { + yylex.pos(lval) // our pos + lval.str = yylex.Text() + return COLON + } +/;/ { + yylex.pos(lval) // our pos + lval.str = yylex.Text() + return SEMICOLON + } +/=/ { + yylex.pos(lval) // our pos + lval.str = yylex.Text() + return EQUALS + } +/\+/ { + yylex.pos(lval) // our pos + lval.str = yylex.Text() + return PLUS + } +/\-/ { + yylex.pos(lval) // our pos + lval.str = yylex.Text() + return MINUS + } +/\*/ { + yylex.pos(lval) // our pos + lval.str = yylex.Text() + return MULTIPLY + } +/\// { + yylex.pos(lval) // our pos + lval.str = yylex.Text() + return DIVIDE + } +/==/ { + yylex.pos(lval) // our pos + lval.str = yylex.Text() + return EQ + } +/!=/ { + yylex.pos(lval) // our pos + lval.str = yylex.Text() + return NEQ + } +// { + yylex.pos(lval) // our pos + lval.str = yylex.Text() + return GT + } +/<=/ { + yylex.pos(lval) // our pos + lval.str = yylex.Text() + return LTE + } +/>=/ { + yylex.pos(lval) // our pos + lval.str = yylex.Text() + return GTE + } +/&&/ { + yylex.pos(lval) // our pos + lval.str = yylex.Text() + return AND + } +/\|\|/ { + yylex.pos(lval) // our pos + lval.str = yylex.Text() + return OR + } +/!/ { + yylex.pos(lval) // our pos + lval.str = yylex.Text() + return NOT + } +/in/ { + yylex.pos(lval) // our pos + lval.str = yylex.Text() + return IN + } +/bool/ { + yylex.pos(lval) // our pos + lval.str = yylex.Text() + return BOOL_IDENTIFIER + } +/str/ { + yylex.pos(lval) // our pos + lval.str = yylex.Text() + return STR_IDENTIFIER + } +/int/ { + yylex.pos(lval) // our pos + lval.str = yylex.Text() + return INT_IDENTIFIER + } +/float/ { + yylex.pos(lval) // our pos + lval.str = yylex.Text() + return FLOAT_IDENTIFIER + } +/struct/ { + yylex.pos(lval) // our pos + lval.str = yylex.Text() + return STRUCT_IDENTIFIER + } +/variant/ { + yylex.pos(lval) // our pos + lval.str = yylex.Text() + return VARIANT_IDENTIFIER + } +/true|false/ { + yylex.pos(lval) // our pos + s := yylex.Text() + if s == "true" { + lval.bool = true + } else if s == "false" { + lval.bool = false + } else { + // the lexer was wrong + panic(fmt.Sprintf("error lexing BOOL, got: %s", s)) + } + return BOOL + } +/"(\\.|[^"])*"/ + { // This matches any number of the bracketed patterns + // that are surrounded by the two quotes on each side. + // The bracket pattern is any escaped char or something + // that is not a single quote char. See this reference: + // https://www.lysator.liu.se/c/ANSI-C-grammar-l.html#STRING-LITERAL + // old: /"[\a\b\t\n\v\f\r !#$%&'()*+,-.\/0-9:;<=>?@A-Z\[\\\]^_a-z{|}~]*"/ + + yylex.pos(lval) // our pos + s := yylex.Text() + + x, err := strconv.Unquote(s) // expects quote wrapping! + if err == nil { + lval.str = x // it comes out all cleanly escaped + } else if err == strconv.ErrSyntax { + // this catches improper escape codes or lone \ + lp := yylex.cast() + lp.lexerErr = &LexParseErr{ + Err: ErrLexerStringBadEscaping, + Str: s, + Row: yylex.Line(), + Col: yylex.Column(), + } + return ERROR + } else if err != nil { + // unhandled error + panic(fmt.Sprintf("error lexing STRING, got: %v", err)) + } else { // no chars to escape found + lval.str = s[1:len(s)-1] // remove the two quotes + } + + return STRING + } +/\-?[0-9]+/ + { + yylex.pos(lval) // our pos + s := yylex.Text() + var err error + lval.int, err = strconv.ParseInt(s, 10, 64) // int64 + if err == nil { + return INTEGER + } else if e := err.(*strconv.NumError); e.Err == strconv.ErrRange { + // this catches range errors for very large ints + lp := yylex.cast() + lp.lexerErr = &LexParseErr{ + Err: ErrLexerIntegerOverflow, + Str: s, + Row: yylex.Line(), + Col: yylex.Column(), + } + return ERROR + } else { + panic(fmt.Sprintf("error lexing INTEGER, got: %v", err)) + } + } +/\-?[0-9]+\.[0-9]+/ + { + yylex.pos(lval) // our pos + s := yylex.Text() + var err error + lval.float, err = strconv.ParseFloat(s, 64) // float64 + if err == nil { + return FLOAT + } else if e := err.(*strconv.NumError); e.Err == strconv.ErrRange { + // this catches range errors for very large floats + lp := yylex.cast() + lp.lexerErr = &LexParseErr{ + Err: ErrLexerFloatOverflow, + Str: s, + Row: yylex.Line(), + Col: yylex.Column(), + } + return ERROR + } else { + panic(fmt.Sprintf("error lexing FLOAT, got: %v", err)) + } + } +/\$[a-z][a-z0-9]*{[0-9]+}/ + { + // we have this as a single token, because otherwise the + // parser can get confused by the curly brackets :/ + yylex.pos(lval) // our pos + s := yylex.Text() + s = s[1:len(s)] // remove the leading $ + s = s[0:len(s)-1] // remove the trailing close curly + // XXX: nex has a bug that it gets confused by the + // following single curly brace. Please see: + // https://github.com/blynn/nex/issues/48 + a := strings.Split(s, "{") // XXX: close match here: } + if len(a) != 2 { + panic(fmt.Sprintf("error lexing VAR_IDENTIFIER_HX: %v", a)) + } + lval.str = a[0] + var err error + lval.int, err = strconv.ParseInt(a[1], 10, 64) // int64 + if err == nil { + return VAR_IDENTIFIER_HX + } else if e := err.(*strconv.NumError); e.Err == strconv.ErrRange { + // this catches range errors for very large ints + lp := yylex.cast() + lp.lexerErr = &LexParseErr{ + Err: ErrLexerIntegerOverflow, + Str: a[1], + Row: yylex.Line(), + Col: yylex.Column(), + } + return ERROR + } else { + panic(fmt.Sprintf("error lexing VAR_IDENTIFIER_HX, got: %v", err)) + } + } +/\$[a-z][a-z0-9]*/ + { + yylex.pos(lval) // our pos + s := yylex.Text() + lval.str = s[1:len(s)] // remove the leading $ + return VAR_IDENTIFIER + } +/[a-z][a-z0-9]*/ + { + yylex.pos(lval) // our pos + lval.str = yylex.Text() + return IDENTIFIER + } +/#[^\n]*/ + { // this matches a (#) pound char followed by any + // number of chars that aren't the (\n) newline! + + yylex.pos(lval) // our pos + s := yylex.Text() + + lval.str = s[1:len(s)] // remove the leading # + //log.Printf("lang: lexer: comment: `%s`", lval.str) + //return COMMENT // skip return to avoid parsing + } +/./ { + yylex.pos(lval) // our pos + s := yylex.Text() + lp := yylex.cast() + lp.lexerErr = &LexParseErr{ + Err: ErrLexerUnrecognized, + Str: s, + Row: yylex.Line(), + Col: yylex.Column(), + } + return ERROR + } +// + +// Mgmt +// Copyright (C) 2013-2018+ 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 lang + +import ( + "fmt" + "strconv" +) diff --git a/lang/lexparse.go b/lang/lexparse.go new file mode 100644 index 00000000..a3e689ec --- /dev/null +++ b/lang/lexparse.go @@ -0,0 +1,80 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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 lang // TODO: move this into a sub package of lang/$name? + +import ( + "fmt" + "io" + + "github.com/purpleidea/mgmt/lang/interfaces" +) + +// These constants represent the different possible lexer/parser errors. +const ( + ErrLexerUnrecognized = interfaces.Error("unrecognized") + ErrLexerStringBadEscaping = interfaces.Error("string: bad escaping") + ErrLexerIntegerOverflow = interfaces.Error("integer: overflow") + ErrLexerFloatOverflow = interfaces.Error("float: overflow") + ErrParseError = interfaces.Error("parser") + ErrParseAdditionalEquals = interfaces.Error(errstrParseAdditionalEquals) + ErrParseExpectingComma = interfaces.Error(errstrParseExpectingComma) +) + +// LexParseErr is a permanent failure error to notify about borkage. +type LexParseErr struct { + Err interfaces.Error + Str string + Row int // this is zero-indexed (the first line is 0) + Col int // this is zero-indexed (the first char is 0) +} + +// Error displays this error with all the relevant state information. +func (e *LexParseErr) Error() string { + return fmt.Sprintf("%s: `%s` @%d:%d", e.Err, e.Str, e.Row+1, e.Col+1) +} + +// lexParseAST is a struct which we pass into the lexer/parser so that we have a +// location to store the AST to avoid having to use a global variable. +type lexParseAST struct { + ast interfaces.Stmt + + row int + col int + + lexerErr error // from lexer + parseErr error // from Error(e string) +} + +// LexParse runs the lexer/parser machinery and returns the AST. +func LexParse(input io.Reader) (interfaces.Stmt, error) { + lp := &lexParseAST{} + // parseResult is a seemingly unused field in the Lexer struct for us... + lexer := NewLexerWithInit(input, func(y *Lexer) { y.parseResult = lp }) + yyParse(lexer) // writes the result to lp.ast + var err error + if e := lp.parseErr; e != nil { + err = e + } + if e := lp.lexerErr; e != nil { + err = e + } + if err != nil { + return nil, err + } + return lp.ast, nil +} diff --git a/lang/lexparse_test.go b/lang/lexparse_test.go new file mode 100644 index 00000000..467a7a6d --- /dev/null +++ b/lang/lexparse_test.go @@ -0,0 +1,855 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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 lang + +import ( + "reflect" + "strings" + "testing" + + "github.com/purpleidea/mgmt/lang/interfaces" + + "github.com/davecgh/go-spew/spew" +) + +func TestLexParse0(t *testing.T) { + type test struct { // an individual test + name string + code string + fail bool + exp interfaces.Stmt + } + values := []test{} + + { + values = append(values, test{ + "nil", + ``, + false, + nil, + }) + } + { + values = append(values, test{ + name: "simple assignment", + code: `$rewsna = -42`, + fail: false, + exp: &StmtProg{ + Prog: []interfaces.Stmt{ + &StmtBind{ + Ident: "rewsna", + Value: &ExprInt{ + V: -42, + }, + }, + }, + }, + }) + } + { + values = append(values, test{ + name: "one res", + code: `noop "n1" {}`, + fail: false, + //exp: ???, // FIXME: add the expected AST + }) + } + { + values = append(values, test{ + name: "res with keyword", + code: `false "n1" {}`, // false is a special keyword + fail: true, + }) + } + { + values = append(values, test{ + name: "bad escaping", + code: ` + test "n1" { + str => "he\ llo", # incorrect escaping + } + `, + fail: true, + }) + } + { + values = append(values, test{ + name: "int overflow", + code: ` + test "n1" { + int => 888888888888888888888888, # overflows + } + `, + fail: true, + }) + } + { + values = append(values, test{ + name: "overflow after lexer", + code: ` + test "n1" { + uint8 => 128, # does not overflow at lexer stage + } + `, + fail: false, + //exp: ???, // FIXME: add the expected AST + }) + } + { + values = append(values, test{ + name: "one res", + code: ` + test "n1" { + int16 => 01134, # some comment + } + `, + fail: false, + //exp: ???, // FIXME: add the expected AST + }) + } + { + // TODO: skip trailing comma requirement on one-liners + values = append(values, test{ + name: "two lists", + code: ` + $somelist = [42, 0, -13,] + $somelonglist = [ + "hello", + "and", + "how", + "are", + "you?", + ] + `, + fail: false, + //exp: ???, // FIXME: add the expected AST + }) + } + { + values = append(values, test{ + name: "one map", + code: ` + $somemap = { + "foo" => "foo1", + "bar" => "bar1", + } + `, + fail: false, + //exp: ???, // FIXME: add the expected AST + }) + } + { + values = append(values, test{ + name: "another map", + code: ` + $somemap = { + "foo" => -13, + "bar" => 42, + } + `, + fail: false, + //exp: ???, // FIXME: add the expected AST + }) + } + // TODO: alternate possible syntax ? + //{ + // values = append(values, test{ // ? + // code: ` + // $somestruct = struct{ + // foo: "foo1"; + // bar: 42 # no trailing semicolon at the moment + // } + // `, + // fail: false, + // //exp: ???, // FIXME: add the expected AST + // }) + //} + { + values = append(values, test{ + name: "one struct", + code: ` + $somestruct = struct{ + foo => "foo1", + bar => 42, + } + `, + fail: false, + //exp: ???, // FIXME: add the expected AST + }) + } + { + values = append(values, test{ + name: "struct with nested struct", + code: ` + $somestruct = struct{ + foo => "foo1", + bar => struct{ + a => true, + b => "hello", + }, + baz => 42, + } + `, + fail: false, + //exp: ???, // FIXME: add the expected AST + }) + } + // types + { + values = append(values, test{ + name: "some lists", + code: ` + $intlist []int = [42, -0, 13,] + $intlistnested [][]int = [[42,], [], [100, -0,], [-13,],] + `, + fail: false, + //exp: ???, // FIXME: add the expected AST + }) + } + { + values = append(values, test{ + name: "maps and lists", + code: ` + $strmap {str: int} = { + "key1" => 42, + "key2" => -13, + } + $mapstrintlist {str: []int} = { + "key1" => [42, 44,], + "key2" => [], + "key3" => [-13,], + } + `, + fail: false, + //exp: ???, // FIXME: add the expected AST + }) + } + { + values = append(values, test{ + name: "some structs", + code: ` + $structx struct{a int; b bool; c str} = struct{ + a => 42, + b => true, + c => "hello", + } + $structx2 struct{a int; b []bool; c str} = struct{ + a => 42, + b => [true, false, false, true,], + c => "hello", + } + `, + fail: false, + //exp: ???, // FIXME: add the expected AST + }) + } + { + values = append(values, test{ + name: "res with floats", + code: ` + test "n1" { + float32 => -25.38789, # some float + float64 => 53.393908945, # some float + } + `, + fail: false, + //exp: ???, // FIXME: add the expected AST + }) + } + // FIXME: why doesn't this overflow, and thus fail? + // from the docs: If s is syntactically well-formed but is more than 1/2 + // ULP away from the largest floating point number of the given size, + // ParseFloat returns f = ±Inf, err.Err = ErrRange. + //{ + // values = append(values, test{ + // name: "overflowing float", + // code: ` + // test "n1" { + // float32 => -457643875645764387564578645457864525457643875645764387564578645457864525.457643875645764387564578645457864525387899898753459879587574928798759863965, # overflow + // } + // `, + // fail: true, + // }) + //} + { + values = append(values, test{ + name: "res and addition", + code: ` + test "n1" { + float32 => -25.38789 + 32.6, + } + `, + fail: false, + //exp: ???, // FIXME: add the expected AST + }) + } + { + exp := &StmtProg{ + Prog: []interfaces.Stmt{ + &StmtRes{ + Kind: "test", + Name: &ExprStr{ + V: "n1", + }, + Fields: []*StmtResField{ + { + Field: "int64ptr", + Value: &ExprCall{ + Name: operatorFuncName, + Args: []interfaces.Expr{ + &ExprStr{ + V: "+", + }, + &ExprInt{ + V: 13, + }, + &ExprInt{ + V: 42, + }, + }, + }, + }, + }, + }, + }, + } + values = append(values, test{ + name: "addition", + code: ` + test "n1" { + int64ptr => 13 + 42, + } + `, + fail: false, + exp: exp, + }) + } + { + exp := &StmtProg{ + Prog: []interfaces.Stmt{ + &StmtRes{ + Kind: "test", + Name: &ExprStr{ + V: "n1", + }, + Fields: []*StmtResField{ + { + Field: "float32", + Value: &ExprCall{ + Name: operatorFuncName, + Args: []interfaces.Expr{ + &ExprStr{ + V: "+", + }, + &ExprCall{ + Name: operatorFuncName, + Args: []interfaces.Expr{ + &ExprStr{ + V: "+", + }, + &ExprFloat{ + V: -25.38789, + }, + &ExprFloat{ + V: 32.6, + }, + }, + }, + &ExprFloat{ + V: 13.7, + }, + }, + }, + }, + }, + }, + }, + } + values = append(values, test{ + name: "multiple float addition", + code: ` + test "n1" { + float32 => -25.38789 + 32.6 + 13.7, + } + `, + fail: false, + exp: exp, + }) + } + { + exp := &StmtProg{ + Prog: []interfaces.Stmt{ + &StmtRes{ + Kind: "test", + Name: &ExprStr{ + V: "n1", + }, + Fields: []*StmtResField{ + { + Field: "int64ptr", + Value: &ExprCall{ + Name: operatorFuncName, + Args: []interfaces.Expr{ + &ExprStr{ + V: "+", + }, + &ExprInt{ + V: 4, + }, + &ExprCall{ + Name: operatorFuncName, + Args: []interfaces.Expr{ + &ExprStr{ + V: "*", + }, + &ExprInt{ + V: 3, + }, + &ExprInt{ + V: 12, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + values = append(values, test{ + name: "order of operations lucky", + code: ` + test "n1" { + int64ptr => 4 + 3 * 12, # 40, not 84 + } + `, + fail: false, + exp: exp, + }) + } + { + exp := &StmtProg{ + Prog: []interfaces.Stmt{ + &StmtRes{ + Kind: "test", + Name: &ExprStr{ + V: "n1", + }, + Fields: []*StmtResField{ + { + Field: "int64ptr", + Value: &ExprCall{ + Name: operatorFuncName, + Args: []interfaces.Expr{ + &ExprStr{ + V: "+", + }, + &ExprCall{ + Name: operatorFuncName, + Args: []interfaces.Expr{ + &ExprStr{ + V: "*", + }, + &ExprInt{ + V: 3, + }, + &ExprInt{ + V: 12, + }, + }, + }, + &ExprInt{ + V: 4, + }, + }, + }, + }, + }, + }, + }, + } + values = append(values, test{ + name: "order of operations needs left precedence", + code: ` + test "n1" { + int64ptr => 3 * 12 + 4, # 40, not 48 + } + `, + fail: false, + exp: exp, + }) + } + { + exp := &StmtProg{ + Prog: []interfaces.Stmt{ + &StmtRes{ + Kind: "test", + Name: &ExprStr{ + V: "n1", + }, + Fields: []*StmtResField{ + { + Field: "int64ptr", + Value: &ExprCall{ + Name: operatorFuncName, + Args: []interfaces.Expr{ + &ExprStr{ + V: "*", + }, + &ExprInt{ + V: 3, + }, + &ExprCall{ + Name: operatorFuncName, + Args: []interfaces.Expr{ + &ExprStr{ + V: "+", + }, + &ExprInt{ + V: 12, + }, + &ExprInt{ + V: 4, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + values = append(values, test{ + name: "order of operations parens", + code: ` + test "n1" { + int64ptr => 3 * (12 + 4), # 48, not 40 + } + `, + fail: false, + exp: exp, + }) + } + { + exp := &StmtProg{ + Prog: []interfaces.Stmt{ + &StmtRes{ + Kind: "test", + Name: &ExprStr{ + V: "n1", + }, + Fields: []*StmtResField{ + { + Field: "boolptr", + Value: &ExprCall{ + Name: operatorFuncName, + Args: []interfaces.Expr{ + &ExprStr{ + V: ">", + }, + &ExprCall{ + Name: operatorFuncName, + Args: []interfaces.Expr{ + &ExprStr{ + V: "+", + }, + &ExprInt{ + V: 3, + }, + &ExprInt{ + V: 4, + }, + }, + }, + &ExprInt{ + V: 5, + }, + }, + }, + }, + }, + }, + }, + } + values = append(values, test{ + name: "order of operations bools", + code: ` + test "n1" { + boolptr => 3 + 4 > 5, # should be true + } + `, + fail: false, + exp: exp, + }) + } + { + exp := &StmtProg{ + Prog: []interfaces.Stmt{ + &StmtRes{ + Kind: "test", + Name: &ExprStr{ + V: "n1", + }, + Fields: []*StmtResField{ + { + Field: "boolptr", + Value: &ExprCall{ + Name: operatorFuncName, + Args: []interfaces.Expr{ + &ExprStr{ + V: ">", + }, + &ExprInt{ + V: 3, + }, + &ExprCall{ + Name: operatorFuncName, + Args: []interfaces.Expr{ + &ExprStr{ + V: "+", + }, + &ExprInt{ + V: 4, + }, + &ExprInt{ + V: 5, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + values = append(values, test{ + name: "order of operations bools reversed", + code: ` + test "n1" { + boolptr => 3 > 4 + 5, # should be false + } + `, + fail: false, + exp: exp, + }) + } + { + exp := &StmtProg{ + Prog: []interfaces.Stmt{ + &StmtRes{ + Kind: "test", + Name: &ExprStr{ + V: "n1", + }, + Fields: []*StmtResField{ + { + Field: "boolptr", + Value: &ExprCall{ + Name: operatorFuncName, + Args: []interfaces.Expr{ + &ExprStr{ + V: ">", + }, + &ExprCall{ + Name: operatorFuncName, + Args: []interfaces.Expr{ + &ExprStr{ + V: "!", + }, + &ExprInt{ + V: 3, + }, + }, + }, + &ExprInt{ + V: 4, + }, + }, + }, + }, + }, + }, + }, + } + values = append(values, test{ + name: "order of operations with not", + code: ` + test "n1" { + boolptr => ! 3 > 4, # should parse, but not compile + } + `, + fail: false, + exp: exp, + }) + } + { + exp := &StmtProg{ + Prog: []interfaces.Stmt{ + &StmtRes{ + Kind: "test", + Name: &ExprStr{ + V: "n1", + }, + Fields: []*StmtResField{ + { + Field: "boolptr", + Value: &ExprCall{ + Name: operatorFuncName, + Args: []interfaces.Expr{ + &ExprStr{ + V: "&&", + }, + &ExprCall{ + Name: operatorFuncName, + Args: []interfaces.Expr{ + &ExprStr{ + V: "<", + }, + &ExprInt{ + V: 7, + }, + &ExprInt{ + V: 4, + }, + }, + }, + &ExprBool{ + V: true, + }, + }, + }, + }, + }, + }, + }, + } + values = append(values, test{ + name: "order of operations logical", + code: ` + test "n1" { + boolptr => 7 < 4 && true, # should be false + } + `, + fail: false, + exp: exp, + }) + } + + for index, test := range values { // run all the tests + name, code, fail, exp := test.name, test.code, test.fail, test.exp + + if name == "" { + name = "" + } + + //if index != 3 { // hack to run a subset (useful for debugging) + //if (index != 20 && index != 21) { + //if test.name != "nil" { + // continue + //} + + t.Logf("\n\ntest #%d (%s) ----------------\n\n", index, name) + + str := strings.NewReader(code) + ast, err := LexParse(str) + + if !fail && err != nil { + t.Errorf("test #%d: lex/parse failed with: %+v", index, err) + continue + } + if fail && err == nil { + t.Errorf("test #%d: lex/parse passed, expected fail", index) + continue + } + + if !fail && ast == nil { + t.Errorf("test #%d: lex/parse was nil", index) + continue + } + + if exp != nil { + if !reflect.DeepEqual(ast, exp) { + t.Errorf("test #%d: AST did not match expected", index) + // TODO: consider making our own recursive print function + t.Logf("test #%d: actual: \n%s", index, spew.Sdump(ast)) + t.Logf("test #%d: expected: \n%s", index, spew.Sdump(exp)) + continue + } + } + } +} + +func TestLexParse1(t *testing.T) { + code := ` + $a = 42 + $b = true + $c = 13 + $d = "hello" + $e = true + $f = 3.13 + # some noop resource + noop "n0" { + foo => true, + bar => false # this should be a parser error (no comma) + } + # hello + # world + test "n1" {} + ` // error + str := strings.NewReader(code) + _, err := LexParse(str) + if e, ok := err.(*LexParseErr); ok && e.Err != ErrParseExpectingComma { + t.Errorf("lex/parse failure, got: %+v", e) + } else if err == nil { + t.Errorf("lex/parse success, expected error") + } else { + if e.Row != 10 || e.Col != 9 { + t.Errorf("expected error at 10 x 9, got: %d x %d", e.Row, e.Col) + } + t.Logf("row x col: %d x %d", e.Row, e.Col) + t.Logf("message: %s", e.Str) + t.Logf("output: %+v", err) + } +} + +func TestLexParse2(t *testing.T) { + code := ` + $a == 13 + test "t1" { + int8 => $a, + } + ` // error, assignment is a single equals, not two + str := strings.NewReader(code) + _, err := LexParse(str) + if e, ok := err.(*LexParseErr); ok && e.Err != ErrParseAdditionalEquals { + t.Errorf("lex/parse failure, got: %+v", e) + } else if err == nil { + t.Errorf("lex/parse success, expected error") + } else { + // TODO: when this is accurate, pick values and enable this! + //if e.Row != 8 || e.Col != 2 { + // t.Errorf("expected error at 8 x 2, got: %d x %d", e.Row, e.Col) + //} + t.Logf("row x col: %d x %d", e.Row, e.Col) + t.Logf("message: %s", e.Str) + t.Logf("output: %+v", err) + } +} diff --git a/lang/parser.y b/lang/parser.y new file mode 100644 index 00000000..804d1c14 --- /dev/null +++ b/lang/parser.y @@ -0,0 +1,819 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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 lang + +import ( + "fmt" + "strings" + + "github.com/purpleidea/mgmt/lang/interfaces" + "github.com/purpleidea/mgmt/lang/types" +) + +const ( + errstrParseAdditionalEquals = "additional equals in bind statement" + errstrParseExpectingComma = "expecting trailing comma" +) + +func init() { + yyErrorVerbose = true // set the global that enables showing full errors +} +%} + +%union { + row int + col int + + //err error // TODO: if we ever match ERROR in the parser + + bool bool + str string + int int64 // this is the .int as seen in lexer.nex + float float64 + + strSlice []string + + typ *types.Type + + stmts []interfaces.Stmt + stmt interfaces.Stmt + + exprs []interfaces.Expr + expr interfaces.Expr + + mapKVs []*ExprMapKV + mapKV *ExprMapKV + + structFields []*ExprStructField + structField *ExprStructField + + resFields []*StmtResField + resField *StmtResField +} + +%token OPEN_CURLY CLOSE_CURLY +%token OPEN_PAREN CLOSE_PAREN +%token OPEN_BRACK CLOSE_BRACK +%token IF ELSE +%token STRING BOOL INTEGER FLOAT +%token EQUALS +%token COMMA COLON SEMICOLON +%token ROCKET +%token STR_IDENTIFIER BOOL_IDENTIFIER INT_IDENTIFIER FLOAT_IDENTIFIER +%token STRUCT_IDENTIFIER VARIANT_IDENTIFIER VAR_IDENTIFIER IDENTIFIER +%token VAR_IDENTIFIER_HX +%token COMMENT ERROR + +// precedence table +// "Operator precedence is determined by the line ordering of the declarations; +// the higher the line number of the declaration (lower on the page or screen), +// the higher the precedence." +// From: https://www.gnu.org/software/bison/manual/html_node/Infix-Calc.html +// FIXME: a yacc specialist should check the precedence and add more tests! +%left AND OR +%nonassoc LT GT LTE GTE EQ NEQ // TODO: is %nonassoc correct for all of these? +%left PLUS MINUS +%left MULTIPLY DIVIDE +%right NOT +//%right EXP // exponentiation +%nonassoc IN // TODO: is %nonassoc correct for this? + +%error IDENTIFIER STRING OPEN_CURLY IDENTIFIER ROCKET BOOL CLOSE_CURLY: errstrParseExpectingComma +%error IDENTIFIER STRING OPEN_CURLY IDENTIFIER ROCKET STRING CLOSE_CURLY: errstrParseExpectingComma +%error IDENTIFIER STRING OPEN_CURLY IDENTIFIER ROCKET INTEGER CLOSE_CURLY: errstrParseExpectingComma +%error IDENTIFIER STRING OPEN_CURLY IDENTIFIER ROCKET FLOAT CLOSE_CURLY: errstrParseExpectingComma + +%error VAR_IDENTIFIER EQ BOOL: errstrParseAdditionalEquals +%error VAR_IDENTIFIER EQ STRING: errstrParseAdditionalEquals +%error VAR_IDENTIFIER EQ INTEGER: errstrParseAdditionalEquals +%error VAR_IDENTIFIER EQ FLOAT: errstrParseAdditionalEquals + +%% +top: + prog + { + posLast(yylex, yyDollar) // our pos + // store the AST in the struct that we previously passed in + lp := cast(yylex) + lp.ast = $1.stmt + // this is equivalent to: + //lp := yylex.(*Lexer).parseResult + //lp.(*lexParseAST).ast = $1.stmt + } +; +prog: + /* end of list */ + { + posLast(yylex, yyDollar) // our pos + $$.stmt = &StmtProg{ + Prog: []interfaces.Stmt{}, + } + } +| prog stmt + { + posLast(yylex, yyDollar) // our pos + // TODO: should we just skip comments for now? + //if _, ok := $2.stmt.(*StmtComment); !ok { + //} + if stmt, ok := $1.stmt.(*StmtProg); ok { + stmts := stmt.Prog + stmts = append(stmts, $2.stmt) + $$.stmt = &StmtProg{ + Prog: stmts, + } + } + } +; +stmt: + COMMENT + { + posLast(yylex, yyDollar) // our pos + $$.stmt = &StmtComment{ + Value: $1.str, + } + } +| bind + { + posLast(yylex, yyDollar) // our pos + $$.stmt = $1.stmt + } +| resource + { + posLast(yylex, yyDollar) // our pos + $$.stmt = $1.stmt + } +| IF expr OPEN_CURLY prog CLOSE_CURLY + { + posLast(yylex, yyDollar) // our pos + $$.stmt = &StmtIf{ + Condition: $2.expr, + ThenBranch: $4.stmt, + //ElseBranch: nil, + } + } +| IF expr OPEN_CURLY prog CLOSE_CURLY ELSE OPEN_CURLY prog CLOSE_CURLY + { + posLast(yylex, yyDollar) // our pos + $$.stmt = &StmtIf{ + Condition: $2.expr, + ThenBranch: $4.stmt, + ElseBranch: $8.stmt, + } + } +/* + // resource bind +| rbind + { + posLast(yylex, yyDollar) // our pos + $$.stmt = $1.stmt + } +*/ +; +expr: + BOOL + { + posLast(yylex, yyDollar) // our pos + $$.expr = &ExprBool{ + V: $1.bool, + } + } +| STRING + { + posLast(yylex, yyDollar) // our pos + $$.expr = &ExprStr{ + V: $1.str, + } + } +| INTEGER + { + posLast(yylex, yyDollar) // our pos + $$.expr = &ExprInt{ + V: $1.int, + } + } +| FLOAT + { + posLast(yylex, yyDollar) // our pos + $$.expr = &ExprFloat{ + V: $1.float, + } + } +| list + { + posLast(yylex, yyDollar) // our pos + // TODO: list could be squashed in here directly... + $$.expr = $1.expr + } +| map + { + posLast(yylex, yyDollar) // our pos + // TODO: map could be squashed in here directly... + $$.expr = $1.expr + } +| struct + { + posLast(yylex, yyDollar) // our pos + // TODO: struct could be squashed in here directly... + $$.expr = $1.expr + } +| call + { + posLast(yylex, yyDollar) // our pos + // TODO: call could be squashed in here directly... + $$.expr = $1.expr + } +| var + { + posLast(yylex, yyDollar) // our pos + // TODO: var could be squashed in here directly... + $$.expr = $1.expr + } +| IF expr OPEN_CURLY expr CLOSE_CURLY ELSE OPEN_CURLY expr CLOSE_CURLY + { + posLast(yylex, yyDollar) // our pos + $$.expr = &ExprIf{ + Condition: $2.expr, + ThenBranch: $4.expr, + ElseBranch: $8.expr, + } + } + // parenthesis wrap an expression for precedence +| OPEN_PAREN expr CLOSE_PAREN + { + posLast(yylex, yyDollar) // our pos + $$.expr = $2.expr + } +; +list: + // `[42, 0, -13]` + OPEN_BRACK list_elements CLOSE_BRACK + { + posLast(yylex, yyDollar) // our pos + $$.expr = &ExprList{ + Elements: $2.exprs, + } + } +; +list_elements: + /* end of list */ + { + posLast(yylex, yyDollar) // our pos + $$.exprs = []interfaces.Expr{} + } +| list_elements list_element + { + posLast(yylex, yyDollar) // our pos + $$.exprs = append($1.exprs, $2.expr) + } +; +list_element: + expr COMMA + { + posLast(yylex, yyDollar) // our pos + $$.expr = $1.expr + } +; +map: + // `{"hello" => "there", "world" => "big", }` + OPEN_CURLY map_kvs CLOSE_CURLY + { + posLast(yylex, yyDollar) // our pos + $$.expr = &ExprMap{ + KVs: $2.mapKVs, + } + } +; +map_kvs: + /* end of list */ + { + posLast(yylex, yyDollar) // our pos + $$.mapKVs = []*ExprMapKV{} + } +| map_kvs map_kv + { + posLast(yylex, yyDollar) // our pos + $$.mapKVs = append($1.mapKVs, $2.mapKV) + } +; +map_kv: + expr ROCKET expr COMMA + { + posLast(yylex, yyDollar) // our pos + $$.mapKV = &ExprMapKV{ + Key: $1.expr, + Val: $3.expr, + } + } +; +struct: + // `struct{answer => 0, truth => false, hello => "world",}` + STRUCT_IDENTIFIER OPEN_CURLY struct_fields CLOSE_CURLY + { + posLast(yylex, yyDollar) // our pos + $$.expr = &ExprStruct{ + Fields: $3.structFields, + } + } +; +struct_fields: + /* end of list */ + { + posLast(yylex, yyDollar) // our pos + $$.structFields = []*ExprStructField{} + } +| struct_fields struct_field + { + posLast(yylex, yyDollar) // our pos + $$.structFields = append($1.structFields, $2.structField) + } +; +struct_field: + IDENTIFIER ROCKET expr COMMA + { + posLast(yylex, yyDollar) // our pos + $$.structField = &ExprStructField{ + Name: $1.str, + Value: $3.expr, + } + } +; +call: + IDENTIFIER OPEN_PAREN call_args CLOSE_PAREN + { + posLast(yylex, yyDollar) // our pos + $$.expr = &ExprCall{ + Name: $1.str, + Args: $3.exprs, + } + } +| expr PLUS expr + { + posLast(yylex, yyDollar) // our pos + $$.expr = &ExprCall{ + Name: operatorFuncName, + Args: []interfaces.Expr{ + &ExprStr{ // operator first + V: $2.str, // for PLUS this is a `+` character + }, + $1.expr, + $3.expr, + }, + } + } +| expr MINUS expr + { + posLast(yylex, yyDollar) // our pos + $$.expr = &ExprCall{ + Name: operatorFuncName, + Args: []interfaces.Expr{ + &ExprStr{ // operator first + V: $2.str, + }, + $1.expr, + $3.expr, + }, + } + } +| expr MULTIPLY expr + { + posLast(yylex, yyDollar) // our pos + $$.expr = &ExprCall{ + Name: operatorFuncName, + Args: []interfaces.Expr{ + &ExprStr{ // operator first + V: $2.str, + }, + $1.expr, + $3.expr, + }, + } + } +| expr DIVIDE expr + { + posLast(yylex, yyDollar) // our pos + $$.expr = &ExprCall{ + Name: operatorFuncName, + Args: []interfaces.Expr{ + &ExprStr{ // operator first + V: $2.str, + }, + $1.expr, + $3.expr, + }, + } + } +| expr EQ expr + { + posLast(yylex, yyDollar) // our pos + $$.expr = &ExprCall{ + Name: operatorFuncName, + Args: []interfaces.Expr{ + &ExprStr{ // operator first + V: $2.str, + }, + $1.expr, + $3.expr, + }, + } + } +| expr NEQ expr + { + posLast(yylex, yyDollar) // our pos + $$.expr = &ExprCall{ + Name: operatorFuncName, + Args: []interfaces.Expr{ + &ExprStr{ // operator first + V: $2.str, + }, + $1.expr, + $3.expr, + }, + } + } +| expr LT expr + { + posLast(yylex, yyDollar) // our pos + $$.expr = &ExprCall{ + Name: operatorFuncName, + Args: []interfaces.Expr{ + &ExprStr{ // operator first + V: $2.str, + }, + $1.expr, + $3.expr, + }, + } + } +| expr GT expr + { + posLast(yylex, yyDollar) // our pos + $$.expr = &ExprCall{ + Name: operatorFuncName, + Args: []interfaces.Expr{ + &ExprStr{ // operator first + V: $2.str, + }, + $1.expr, + $3.expr, + }, + } + } +| expr LTE expr + { + posLast(yylex, yyDollar) // our pos + $$.expr = &ExprCall{ + Name: operatorFuncName, + Args: []interfaces.Expr{ + &ExprStr{ // operator first + V: $2.str, + }, + $1.expr, + $3.expr, + }, + } + } +| expr GTE expr + { + posLast(yylex, yyDollar) // our pos + $$.expr = &ExprCall{ + Name: operatorFuncName, + Args: []interfaces.Expr{ + &ExprStr{ // operator first + V: $2.str, + }, + $1.expr, + $3.expr, + }, + } + } +| expr AND expr + { + posLast(yylex, yyDollar) // our pos + $$.expr = &ExprCall{ + Name: operatorFuncName, + Args: []interfaces.Expr{ + &ExprStr{ // operator first + V: $2.str, + }, + $1.expr, + $3.expr, + }, + } + } +| expr OR expr + { + posLast(yylex, yyDollar) // our pos + $$.expr = &ExprCall{ + Name: operatorFuncName, + Args: []interfaces.Expr{ + &ExprStr{ // operator first + V: $2.str, + }, + $1.expr, + $3.expr, + }, + } + } +| NOT expr + { + posLast(yylex, yyDollar) // our pos + $$.expr = &ExprCall{ + Name: operatorFuncName, + Args: []interfaces.Expr{ + &ExprStr{ // operator first + V: $1.str, + }, + $2.expr, + }, + } + } +| VAR_IDENTIFIER_HX + // get the N-th historical value, eg: $foo{3} is equivalent to: history($foo, 3) + { + posLast(yylex, yyDollar) // our pos + $$.expr = &ExprCall{ + Name: historyFuncName, + Args: []interfaces.Expr{ + &ExprVar{ + Name: $1.str, + }, + &ExprInt{ + V: $1.int, + }, + }, + } + } +//| VAR_IDENTIFIER OPEN_CURLY INTEGER CLOSE_CURLY +// { +// posLast(yylex, yyDollar) // our pos +// $$.expr = &ExprCall{ +// Name: historyFuncName, +// Args: []interfaces.Expr{ +// &ExprVar{ +// Name: $1.str, +// }, +// &ExprInt{ +// V: $3.int, +// }, +// }, +// } +// } +| expr IN expr + { + posLast(yylex, yyDollar) // our pos + $$.expr = &ExprCall{ + Name: containsFuncName, + Args: []interfaces.Expr{ + $1.expr, + $3.expr, + }, + } + } +; +// list order gets us the position of the arg, but named params would work too! +call_args: + /* end of list */ + { + posLast(yylex, yyDollar) // our pos + $$.exprs = []interfaces.Expr{} + } + // seems that "left recursion" works here... thanks parser generator! +| call_args COMMA expr + { + posLast(yylex, yyDollar) // our pos + $$.exprs = append($1.exprs, $3.expr) + } +| expr + { + posLast(yylex, yyDollar) // our pos + $$.exprs = append([]interfaces.Expr{}, $1.expr) + } +; +var: + VAR_IDENTIFIER + { + posLast(yylex, yyDollar) // our pos + $$.expr = &ExprVar{ + Name: $1.str, + } + } +; +bind: + VAR_IDENTIFIER EQUALS expr + { + posLast(yylex, yyDollar) // our pos + $$.stmt = &StmtBind{ + Ident: $1.str, + Value: $3.expr, + } + } +| VAR_IDENTIFIER type EQUALS expr + { + posLast(yylex, yyDollar) // our pos + var expr interfaces.Expr = $4.expr + if err := expr.SetType($2.typ); err != nil { + panic(fmt.Sprintf("can't set type in parser: %+v", err)) + } + $$.stmt = &StmtBind{ + Ident: $1.str, + Value: expr, + } + } +; +/* TODO: do we want to include this? +// resource bind +rbind: + VAR_IDENTIFIER EQUALS resource + { + posLast(yylex, yyDollar) // our pos + // XXX: this kind of bind is different than the others, because + // it can only really be used for send->recv stuff, eg: + // foo.SomeString -> bar.SomeOtherString + $$.expr = &StmtBind{ + Ident: $1.str, + Value: $3.stmt, + } + } +; +*/ +resource: + IDENTIFIER expr OPEN_CURLY resource_body CLOSE_CURLY + { + posLast(yylex, yyDollar) // our pos + $$.stmt = &StmtRes{ + Kind: $1.str, + Name: $2.expr, + Fields: $4.resFields, + } + } +; +resource_body: + /* end of list */ + { + posLast(yylex, yyDollar) // our pos + $$.resFields = []*StmtResField{} + } +| resource_body resource_field + { + posLast(yylex, yyDollar) // our pos + $$.resFields = append($1.resFields, $2.resField) + } +; +resource_field: + IDENTIFIER ROCKET expr COMMA + { + posLast(yylex, yyDollar) // our pos + $$.resField = &StmtResField{ + Field: $1.str, + Value: $3.expr, + } + } +; +type: + BOOL_IDENTIFIER + { + posLast(yylex, yyDollar) // our pos + $$.typ = types.NewType($1.str) // "bool" + } +| STR_IDENTIFIER + { + posLast(yylex, yyDollar) // our pos + $$.typ = types.NewType($1.str) // "str" + } +| INT_IDENTIFIER + { + posLast(yylex, yyDollar) // our pos + $$.typ = types.NewType($1.str) // "int" + } +| FLOAT_IDENTIFIER + { + posLast(yylex, yyDollar) // our pos + $$.typ = types.NewType($1.str) // "float" + } +| OPEN_BRACK CLOSE_BRACK type + // list: []int or [][]str (with recursion) + { + posLast(yylex, yyDollar) // our pos + $$.typ = types.NewType("[]" + $3.typ.String()) + } +| OPEN_CURLY type COLON type CLOSE_CURLY + // map: {str: int} or {str: []int} + { + posLast(yylex, yyDollar) // our pos + $$.typ = types.NewType(fmt.Sprintf("{%s: %s}", $2.typ.String(), $4.typ.String())) + } +| STRUCT_IDENTIFIER OPEN_CURLY type_struct_fields CLOSE_CURLY + // struct: struct{} or struct{a bool} or struct{a bool; bb int} + { + posLast(yylex, yyDollar) // our pos + $$.typ = types.NewType(fmt.Sprintf("%s{%s}", $1.str, strings.Join($3.strSlice, "; "))) + } +| VARIANT_IDENTIFIER + { + posLast(yylex, yyDollar) // our pos + $$.typ = types.NewType($1.str) // "variant" + } +; +type_struct_fields: + /* end of list */ + { + posLast(yylex, yyDollar) // our pos + $$.strSlice = []string{} + } +| type_struct_fields SEMICOLON type_struct_field + { + posLast(yylex, yyDollar) // our pos + $$.strSlice = append($1.strSlice, $3.str) + } +| type_struct_field + { + posLast(yylex, yyDollar) // our pos + $$.strSlice = []string{$1.str} + } +; +type_struct_field: + IDENTIFIER type + { + posLast(yylex, yyDollar) // our pos + $$.str = fmt.Sprintf("%s %s", $1.str, $2.typ.String()) + } +; +%% +// pos is a helper function used to track the position in the parser. +func pos(y yyLexer, dollar yySymType) { + lp := cast(y) + lp.row = dollar.row + lp.col = dollar.col + // FIXME: in some cases the before last value is most meaningful... + //lp.row = append(lp.row, dollar.row) + //lp.col = append(lp.col, dollar.col) + //log.Printf("parse: %d x %d", lp.row, lp.col) + return +} + +// cast is used to pull out the parser run-specific struct we store our AST in. +// this is usually called in the parser. +func cast(y yyLexer) *lexParseAST { + x := y.(*Lexer).parseResult + return x.(*lexParseAST) +} + +// postLast pulls out the "last token" and does a pos with that. This is a hack! +func posLast(y yyLexer, dollars []yySymType) { + // pick the last token in the set matched by the parser + pos(y, dollars[len(dollars)-1]) // our pos +} + +// cast is used to pull out the parser run-specific struct we store our AST in. +// this is usually called in the lexer. +func (yylex *Lexer) cast() *lexParseAST { + return yylex.parseResult.(*lexParseAST) +} + +// pos is a helper function used to track the position in the lexer. +func (yylex *Lexer) pos(lval *yySymType) { + lval.row = yylex.Line() + lval.col = yylex.Column() + // TODO: we could use: `s := yylex.Text()` to calculate a delta length! + //log.Printf("lexer: %d x %d", lval.row, lval.col) +} + +// Error is the error handler which gets called on a parsing error. +func (yylex *Lexer) Error(str string) { + lp := yylex.cast() + if str != "" { + // This error came from the parser. It is usually also set when + // the lexer fails, because it ends up generating ERROR tokens, + // which most parsers usually don't match and store in the AST. + err := ErrParseError // TODO: add more specific types... + if strings.HasSuffix(str, ErrParseAdditionalEquals.Error()) { + err = ErrParseAdditionalEquals + } else if strings.HasSuffix(str, ErrParseExpectingComma.Error()) { + err = ErrParseExpectingComma + } + lp.parseErr = &LexParseErr{ + Err: err, + Str: str, + // FIXME: get these values, by tracking pos in parser... + // FIXME: currently, the values we get are mostly wrong! + Row: lp.row, //lp.row[len(lp.row)-1], + Col: lp.col, //lp.col[len(lp.col)-1], + } + } +} diff --git a/lang/structs.go b/lang/structs.go new file mode 100644 index 00000000..492b346f --- /dev/null +++ b/lang/structs.go @@ -0,0 +1,3051 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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 lang // TODO: move this into a sub package of lang/$name? + +import ( + "fmt" + "log" + "reflect" + "strings" + + "github.com/purpleidea/mgmt/lang/funcs" + "github.com/purpleidea/mgmt/lang/funcs/structs" + "github.com/purpleidea/mgmt/lang/interfaces" + "github.com/purpleidea/mgmt/lang/types" + "github.com/purpleidea/mgmt/lang/unification" + "github.com/purpleidea/mgmt/pgraph" + "github.com/purpleidea/mgmt/resources" + "github.com/purpleidea/mgmt/util" + + errwrap "github.com/pkg/errors" +) + +// StmtBind is a representation of an assignment, which binds a variable to an +// expression. +type StmtBind struct { + Ident string + Value interfaces.Expr +} + +// Interpolate returns a new node (or itself) once it has been expanded. This +// generally increases the size of the AST when it is used. It calls Interpolate +// on any child elements and builds the new node with those new node contents. +func (obj *StmtBind) Interpolate() (interfaces.Stmt, error) { + interpolated, err := obj.Value.Interpolate() + if err != nil { + return nil, err + } + return &StmtBind{ + Ident: obj.Ident, + Value: interpolated, + }, nil +} + +// SetScope sets the scope of the child expression bound to it. It seems this is +// necessary in order to reach this, in particular in situations when a bound +// expression points to a previously bound expression. +func (obj *StmtBind) SetScope(scope *interfaces.Scope) error { + return obj.Value.SetScope(scope) +} + +// Unify returns the list of invariants that this node produces. It recursively +// calls Unify on any children elements that exist in the AST, and returns the +// collection to the caller. +func (obj *StmtBind) Unify() ([]interfaces.Invariant, error) { + var invariants []interfaces.Invariant + + invars, err := obj.Value.Unify() + if err != nil { + return nil, err + } + invariants = append(invariants, invars...) + + return invariants, nil +} + +// Graph returns the reactive function graph which is expressed by this node. It +// includes any vertices produced by this node, and the appropriate edges to any +// vertices that are produced by its children. Nodes which fulfill the Expr +// interface directly produce vertices (and possible children) where as nodes +// that fulfill the Stmt interface do not produces vertices, where as their +// children might. This particular bind statement adds its linked expression to +// the graph. It is not logically done in the ExprVar since that could exist +// mulitple times for the single binding operation done here. +func (obj *StmtBind) Graph() (*pgraph.Graph, error) { + return obj.Value.Graph() +} + +// Output for the bind statement produces no output. Any values of interest come +// from the use of the var which this binds the expression to. +func (obj *StmtBind) Output() (*interfaces.Output, error) { + return (&interfaces.Output{}).Empty(), nil +} + +// StmtRes is a representation of a resource. +type StmtRes struct { + Kind string // kind of resource, eg: pkg, file, svc, etc... + Name interfaces.Expr // unique name for the res of this kind + Fields []*StmtResField // list of fields in parsed order +} + +// Interpolate returns a new node (or itself) once it has been expanded. This +// generally increases the size of the AST when it is used. It calls Interpolate +// on any child elements and builds the new node with those new node contents. +// TODO: could we expand Name (if it's a list) to have this return a list of +// Res's ? We'd have to return a StmtProg containing those in its place... +func (obj *StmtRes) Interpolate() (interfaces.Stmt, error) { + name, err := obj.Name.Interpolate() + if err != nil { + return nil, err + } + + fields := []*StmtResField{} + for _, x := range obj.Fields { + interpolated, err := x.Value.Interpolate() + if err != nil { + return nil, err + } + field := &StmtResField{ + Field: x.Field, + Value: interpolated, + } + fields = append(fields, field) + } + return &StmtRes{ + Kind: obj.Kind, + Name: name, + Fields: fields, + }, nil +} + +// SetScope stores the scope for later use in this resource and it's children, +// which it propogates this downwards to. +func (obj *StmtRes) SetScope(scope *interfaces.Scope) error { + if err := obj.Name.SetScope(scope); err != nil { + return err + } + for _, x := range obj.Fields { + if err := x.Value.SetScope(scope); err != nil { + return err + } + } + return nil +} + +// Unify returns the list of invariants that this node produces. It recursively +// calls Unify on any children elements that exist in the AST, and returns the +// collection to the caller. +func (obj *StmtRes) Unify() ([]interfaces.Invariant, error) { + var invariants []interfaces.Invariant + + invars, err := obj.Name.Unify() + if err != nil { + return nil, err + } + invariants = append(invariants, invars...) + + // name must be a string + invar := &unification.EqualsInvariant{ + Expr: obj.Name, + Type: types.TypeStr, + } + invariants = append(invariants, invar) + + // collect all the invariants of each field + for _, x := range obj.Fields { + invars, err := x.Value.Unify() + if err != nil { + return nil, err + } + invariants = append(invariants, invars...) + } + + typMap, err := resources.LangFieldNameToStructType(obj.Kind) + if err != nil { + return nil, err + } + + for _, x := range obj.Fields { + field := strings.TrimSpace(x.Field) + if len(field) != len(x.Field) { + return nil, fmt.Errorf("field was wrapped in whitespace") + } + if len(strings.Fields(field)) != 1 { + return nil, fmt.Errorf("field was empty or contained spaces") + } + + typ, exists := typMap[x.Field] + if !exists { + return nil, fmt.Errorf("could not determine type for `%s` field of `%s`", x.Field, obj.Kind) + } + invar := &unification.EqualsInvariant{ + Expr: x.Value, + Type: typ, + } + invariants = append(invariants, invar) + } + + return invariants, nil +} + +// Graph returns the reactive function graph which is expressed by this node. It +// includes any vertices produced by this node, and the appropriate edges to any +// vertices that are produced by its children. Nodes which fulfill the Expr +// interface directly produce vertices (and possible children) where as nodes +// that fulfill the Stmt interface do not produces vertices, where as their +// children might. It is interesting to note that nothing directly adds an edge +// to the resources created, but rather, once all the values (expressions) with +// no outgoing edges have produced at least a single value, then the resources +// know they're able to be built. +func (obj *StmtRes) Graph() (*pgraph.Graph, error) { + graph, err := pgraph.NewGraph("res") + if err != nil { + return nil, errwrap.Wrapf(err, "could not create graph") + } + + g, err := obj.Name.Graph() + if err != nil { + return nil, err + } + graph.AddGraph(g) + + for _, x := range obj.Fields { + g, err := x.Value.Graph() + if err != nil { + return nil, err + } + graph.AddGraph(g) + } + + return graph, nil +} + +// Output returns the output that this "program" produces. This output is what +// is used to build the output graph. This only exists for statements. The +// analogous function for expressions is Value. Those Value functions might get +// called by this Output function if they are needed to produce the output. In +// the case of this resource statement, this is definitely the case. +// XXX: Add MetaParams... +func (obj *StmtRes) Output() (*interfaces.Output, error) { + nameValue, err := obj.Name.Value() + if err != nil { + return nil, err + } + name := nameValue.Str() // must not panic + + res, err := resources.NewNamedResource(obj.Kind, name) + if err != nil { + return nil, errwrap.Wrapf(err, "cannot create resource kind `%s` with named `%s`", obj.Kind, name) + } + + s := reflect.ValueOf(res).Elem() // pointer to struct, then struct + if k := s.Kind(); k != reflect.Struct { + panic(fmt.Sprintf("expected struct, got: %s", k)) + } + + mapping, err := resources.LangFieldNameToStructFieldName(obj.Kind) + if err != nil { + return nil, err + } + ts := reflect.TypeOf(res).Elem() // pointer to struct, then struct + + // FIXME: we could probably simplify this code... + for _, x := range obj.Fields { + typ, err := x.Value.Type() + if err != nil { + return nil, errwrap.Wrapf(err, "resource field `%s` did not return a type", x.Field) + } + + fieldValue, err := x.Value.Value() // Value method on Expr + if err != nil { + return nil, err + } + val := fieldValue.Value() // get interface value + + name, exists := mapping[x.Field] // lookup recommended field name + if !exists { + return nil, fmt.Errorf("field `%s` does not exist", x.Field) // user made a typo? + } + + f := s.FieldByName(name) // exported field + if !f.IsValid() || !f.CanSet() { + return nil, fmt.Errorf("field `%s` cannot be set", name) // field is broken? + } + + tf, exists := ts.FieldByName(name) // exported field type + if !exists { // illogical because of above check? + return nil, fmt.Errorf("field `%s` type does not exist", x.Field) + } + + // is expr type compatible with expected field type? + t, err := types.TypeOf(tf.Type) + if err != nil { + return nil, errwrap.Wrapf(err, "resource field `%s` has no compatible type", x.Field) + } + if err := t.Cmp(typ); err != nil { + return nil, errwrap.Wrapf(err, "resource field `%s` of type `%+v`, cannot take type `%+v", x.Field, t, typ) + } + + // user `pestle` on #go-nuts irc wrongly insisted that it wasn't + // right to use reflect to do all of this. what is a better way? + + // first iterate through the raw pointers to the underlying type + ttt := tf.Type // ttt is field expected type + tkk := ttt.Kind() + for tkk == reflect.Ptr { + ttt = ttt.Elem() // un-nest one pointer + tkk = ttt.Kind() + } + + // all our int's are src kind == reflect.Int64 in our language! + if interfaces.Debug { + log.Printf("field `%s`: type(%+v), expected(%+v)", x.Field, typ, tkk) + } + + // overflow check + switch tkk { // match on destination field kind + case reflect.Int, reflect.Int64, reflect.Int32, reflect.Int16, reflect.Int8: + ff := reflect.Zero(ttt) // test on a non-ptr equivalent + if ff.OverflowInt(val.(int64)) { // this is valid! + return nil, fmt.Errorf("field `%s` is an `%s`, and value `%d` will overflow it", x.Field, f.Kind(), val) + } + + case reflect.Uint, reflect.Uint64, reflect.Uint32, reflect.Uint16, reflect.Uint8: + ff := reflect.Zero(ttt) + if ff.OverflowUint(uint64(val.(int64))) { // TODO: is this correct? + return nil, fmt.Errorf("field `%s` is an `%s`, and value `%d` will overflow it", x.Field, f.Kind(), val) + } + + case reflect.Float64, reflect.Float32: + ff := reflect.Zero(ttt) + if ff.OverflowFloat(val.(float64)) { + return nil, fmt.Errorf("field `%s` is an `%s`, and value `%d` will overflow it", x.Field, f.Kind(), val) + } + } + + value := reflect.ValueOf(val) // raw value + value = value.Convert(ttt) // now convert our raw value properly + + // finally build a new value to set + tt := tf.Type + kk := tt.Kind() + if interfaces.Debug { + log.Printf("field `%s`: start(%v)->kind(%v)", x.Field, tt, kk) + } + //fmt.Printf("start: %v || %+v\n", tt, kk) + for kk == reflect.Ptr { + tt = tt.Elem() // un-nest one pointer + kk = tt.Kind() + if interfaces.Debug { + log.Printf("field `%s`:\tloop(%v)->kind(%v)", x.Field, tt, kk) + } + // wrap in ptr by one level + valof := reflect.ValueOf(value.Interface()) + value = reflect.New(valof.Type()) + value.Elem().Set(valof) + } + f.Set(value) // set it ! + } + + return &interfaces.Output{ + Resources: []resources.Res{res}, + }, nil +} + +// StmtResField represents a single field in the parsed resource representation. +// This does not satisfy the Stmt interface. +type StmtResField struct { + Field string + Value interfaces.Expr +} + +// StmtEdge is a representation of a dependency. It also supports send/recv. +// Edges represents that the first resource (Kind/Name) listed in the +// EdgeHalfList should happen in the resource graph *before* the next resource +// in the list. If there are multiple StmtEdgeHalf structs listed, then they +// should represent a chain, eg: a->b->c, should compile into a->b & b->c. If +// specified, values are sent and received along these edges if the Send/Recv +// names are compatible and listed. In this case of Send/Recv, only lists of +// length two are legal. +type StmtEdge struct { + EdgeHalfList []*StmtEdgeHalf // represents a chain of edges + + // TODO: should notify be an Expr? + Notify bool // specifies that this edge sends a notification as well +} + +// Interpolate returns a new node (or itself) once it has been expanded. This +// generally increases the size of the AST when it is used. It calls Interpolate +// on any child elements and builds the new node with those new node contents. +// TODO: could we expand the Name's from the EdgeHalf (if they're lists) to have +// them return a list of Edges's ? +// XXX: type check the kind1:send -> kind2:recv fields are compatible! +// XXX: we won't know the names yet, but it's okay. +func (obj *StmtEdge) Interpolate() (interfaces.Stmt, error) { + edgeHalfList := []*StmtEdgeHalf{} + for _, x := range obj.EdgeHalfList { + name, err := x.Name.Interpolate() + if err != nil { + return nil, err + } + + edgeHalf := &StmtEdgeHalf{ + Kind: x.Kind, + Name: name, + SendRecv: x.SendRecv, + } + edgeHalfList = append(edgeHalfList, edgeHalf) + } + + return &StmtEdge{ + EdgeHalfList: edgeHalfList, + Notify: obj.Notify, + }, nil +} + +// SetScope stores the scope for later use in this resource and it's children, +// which it propogates this downwards to. +func (obj *StmtEdge) SetScope(scope *interfaces.Scope) error { + for _, x := range obj.EdgeHalfList { + if err := x.Name.SetScope(scope); err != nil { + return err + } + } + return nil +} + +// Unify returns the list of invariants that this node produces. It recursively +// calls Unify on any children elements that exist in the AST, and returns the +// collection to the caller. +func (obj *StmtEdge) Unify() ([]interfaces.Invariant, error) { + var invariants []interfaces.Invariant + + // TODO: this sort of sideloaded validation could happen in a dedicated + // Validate() function, but for now is here for lack of a better place! + if len(obj.EdgeHalfList) == 1 { + return nil, fmt.Errorf("can't create an edge with only one half") + } + if len(obj.EdgeHalfList) == 2 { + if (obj.EdgeHalfList[0].SendRecv == "") != (obj.EdgeHalfList[1].SendRecv == "") { // xor + return nil, fmt.Errorf("you must specify both send/recv fields or neither") + } + + // XXX: check that the kind1:send -> kind2:recv fields are type + // compatible! XXX: we won't know the names yet, but it's okay. + } + + for _, x := range obj.EdgeHalfList { + if x.Kind == "" { + return nil, fmt.Errorf("missing resource kind in edge") + } + + if x.SendRecv != "" && len(obj.EdgeHalfList) != 2 { + return nil, fmt.Errorf("send/recv edges must come in pairs") + } + + invars, err := x.Name.Unify() + if err != nil { + return nil, err + } + invariants = append(invariants, invars...) + + // name must be a string + invar := &unification.EqualsInvariant{ + Expr: x.Name, + Type: types.TypeStr, + } + invariants = append(invariants, invar) + } + + return invariants, nil +} + +// Graph returns the reactive function graph which is expressed by this node. It +// includes any vertices produced by this node, and the appropriate edges to any +// vertices that are produced by its children. Nodes which fulfill the Expr +// interface directly produce vertices (and possible children) where as nodes +// that fulfill the Stmt interface do not produces vertices, where as their +// children might. It is interesting to note that nothing directly adds an edge +// to the edges created, but rather, once all the values (expressions) with no +// outgoing function graph edges have produced at least a single value, then the +// edges know they're able to be built. +func (obj *StmtEdge) Graph() (*pgraph.Graph, error) { + graph, err := pgraph.NewGraph("edge") + if err != nil { + return nil, errwrap.Wrapf(err, "could not create graph") + } + + for _, x := range obj.EdgeHalfList { + g, err := x.Name.Graph() + if err != nil { + return nil, err + } + graph.AddGraph(g) + } + + return graph, nil +} + +// Output returns the output that this "program" produces. This output is what +// is used to build the output graph. This only exists for statements. The +// analogous function for expressions is Value. Those Value functions might get +// called by this Output function if they are needed to produce the output. In +// the case of this edge statement, this is definitely the case. This edge stmt +// returns output consisting of edges. +func (obj *StmtEdge) Output() (*interfaces.Output, error) { + edges := []*interfaces.Edge{} + + for i := 0; i < len(obj.EdgeHalfList)-1; i++ { + nameValue1, err := obj.EdgeHalfList[i].Name.Value() + if err != nil { + return nil, err + } + name1 := nameValue1.Str() // must not panic + + nameValue2, err := obj.EdgeHalfList[i+1].Name.Value() + if err != nil { + return nil, err + } + name2 := nameValue2.Str() // must not panic + + edge := &interfaces.Edge{ + Kind1: obj.EdgeHalfList[i].Kind, + Name1: name1, + Send: obj.EdgeHalfList[i].SendRecv, + + Kind2: obj.EdgeHalfList[i+1].Kind, + Name2: name2, + Recv: obj.EdgeHalfList[i+1].SendRecv, + + Notify: obj.Notify, + } + edges = append(edges, edge) + } + + return &interfaces.Output{ + Edges: edges, + }, nil +} + +// StmtEdgeHalf represents half of an edge in the parsed edge representation. +// This does not satisfy the Stmt interface. +type StmtEdgeHalf struct { + Kind string // kind of resource, eg: pkg, file, svc, etc... + Name interfaces.Expr // unique name for the res of this kind + SendRecv string // name of field to send/recv from, empty to ignore +} + +// StmtIf represents an if condition that contains between one and two branches +// of statements to be executed based on the evaluation of the boolean condition +// over time. In particular, this is different from an ExprIf which returns a +// value, where as this produces some Output. Normally if one of the branches is +// optional, it is the else branch, although this struct allows either to be +// optional, even if it is not commonly used. +type StmtIf struct { + Condition interfaces.Expr + ThenBranch interfaces.Stmt // optional, but usually present + ElseBranch interfaces.Stmt // optional +} + +// Interpolate returns a new node (or itself) once it has been expanded. This +// generally increases the size of the AST when it is used. It calls Interpolate +// on any child elements and builds the new node with those new node contents. +func (obj *StmtIf) Interpolate() (interfaces.Stmt, error) { + condition, err := obj.Condition.Interpolate() + if err != nil { + return nil, errwrap.Wrapf(err, "could not interpolate Condition") + } + var thenBranch interfaces.Stmt + if obj.ThenBranch != nil { + thenBranch, err = obj.ThenBranch.Interpolate() + if err != nil { + return nil, errwrap.Wrapf(err, "could not interpolate ThenBranch") + } + } + var elseBranch interfaces.Stmt + if obj.ElseBranch != nil { + elseBranch, err = obj.ElseBranch.Interpolate() + if err != nil { + return nil, errwrap.Wrapf(err, "could not interpolate ElseBranch") + } + } + return &StmtIf{ + Condition: condition, + ThenBranch: thenBranch, + ElseBranch: elseBranch, + }, nil +} + +// SetScope stores the scope for later use in this resource and it's children, +// which it propogates this downwards to. +func (obj *StmtIf) SetScope(scope *interfaces.Scope) error { + if err := obj.Condition.SetScope(scope); err != nil { + return err + } + if obj.ThenBranch != nil { + if err := obj.ThenBranch.SetScope(scope); err != nil { + return err + } + } + if obj.ElseBranch != nil { + if err := obj.ElseBranch.SetScope(scope); err != nil { + return err + } + } + return nil +} + +// Unify returns the list of invariants that this node produces. It recursively +// calls Unify on any children elements that exist in the AST, and returns the +// collection to the caller. +func (obj *StmtIf) Unify() ([]interfaces.Invariant, error) { + var invariants []interfaces.Invariant + + // conditional expression might have some children invariants to share + condition, err := obj.Condition.Unify() + if err != nil { + return nil, err + } + invariants = append(invariants, condition...) + + // the condition must ultimately be a boolean + conditionInvar := &unification.EqualsInvariant{ + Expr: obj.Condition, + Type: types.TypeBool, + } + invariants = append(invariants, conditionInvar) + + // recurse into the two branches + if obj.ThenBranch != nil { + thenBranch, err := obj.ThenBranch.Unify() + if err != nil { + return nil, err + } + invariants = append(invariants, thenBranch...) + } + + if obj.ElseBranch != nil { + elseBranch, err := obj.ElseBranch.Unify() + if err != nil { + return nil, err + } + invariants = append(invariants, elseBranch...) + } + + return invariants, nil +} + +// Graph returns the reactive function graph which is expressed by this node. It +// includes any vertices produced by this node, and the appropriate edges to any +// vertices that are produced by its children. Nodes which fulfill the Expr +// interface directly produce vertices (and possible children) where as nodes +// that fulfill the Stmt interface do not produces vertices, where as their +// children might. This particular if statement doesn't do anything clever here +// other than adding in both branches of the graph. Since we're functional, this +// shouldn't have any ill effects. +// XXX: is this completely true if we're running technically impure, but safe +// built-in functions on both branches? Can we turn off half of this? +func (obj *StmtIf) Graph() (*pgraph.Graph, error) { + graph, err := pgraph.NewGraph("if") + if err != nil { + return nil, errwrap.Wrapf(err, "could not create graph") + } + + g, err := obj.Condition.Graph() + if err != nil { + return nil, err + } + graph.AddGraph(g) + + for _, x := range []interfaces.Stmt{obj.ThenBranch, obj.ElseBranch} { + if x == nil { + continue + } + g, err := x.Graph() + if err != nil { + return nil, err + } + graph.AddGraph(g) + } + + return graph, nil +} + +// Output returns the output that this "program" produces. This output is what +// is used to build the output graph. This only exists for statements. The +// analogous function for expressions is Value. Those Value functions might get +// called by this Output function if they are needed to produce the output. +func (obj *StmtIf) Output() (*interfaces.Output, error) { + b, err := obj.Condition.Value() + if err != nil { + return nil, err + } + + var output *interfaces.Output + if b.Bool() { // must not panic! + if obj.ThenBranch != nil { // logically then branch is optional + output, err = obj.ThenBranch.Output() + } + } else { + if obj.ElseBranch != nil { // else branch is optional + output, err = obj.ElseBranch.Output() + } + } + if err != nil { + return nil, err + } + + resources := []resources.Res{} + if output != nil { + resources = append(resources, output.Resources...) + //edges = output.Edges + } + + return &interfaces.Output{ + Resources: resources, + //Edges: edges, + }, nil +} + +// StmtProg represents a list of stmt's. This usually occurs at the top-level of +// any program, and often within an if stmt. It also contains the logic so that +// the bind statement's are correctly applied in this scope, and irrespective of +// their order of definition. +type StmtProg struct { + Prog []interfaces.Stmt +} + +// Interpolate returns a new node (or itself) once it has been expanded. This +// generally increases the size of the AST when it is used. It calls Interpolate +// on any child elements and builds the new node with those new node contents. +func (obj *StmtProg) Interpolate() (interfaces.Stmt, error) { + prog := []interfaces.Stmt{} + for _, x := range obj.Prog { + interpolated, err := x.Interpolate() + if err != nil { + return nil, err + } + prog = append(prog, interpolated) + } + return &StmtProg{ + Prog: prog, + }, nil +} + +// SetScope propagates the scope into its list of statements. It does so +// cleverly by first collecting all bind statements and adding those into the +// scope after checking for any collisions. Finally it pushes the new scope +// downwards to all child statements. +func (obj *StmtProg) SetScope(scope *interfaces.Scope) error { + newScope := scope.Copy() + binds := []*StmtBind{} + names := make(map[string]struct{}) + + // collect all the bind statements in the first pass + // this allows them to appear out of order in this scope + for _, x := range obj.Prog { + bind, ok := x.(*StmtBind) + if !ok { + continue + } + // check for duplicates *in this scope* + if _, exists := names[bind.Ident]; exists { + return fmt.Errorf("var `%s` already exists in this scope", bind.Ident) + } + names[bind.Ident] = struct{}{} // add to scope + binds = append(binds, bind) + } + + // now we know there are no duplicates in this scope, there is only + // the possibility of shadowing a variable from the parent scope... + for _, bind := range binds { + // add to scope, (overwriting, aka shadowing is ok) + newScope.Variables[bind.Ident] = bind.Value + } + + // now set the child scopes (even on bind...) + for _, x := range obj.Prog { + if err := x.SetScope(newScope); err != nil { + return err + } + } + + return nil +} + +// Unify returns the list of invariants that this node produces. It recursively +// calls Unify on any children elements that exist in the AST, and returns the +// collection to the caller. +func (obj *StmtProg) Unify() ([]interfaces.Invariant, error) { + var invariants []interfaces.Invariant + + // collect all the invariants of each sub-expression + for _, x := range obj.Prog { + invars, err := x.Unify() + if err != nil { + return nil, err + } + invariants = append(invariants, invars...) + } + + return invariants, nil +} + +// Graph returns the reactive function graph which is expressed by this node. It +// includes any vertices produced by this node, and the appropriate edges to any +// vertices that are produced by its children. Nodes which fulfill the Expr +// interface directly produce vertices (and possible children) where as nodes +// that fulfill the Stmt interface do not produces vertices, where as their +// children might. +func (obj *StmtProg) Graph() (*pgraph.Graph, error) { + graph, err := pgraph.NewGraph("prog") + if err != nil { + return nil, errwrap.Wrapf(err, "could not create graph") + } + + // collect all graphs that need to be included + for _, x := range obj.Prog { + g, err := x.Graph() + if err != nil { + return nil, err + } + graph.AddGraph(g) + } + + return graph, nil +} + +// Output returns the output that this "program" produces. This output is what +// is used to build the output graph. This only exists for statements. The +// analogous function for expressions is Value. Those Value functions might get +// called by this Output function if they are needed to produce the output. +func (obj *StmtProg) Output() (*interfaces.Output, error) { + resources := []resources.Res{} + + for _, stmt := range obj.Prog { + output, err := stmt.Output() + if err != nil { + return nil, err + } + if output != nil { + resources = append(resources, output.Resources...) + //edges = append(edges, output.Edges) + } + } + + return &interfaces.Output{ + Resources: resources, + //Edges: edges, + }, nil +} + +// StmtComment is a representation of a comment. It is currently unused. It +// probably makes sense to make a third kind of Node (not a Stmt or an Expr) so +// that comments can still be part of the AST (for eventual automatic code +// formatting) but so that they can exist anywhere in the code. Currently these +// are dropped by the lexer. +type StmtComment struct { + Value string +} + +// Interpolate returns a new node (or itself) once it has been expanded. This +// generally increases the size of the AST when it is used. It calls Interpolate +// on any child elements and builds the new node with those new node contents. +// Here it simply returns itself, as no interpolation is possible. +func (obj *StmtComment) Interpolate() (interfaces.Stmt, error) { return obj, nil } + +// SetScope does nothing for this struct, because it has no child nodes, and it +// does not need to know about the parent scope. +func (obj *StmtComment) SetScope(*interfaces.Scope) error { return nil } + +// Unify returns the list of invariants that this node produces. It recursively +// calls Unify on any children elements that exist in the AST, and returns the +// collection to the caller. +func (obj *StmtComment) Unify() ([]interfaces.Invariant, error) { + return []interfaces.Invariant{}, nil +} + +// Graph returns the reactive function graph which is expressed by this node. It +// includes any vertices produced by this node, and the appropriate edges to any +// vertices that are produced by its children. Nodes which fulfill the Expr +// interface directly produce vertices (and possible children) where as nodes +// that fulfill the Stmt interface do not produces vertices, where as their +// children might. This particular graph does nothing clever. +func (obj *StmtComment) Graph() (*pgraph.Graph, error) { + graph, err := pgraph.NewGraph("comment") + if err != nil { + return nil, errwrap.Wrapf(err, "could not create graph") + } + return graph, nil +} + +// Output for the comment statement produces no output. +func (obj *StmtComment) Output() (*interfaces.Output, error) { + return (&interfaces.Output{}).Empty(), nil +} + +// ExprBool is a representation of a boolean. +type ExprBool struct { + V bool +} + +// String returns a short representation of this expression. +func (obj *ExprBool) String() string { return fmt.Sprintf("bool(%t)", obj.V) } + +// Interpolate returns a new node (or itself) once it has been expanded. This +// generally increases the size of the AST when it is used. It calls Interpolate +// on any child elements and builds the new node with those new node contents. +// Here it simply returns itself, as no interpolation is possible. +func (obj *ExprBool) Interpolate() (interfaces.Expr, error) { return obj, nil } + +// SetScope does nothing for this struct, because it has no child nodes, and it +// does not need to know about the parent scope. +func (obj *ExprBool) SetScope(*interfaces.Scope) error { return nil } + +// SetType will make no changes if called here. It will error if anything other +// than a Bool is passed in, and doesn't need to be called for this expr to work. +func (obj *ExprBool) SetType(typ *types.Type) error { return types.TypeBool.Cmp(typ) } + +// Type returns the type of this expression. This method always returns Bool here. +func (obj *ExprBool) Type() (*types.Type, error) { return types.TypeBool, nil } + +// Unify returns the list of invariants that this node produces. It recursively +// calls Unify on any children elements that exist in the AST, and returns the +// collection to the caller. +func (obj *ExprBool) Unify() ([]interfaces.Invariant, error) { + invariants := []interfaces.Invariant{ + &unification.EqualsInvariant{ + Expr: obj, + Type: types.TypeBool, + }, + } + return invariants, nil +} + +// Graph returns the reactive function graph which is expressed by this node. It +// includes any vertices produced by this node, and the appropriate edges to any +// vertices that are produced by its children. Nodes which fulfill the Expr +// interface directly produce vertices (and possible children) where as nodes +// that fulfill the Stmt interface do not produces vertices, where as their +// children might. This returns a graph with a single vertex (itself) in it. +func (obj *ExprBool) Graph() (*pgraph.Graph, error) { + graph, err := pgraph.NewGraph("bool") + if err != nil { + return nil, errwrap.Wrapf(err, "could not create graph") + } + graph.AddVertex(obj) + return graph, nil +} + +// Func returns the reactive stream of values that this expression produces. +func (obj *ExprBool) Func() (interfaces.Func, error) { + return &structs.ConstFunc{ + Value: &types.BoolValue{V: obj.V}, + }, nil +} + +// SetValue for a bool expression is always populated statically, and does not +// ever receive any incoming values (no incoming edges) so this should never be +// called. It has been implemented for uniformity. +func (obj *ExprBool) SetValue(value types.Value) error { + if err := types.TypeBool.Cmp(value.Type()); err != nil { + return err + } + obj.V = value.Bool() + return nil +} + +// Value returns the value of this expression in our type system. This will +// usually only be valid once the engine has run and values have been produced. +// This might get called speculatively (early) during unification to learn more. +// This particular value is always known since it is a constant. +func (obj *ExprBool) Value() (types.Value, error) { + return &types.BoolValue{ + V: obj.V, + }, nil +} + +// ExprStr is a representation of a string. +type ExprStr struct { + V string // value of this string +} + +// String returns a short representation of this expression. +func (obj *ExprStr) String() string { return fmt.Sprintf("str(%s)", obj.V) } + +// Interpolate returns a new node (or itself) once it has been expanded. This +// generally increases the size of the AST when it is used. It calls Interpolate +// on any child elements and builds the new node with those new node contents. +// Here it attempts to expand the string if there are any internal variables +// which need interpolation. If any are found, it returns a larger AST which +// has a function which returns a string as its root. Otherwise it returns +// itself. +func (obj *ExprStr) Interpolate() (interfaces.Expr, error) { + pos := &Pos{ + // column/line number, starting at 1 + //Column: -1, // TODO + //Line: -1, // TODO + //Filename: "", // optional source filename, if known + } + result, err := InterpolateStr(obj.V, pos) + if err != nil { + return nil, err + } + if result == nil { + return obj, nil // pass self through, no changes + } + // we got something, overwrite the existing static str + return result, nil // replacement +} + +// SetScope does nothing for this struct, because it has no child nodes, and it +// does not need to know about the parent scope. +func (obj *ExprStr) SetScope(*interfaces.Scope) error { return nil } + +// SetType will make no changes if called here. It will error if anything other +// than an Str is passed in, and doesn't need to be called for this expr to work. +func (obj *ExprStr) SetType(typ *types.Type) error { return types.TypeStr.Cmp(typ) } + +// Type returns the type of this expression. This method always returns Str here. +func (obj *ExprStr) Type() (*types.Type, error) { return types.TypeStr, nil } + +// Unify returns the list of invariants that this node produces. It recursively +// calls Unify on any children elements that exist in the AST, and returns the +// collection to the caller. +func (obj *ExprStr) Unify() ([]interfaces.Invariant, error) { + invariants := []interfaces.Invariant{ + &unification.EqualsInvariant{ + Expr: obj, // unique id for this expression (a pointer) + Type: types.TypeStr, + }, + } + return invariants, nil +} + +// Graph returns the reactive function graph which is expressed by this node. It +// includes any vertices produced by this node, and the appropriate edges to any +// vertices that are produced by its children. Nodes which fulfill the Expr +// interface directly produce vertices (and possible children) where as nodes +// that fulfill the Stmt interface do not produces vertices, where as their +// children might. This returns a graph with a single vertex (itself) in it. +func (obj *ExprStr) Graph() (*pgraph.Graph, error) { + graph, err := pgraph.NewGraph("str") + if err != nil { + return nil, errwrap.Wrapf(err, "could not create graph") + } + graph.AddVertex(obj) + return graph, nil +} + +// Func returns the reactive stream of values that this expression produces. +func (obj *ExprStr) Func() (interfaces.Func, error) { + return &structs.ConstFunc{ + Value: &types.StrValue{V: obj.V}, + }, nil +} + +// SetValue for an str expression is always populated statically, and does not +// ever receive any incoming values (no incoming edges) so this should never be +// called. It has been implemented for uniformity. +func (obj *ExprStr) SetValue(value types.Value) error { + if err := types.TypeStr.Cmp(value.Type()); err != nil { + return err + } + obj.V = value.Str() + return nil +} + +// Value returns the value of this expression in our type system. This will +// usually only be valid once the engine has run and values have been produced. +// This might get called speculatively (early) during unification to learn more. +// This particular value is always known since it is a constant. +func (obj *ExprStr) Value() (types.Value, error) { + return &types.StrValue{ + V: obj.V, + }, nil +} + +// ExprInt is a representation of an int. +type ExprInt struct { + V int64 +} + +// String returns a short representation of this expression. +func (obj *ExprInt) String() string { return fmt.Sprintf("int(%d)", obj.V) } + +// Interpolate returns a new node (or itself) once it has been expanded. This +// generally increases the size of the AST when it is used. It calls Interpolate +// on any child elements and builds the new node with those new node contents. +// Here it simply returns itself, as no interpolation is possible. +func (obj *ExprInt) Interpolate() (interfaces.Expr, error) { return obj, nil } + +// SetScope does nothing for this struct, because it has no child nodes, and it +// does not need to know about the parent scope. +func (obj *ExprInt) SetScope(*interfaces.Scope) error { return nil } + +// SetType will make no changes if called here. It will error if anything other +// than an Int is passed in, and doesn't need to be called for this expr to work. +func (obj *ExprInt) SetType(typ *types.Type) error { return types.TypeInt.Cmp(typ) } + +// Type returns the type of this expression. This method always returns Int here. +func (obj *ExprInt) Type() (*types.Type, error) { return types.TypeInt, nil } + +// Unify returns the list of invariants that this node produces. It recursively +// calls Unify on any children elements that exist in the AST, and returns the +// collection to the caller. +func (obj *ExprInt) Unify() ([]interfaces.Invariant, error) { + invariants := []interfaces.Invariant{ + &unification.EqualsInvariant{ + Expr: obj, + Type: types.TypeInt, + }, + } + return invariants, nil +} + +// Graph returns the reactive function graph which is expressed by this node. It +// includes any vertices produced by this node, and the appropriate edges to any +// vertices that are produced by its children. Nodes which fulfill the Expr +// interface directly produce vertices (and possible children) where as nodes +// that fulfill the Stmt interface do not produces vertices, where as their +// children might. This returns a graph with a single vertex (itself) in it. +func (obj *ExprInt) Graph() (*pgraph.Graph, error) { + graph, err := pgraph.NewGraph("int") + if err != nil { + return nil, errwrap.Wrapf(err, "could not create graph") + } + graph.AddVertex(obj) + return graph, nil +} + +// Func returns the reactive stream of values that this expression produces. +func (obj *ExprInt) Func() (interfaces.Func, error) { + return &structs.ConstFunc{ + Value: &types.IntValue{V: obj.V}, + }, nil +} + +// SetValue for an int expression is always populated statically, and does not +// ever receive any incoming values (no incoming edges) so this should never be +// called. It has been implemented for uniformity. +func (obj *ExprInt) SetValue(value types.Value) error { + if err := types.TypeInt.Cmp(value.Type()); err != nil { + return err + } + obj.V = value.Int() + return nil +} + +// Value returns the value of this expression in our type system. This will +// usually only be valid once the engine has run and values have been produced. +// This might get called speculatively (early) during unification to learn more. +// This particular value is always known since it is a constant. +func (obj *ExprInt) Value() (types.Value, error) { + return &types.IntValue{ + V: obj.V, + }, nil +} + +// ExprFloat is a representation of a float. +type ExprFloat struct { + V float64 +} + +// String returns a short representation of this expression. +func (obj *ExprFloat) String() string { + return fmt.Sprintf("float(%g)", obj.V) // TODO: %f instead? +} + +// Interpolate returns a new node (or itself) once it has been expanded. This +// generally increases the size of the AST when it is used. It calls Interpolate +// on any child elements and builds the new node with those new node contents. +// Here it simply returns itself, as no interpolation is possible. +func (obj *ExprFloat) Interpolate() (interfaces.Expr, error) { return obj, nil } + +// SetScope does nothing for this struct, because it has no child nodes, and it +// does not need to know about the parent scope. +func (obj *ExprFloat) SetScope(*interfaces.Scope) error { return nil } + +// SetType will make no changes if called here. It will error if anything other +// than a Float is passed in, and doesn't need to be called for this expr to work. +func (obj *ExprFloat) SetType(typ *types.Type) error { return types.TypeFloat.Cmp(typ) } + +// Type returns the type of this expression. This method always returns Float here. +func (obj *ExprFloat) Type() (*types.Type, error) { return types.TypeFloat, nil } + +// Unify returns the list of invariants that this node produces. It recursively +// calls Unify on any children elements that exist in the AST, and returns the +// collection to the caller. +func (obj *ExprFloat) Unify() ([]interfaces.Invariant, error) { + invariants := []interfaces.Invariant{ + &unification.EqualsInvariant{ + Expr: obj, + Type: types.TypeFloat, + }, + } + return invariants, nil +} + +// Graph returns the reactive function graph which is expressed by this node. It +// includes any vertices produced by this node, and the appropriate edges to any +// vertices that are produced by its children. Nodes which fulfill the Expr +// interface directly produce vertices (and possible children) where as nodes +// that fulfill the Stmt interface do not produces vertices, where as their +// children might. This returns a graph with a single vertex (itself) in it. +func (obj *ExprFloat) Graph() (*pgraph.Graph, error) { + graph, err := pgraph.NewGraph("float") + if err != nil { + return nil, errwrap.Wrapf(err, "could not create graph") + } + graph.AddVertex(obj) + return graph, nil +} + +// Func returns the reactive stream of values that this expression produces. +func (obj *ExprFloat) Func() (interfaces.Func, error) { + return &structs.ConstFunc{ + Value: &types.FloatValue{V: obj.V}, + }, nil +} + +// SetValue for a float expression is always populated statically, and does not +// ever receive any incoming values (no incoming edges) so this should never be +// called. It has been implemented for uniformity. +func (obj *ExprFloat) SetValue(value types.Value) error { + if err := types.TypeFloat.Cmp(value.Type()); err != nil { + return err + } + obj.V = value.Float() + return nil +} + +// Value returns the value of this expression in our type system. This will +// usually only be valid once the engine has run and values have been produced. +// This might get called speculatively (early) during unification to learn more. +// This particular value is always known since it is a constant. +func (obj *ExprFloat) Value() (types.Value, error) { + return &types.FloatValue{ + V: obj.V, + }, nil +} + +// ExprList is a representation of a list. +type ExprList struct { + typ *types.Type + + //Elements []*ExprListElement + Elements []interfaces.Expr +} + +// String returns a short representation of this expression. +func (obj *ExprList) String() string { + var s []string + for _, x := range obj.Elements { + s = append(s, x.String()) + } + return fmt.Sprintf("list(%s)", strings.Join(s, ", ")) +} + +// Interpolate returns a new node (or itself) once it has been expanded. This +// generally increases the size of the AST when it is used. It calls Interpolate +// on any child elements and builds the new node with those new node contents. +func (obj *ExprList) Interpolate() (interfaces.Expr, error) { + elements := []interfaces.Expr{} + for _, x := range obj.Elements { + interpolated, err := x.Interpolate() + if err != nil { + return nil, err + } + elements = append(elements, interpolated) + } + return &ExprList{ + typ: obj.typ, + Elements: elements, + }, nil +} + +// SetScope stores the scope for later use in this resource and it's children, +// which it propogates this downwards to. +func (obj *ExprList) SetScope(scope *interfaces.Scope) error { + for _, x := range obj.Elements { + if err := x.SetScope(scope); err != nil { + return err + } + } + return nil +} + +// SetType is used to set the type of this expression once it is known. This +// usually happens during type unification, but it can also happen during +// parsing if a type is specified explicitly. Since types are static and don't +// change on expressions, if you attempt to set a different type than what has +// previously been set (when not initially known) this will error. +func (obj *ExprList) SetType(typ *types.Type) error { + // TODO: should we ensure this is set to a KindList ? + if obj.typ != nil { + return obj.typ.Cmp(typ) // if not set, ensure it doesn't change + } + obj.typ = typ // set + return nil +} + +// Type returns the type of this expression. +func (obj *ExprList) Type() (*types.Type, error) { + var typ *types.Type + var err error + for i, expr := range obj.Elements { + etyp, e := expr.Type() + if e != nil { + err = errwrap.Wrapf(e, "list index `%d` did not return a type", i) + break + } + if typ == nil { + typ = etyp + } + if e := typ.Cmp(etyp); e != nil { + err = errwrap.Wrapf(e, "list elements have different types") + break + } + } + if err == nil && obj.typ == nil && len(obj.Elements) > 0 { + return &types.Type{ // speculate! + Kind: types.KindList, + Val: typ, + }, nil + } + + if obj.typ == nil { + return nil, interfaces.ErrTypeCurrentlyUnknown + } + return obj.typ, nil +} + +// Unify returns the list of invariants that this node produces. It recursively +// calls Unify on any children elements that exist in the AST, and returns the +// collection to the caller. +func (obj *ExprList) Unify() ([]interfaces.Invariant, error) { + var invariants []interfaces.Invariant + + // if this was set explicitly by the parser + if obj.typ != nil { + invar := &unification.EqualsInvariant{ + Expr: obj, + Type: obj.typ, + } + invariants = append(invariants, invar) + } + + // collect all the invariants of each sub-expression + for _, x := range obj.Elements { + invars, err := x.Unify() + if err != nil { + return nil, err + } + invariants = append(invariants, invars...) + } + + // each element must be equal to each other + if len(obj.Elements) > 1 { + invariant := &unification.EqualityInvariantList{ + Exprs: obj.Elements, + } + invariants = append(invariants, invariant) + } + + // we should be type list of (type of element) + if len(obj.Elements) > 0 { + invariant := &unification.EqualityWrapListInvariant{ + Expr1: obj, // unique id for this expression (a pointer) + Expr2Val: obj.Elements[0], + } + invariants = append(invariants, invariant) + } + + return invariants, nil +} + +// Graph returns the reactive function graph which is expressed by this node. It +// includes any vertices produced by this node, and the appropriate edges to any +// vertices that are produced by its children. Nodes which fulfill the Expr +// interface directly produce vertices (and possible children) where as nodes +// that fulfill the Stmt interface do not produces vertices, where as their +// children might. This returns a graph with a single vertex (itself) in it, and +// the edges from all of the child graphs to this. +func (obj *ExprList) Graph() (*pgraph.Graph, error) { + graph, err := pgraph.NewGraph("list") + if err != nil { + return nil, errwrap.Wrapf(err, "could not create graph") + } + graph.AddVertex(obj) + + // each list element needs to point to the final list expression + for index, x := range obj.Elements { // list elements in order + g, err := x.Graph() + if err != nil { + return nil, err + } + + fieldName := fmt.Sprintf("%d", index) // argNames as integers! + edge := &funcs.Edge{Args: []string{fieldName}} + + var once bool + edgeGenFn := func(v1, v2 pgraph.Vertex) pgraph.Edge { + if once { + panic(fmt.Sprintf("edgeGenFn for list, index `%d` was called twice", index)) + } + once = true + return edge + } + graph.AddEdgeGraphVertexLight(g, obj, edgeGenFn) // element -> list + } + + return graph, nil +} + +// Func returns the reactive stream of values that this expression produces. +func (obj *ExprList) Func() (interfaces.Func, error) { + typ, err := obj.Type() + if err != nil { + return nil, err + } + + // composite func (list, map, struct) + return &structs.CompositeFunc{ + Type: typ, + Len: len(obj.Elements), + }, nil +} + +// SetValue here is a no-op, because algorithmically when this is called from +// the func engine, the child elements (the list elements) will have had this +// done to them first, and as such when we try and retrieve the set value from +// this expression by calling `Value`, it will build it from scratch! +func (obj *ExprList) SetValue(value types.Value) error { + if err := obj.typ.Cmp(value.Type()); err != nil { + return err + } + // noop! + //obj.V = value + return nil +} + +// Value returns the value of this expression in our type system. This will +// usually only be valid once the engine has run and values have been produced. +// This might get called speculatively (early) during unification to learn more. +func (obj *ExprList) Value() (types.Value, error) { + values := []types.Value{} + var typ *types.Type + + for i, expr := range obj.Elements { + etyp, err := expr.Type() + if err != nil { + return nil, errwrap.Wrapf(err, "list index `%d` did not return a type", i) + } + if typ == nil { + typ = etyp + } + if err := typ.Cmp(etyp); err != nil { + return nil, errwrap.Wrapf(err, "list elements have different types") + } + + value, err := expr.Value() + if err != nil { + return nil, err + } + if value == nil { + return nil, fmt.Errorf("value for list index `%d` was nil", i) + } + values = append(values, value) + } + if len(obj.Elements) > 0 { + t := &types.Type{ + Kind: types.KindList, + Val: typ, + } + // Run SetType to ensure type is consistent with what we found, + // which is an easy way to ensure the Cmp passes as expected... + if err := obj.SetType(t); err != nil { + return nil, errwrap.Wrapf(err, "type did not match expected!") + } + } + + return &types.ListValue{ + T: obj.typ, + V: values, + }, nil +} + +// ExprMap is a representation of a (dictionary) map. +type ExprMap struct { + typ *types.Type + + KVs []*ExprMapKV +} + +// String returns a short representation of this expression. +func (obj *ExprMap) String() string { + var s []string + for _, x := range obj.KVs { + s = append(s, fmt.Sprintf("%s: %s", x.Key.String(), x.Val.String())) + } + return fmt.Sprintf("map(%s)", strings.Join(s, ", ")) +} + +// Interpolate returns a new node (or itself) once it has been expanded. This +// generally increases the size of the AST when it is used. It calls Interpolate +// on any child elements and builds the new node with those new node contents. +func (obj *ExprMap) Interpolate() (interfaces.Expr, error) { + kvs := []*ExprMapKV{} + for _, x := range obj.KVs { + interpolatedKey, err := x.Key.Interpolate() + if err != nil { + return nil, err + } + interpolatedVal, err := x.Val.Interpolate() + if err != nil { + return nil, err + } + kv := &ExprMapKV{ + Key: interpolatedKey, + Val: interpolatedVal, + } + kvs = append(kvs, kv) + } + return &ExprMap{ + typ: obj.typ, + KVs: kvs, + }, nil +} + +// SetScope stores the scope for later use in this resource and it's children, +// which it propogates this downwards to. +func (obj *ExprMap) SetScope(scope *interfaces.Scope) error { + for _, x := range obj.KVs { + if err := x.Key.SetScope(scope); err != nil { + return err + } + if err := x.Val.SetScope(scope); err != nil { + return err + } + } + return nil +} + +// SetType is used to set the type of this expression once it is known. This +// usually happens during type unification, but it can also happen during +// parsing if a type is specified explicitly. Since types are static and don't +// change on expressions, if you attempt to set a different type than what has +// previously been set (when not initially known) this will error. +func (obj *ExprMap) SetType(typ *types.Type) error { + // TODO: should we ensure this is set to a KindMap ? + if obj.typ != nil { + return obj.typ.Cmp(typ) // if not set, ensure it doesn't change + } + obj.typ = typ // set + return nil +} + +// Type returns the type of this expression. +func (obj *ExprMap) Type() (*types.Type, error) { + var ktyp, vtyp *types.Type + var err error + for i, x := range obj.KVs { + // keys + kt, e := x.Key.Type() + if e != nil { + err = errwrap.Wrapf(e, "map key, index `%d` did not return a type", i) + break + } + if ktyp == nil { + ktyp = kt + } + if e := ktyp.Cmp(kt); e != nil { + err = errwrap.Wrapf(e, "key elements have different types") + break + } + + // vals + vt, e := x.Val.Type() + if e != nil { + err = errwrap.Wrapf(e, "map val, index `%d` did not return a type", i) + break + } + if vtyp == nil { + vtyp = vt + } + if e := vtyp.Cmp(vt); e != nil { + err = errwrap.Wrapf(e, "val elements have different types") + break + } + } + if err == nil && obj.typ == nil && len(obj.KVs) > 0 { + return &types.Type{ // speculate! + Kind: types.KindMap, + Key: ktyp, + Val: vtyp, + }, nil + } + + if obj.typ == nil { + return nil, interfaces.ErrTypeCurrentlyUnknown + } + return obj.typ, nil +} + +// Unify returns the list of invariants that this node produces. It recursively +// calls Unify on any children elements that exist in the AST, and returns the +// collection to the caller. +func (obj *ExprMap) Unify() ([]interfaces.Invariant, error) { + var invariants []interfaces.Invariant + + // if this was set explicitly by the parser + if obj.typ != nil { + invar := &unification.EqualsInvariant{ + Expr: obj, + Type: obj.typ, + } + invariants = append(invariants, invar) + } + + // collect all the invariants of each sub-expression + for _, x := range obj.KVs { + keyInvars, err := x.Key.Unify() + if err != nil { + return nil, err + } + invariants = append(invariants, keyInvars...) + + valInvars, err := x.Val.Unify() + if err != nil { + return nil, err + } + invariants = append(invariants, valInvars...) + } + + // all keys must have the same type, all vals must have the same type + if len(obj.KVs) > 1 { + keyExprs, valExprs := []interfaces.Expr{}, []interfaces.Expr{} + for i := range obj.KVs { + keyExprs = append(keyExprs, obj.KVs[i].Key) + valExprs = append(valExprs, obj.KVs[i].Val) + } + + keyInvariant := &unification.EqualityInvariantList{ + Exprs: keyExprs, + } + invariants = append(invariants, keyInvariant) + + valInvariant := &unification.EqualityInvariantList{ + Exprs: valExprs, + } + invariants = append(invariants, valInvariant) + } + + // we should be type map of (type of element) + if len(obj.KVs) > 0 { + invariant := &unification.EqualityWrapMapInvariant{ + Expr1: obj, // unique id for this expression (a pointer) + Expr2Key: obj.KVs[0].Key, + Expr2Val: obj.KVs[0].Val, + } + invariants = append(invariants, invariant) + } + + return invariants, nil +} + +// Graph returns the reactive function graph which is expressed by this node. It +// includes any vertices produced by this node, and the appropriate edges to any +// vertices that are produced by its children. Nodes which fulfill the Expr +// interface directly produce vertices (and possible children) where as nodes +// that fulfill the Stmt interface do not produces vertices, where as their +// children might. This returns a graph with a single vertex (itself) in it, and +// the edges from all of the child graphs to this. +func (obj *ExprMap) Graph() (*pgraph.Graph, error) { + graph, err := pgraph.NewGraph("map") + if err != nil { + return nil, errwrap.Wrapf(err, "could not create graph") + } + graph.AddVertex(obj) + + // each map key value pair needs to point to the final map expression + for index, x := range obj.KVs { // map fields in order + g, err := x.Key.Graph() + if err != nil { + return nil, err + } + // do the key names ever change? -- yes + fieldName := fmt.Sprintf("key:%d", index) // stringify map key + edge := &funcs.Edge{Args: []string{fieldName}} + + var once bool + edgeGenFn := func(v1, v2 pgraph.Vertex) pgraph.Edge { + if once { + panic(fmt.Sprintf("edgeGenFn for map, key `%s` was called twice", fieldName)) + } + once = true + return edge + } + graph.AddEdgeGraphVertexLight(g, obj, edgeGenFn) // key -> func + } + + // each map key value pair needs to point to the final map expression + for index, x := range obj.KVs { // map fields in order + g, err := x.Val.Graph() + if err != nil { + return nil, err + } + fieldName := fmt.Sprintf("val:%d", index) // stringify map val + edge := &funcs.Edge{Args: []string{fieldName}} + + var once bool + edgeGenFn := func(v1, v2 pgraph.Vertex) pgraph.Edge { + if once { + panic(fmt.Sprintf("edgeGenFn for map, val `%s` was called twice", fieldName)) + } + once = true + return edge + } + graph.AddEdgeGraphVertexLight(g, obj, edgeGenFn) // val -> func + } + + return graph, nil +} + +// Func returns the reactive stream of values that this expression produces. +func (obj *ExprMap) Func() (interfaces.Func, error) { + typ, err := obj.Type() + if err != nil { + return nil, err + } + + // composite func (list, map, struct) + return &structs.CompositeFunc{ + Type: typ, // the key/val types are known via this type + Len: len(obj.KVs), + }, nil +} + +// SetValue here is a no-op, because algorithmically when this is called from +// the func engine, the child key/value's (the map elements) will have had this +// done to them first, and as such when we try and retrieve the set value from +// this expression by calling `Value`, it will build it from scratch! +func (obj *ExprMap) SetValue(value types.Value) error { + if err := obj.typ.Cmp(value.Type()); err != nil { + return err + } + // noop! + //obj.V = value + return nil +} + +// Value returns the value of this expression in our type system. This will +// usually only be valid once the engine has run and values have been produced. +// This might get called speculatively (early) during unification to learn more. +func (obj *ExprMap) Value() (types.Value, error) { + kvs := make(map[types.Value]types.Value) + var ktyp, vtyp *types.Type + + for i, x := range obj.KVs { + // keys + kt, err := x.Key.Type() + if err != nil { + return nil, errwrap.Wrapf(err, "map key, index `%d` did not return a type", i) + } + if ktyp == nil { + ktyp = kt + } + if err := ktyp.Cmp(kt); err != nil { + return nil, errwrap.Wrapf(err, "key elements have different types") + } + + key, err := x.Key.Value() + if err != nil { + return nil, err + } + if key == nil { + return nil, fmt.Errorf("key for map index `%d` was nil", i) + } + + // vals + vt, err := x.Val.Type() + if err != nil { + return nil, errwrap.Wrapf(err, "map val, index `%d` did not return a type", i) + } + if vtyp == nil { + vtyp = vt + } + if err := vtyp.Cmp(vt); err != nil { + return nil, errwrap.Wrapf(err, "val elements have different types") + } + + val, err := x.Val.Value() + if err != nil { + return nil, err + } + if val == nil { + return nil, fmt.Errorf("val for map index `%d` was nil", i) + } + + kvs[key] = val // add to map + } + if len(obj.KVs) > 0 { + t := &types.Type{ + Kind: types.KindMap, + Key: ktyp, + Val: vtyp, + } + // Run SetType to ensure type is consistent with what we found, + // which is an easy way to ensure the Cmp passes as expected... + if err := obj.SetType(t); err != nil { + return nil, errwrap.Wrapf(err, "type did not match expected!") + } + } + + return &types.MapValue{ + T: obj.typ, + V: kvs, + }, nil +} + +// ExprMapKV represents a key and value pair in a (dictionary) map. This does +// not satisfy the Expr interface. +type ExprMapKV struct { + Key interfaces.Expr // keys can be strings, int's, etc... + Val interfaces.Expr +} + +// ExprStruct is a representation of a struct. +type ExprStruct struct { + typ *types.Type + + Fields []*ExprStructField // the list (fields) are intentionally ordered! +} + +// String returns a short representation of this expression. +func (obj *ExprStruct) String() string { + var s []string + for _, x := range obj.Fields { + s = append(s, fmt.Sprintf("%s: %s", x.Name, x.Value.String())) + } + return fmt.Sprintf("struct(%s)", strings.Join(s, "; ")) +} + +// Interpolate returns a new node (or itself) once it has been expanded. This +// generally increases the size of the AST when it is used. It calls Interpolate +// on any child elements and builds the new node with those new node contents. +func (obj *ExprStruct) Interpolate() (interfaces.Expr, error) { + fields := []*ExprStructField{} + for _, x := range obj.Fields { + interpolated, err := x.Value.Interpolate() + if err != nil { + return nil, err + } + field := &ExprStructField{ + Name: x.Name, // don't interpolate the key + Value: interpolated, + } + fields = append(fields, field) + } + return &ExprStruct{ + typ: obj.typ, + Fields: fields, + }, nil +} + +// SetScope stores the scope for later use in this resource and it's children, +// which it propogates this downwards to. +func (obj *ExprStruct) SetScope(scope *interfaces.Scope) error { + for _, x := range obj.Fields { + if err := x.Value.SetScope(scope); err != nil { + return err + } + } + return nil +} + +// SetType is used to set the type of this expression once it is known. This +// usually happens during type unification, but it can also happen during +// parsing if a type is specified explicitly. Since types are static and don't +// change on expressions, if you attempt to set a different type than what has +// previously been set (when not initially known) this will error. +func (obj *ExprStruct) SetType(typ *types.Type) error { + // TODO: should we ensure this is set to a KindStruct ? + if obj.typ != nil { + return obj.typ.Cmp(typ) // if not set, ensure it doesn't change + } + obj.typ = typ // set + return nil +} + +// Type returns the type of this expression. +func (obj *ExprStruct) Type() (*types.Type, error) { + var m = make(map[string]*types.Type) + ord := []string{} + var err error + for i, x := range obj.Fields { + // vals + t, e := x.Value.Type() + if e != nil { + err = errwrap.Wrapf(e, "field val, index `%s` did not return a type", i) + break + } + if _, exists := m[x.Name]; exists { + err = fmt.Errorf("struct type field index `%d` already exists", i) + break + } + m[x.Name] = t + ord = append(ord, x.Name) + } + if err == nil && obj.typ == nil && len(obj.Fields) > 0 { + return &types.Type{ // speculate! + Kind: types.KindStruct, + Map: m, + Ord: ord, // assume same order as fields + }, nil + } + + if obj.typ == nil { + return nil, interfaces.ErrTypeCurrentlyUnknown + } + return obj.typ, nil +} + +// Unify returns the list of invariants that this node produces. It recursively +// calls Unify on any children elements that exist in the AST, and returns the +// collection to the caller. +func (obj *ExprStruct) Unify() ([]interfaces.Invariant, error) { + var invariants []interfaces.Invariant + + // if this was set explicitly by the parser + if obj.typ != nil { + invar := &unification.EqualsInvariant{ + Expr: obj, + Type: obj.typ, + } + invariants = append(invariants, invar) + } + + // collect all the invariants of each sub-expression + for _, x := range obj.Fields { + invars, err := x.Value.Unify() + if err != nil { + return nil, err + } + invariants = append(invariants, invars...) + } + + // build the reference to ourself if we have undetermined field types + mapped := make(map[string]interfaces.Expr) + ordered := []string{} + for _, x := range obj.Fields { + mapped[x.Name] = x.Value + ordered = append(ordered, x.Name) + } + invariant := &unification.EqualityWrapStructInvariant{ + Expr1: obj, // unique id for this expression (a pointer) + Expr2Map: mapped, + Expr2Ord: ordered, + } + invariants = append(invariants, invariant) + + return invariants, nil +} + +// Graph returns the reactive function graph which is expressed by this node. It +// includes any vertices produced by this node, and the appropriate edges to any +// vertices that are produced by its children. Nodes which fulfill the Expr +// interface directly produce vertices (and possible children) where as nodes +// that fulfill the Stmt interface do not produces vertices, where as their +// children might. This returns a graph with a single vertex (itself) in it, and +// the edges from all of the child graphs to this. +func (obj *ExprStruct) Graph() (*pgraph.Graph, error) { + graph, err := pgraph.NewGraph("struct") + if err != nil { + return nil, errwrap.Wrapf(err, "could not create graph") + } + graph.AddVertex(obj) + + // each struct field needs to point to the final struct expression + for _, x := range obj.Fields { // struct fields in order + g, err := x.Value.Graph() + if err != nil { + return nil, err + } + + fieldName := x.Name + edge := &funcs.Edge{Args: []string{fieldName}} + + var once bool + edgeGenFn := func(v1, v2 pgraph.Vertex) pgraph.Edge { + if once { + panic(fmt.Sprintf("edgeGenFn for struct, arg `%s` was called twice", fieldName)) + } + once = true + return edge + } + graph.AddEdgeGraphVertexLight(g, obj, edgeGenFn) // arg -> func + } + + return graph, nil +} + +// Func returns the reactive stream of values that this expression produces. +func (obj *ExprStruct) Func() (interfaces.Func, error) { + typ, err := obj.Type() + if err != nil { + return nil, err + } + + // composite func (list, map, struct) + return &structs.CompositeFunc{ + Type: typ, + }, nil +} + +// SetValue here is a no-op, because algorithmically when this is called from +// the func engine, the child fields (the struct elements) will have had this +// done to them first, and as such when we try and retrieve the set value from +// this expression by calling `Value`, it will build it from scratch! +func (obj *ExprStruct) SetValue(value types.Value) error { + if err := obj.typ.Cmp(value.Type()); err != nil { + return err + } + // noop! + //obj.V = value + return nil +} + +// Value returns the value of this expression in our type system. This will +// usually only be valid once the engine has run and values have been produced. +// This might get called speculatively (early) during unification to learn more. +func (obj *ExprStruct) Value() (types.Value, error) { + fields := make(map[string]types.Value) + typ := &types.Type{ + Kind: types.KindStruct, + Map: make(map[string]*types.Type), + //Ord: obj.typ.Ord, // assume same order + } + ord := []string{} // can't use obj.typ b/c it can be nil during speculation + + for i, x := range obj.Fields { + // vals + t, err := x.Value.Type() + if err != nil { + return nil, errwrap.Wrapf(err, "field val, index `%s` did not return a type", i) + } + if _, exists := typ.Map[x.Name]; exists { + return nil, fmt.Errorf("struct type field index `%d` already exists", i) + } + typ.Map[x.Name] = t + + val, err := x.Value.Value() + if err != nil { + return nil, err + } + if val == nil { + return nil, fmt.Errorf("val for field index `%d` was nil", i) + } + + if _, exists := fields[x.Name]; exists { + return nil, fmt.Errorf("struct field index `%d` already exists", i) + } + fields[x.Name] = val // add to map + ord = append(ord, x.Name) + } + typ.Ord = ord + if len(obj.Fields) > 0 { + // Run SetType to ensure type is consistent with what we found, + // which is an easy way to ensure the Cmp passes as expected... + if err := obj.SetType(typ); err != nil { + return nil, errwrap.Wrapf(err, "type did not match expected!") + } + } + + return &types.StructValue{ + T: obj.typ, + V: fields, + }, nil +} + +// ExprStructField represents a name value pair in a struct field. This does not +// satisfy the Expr interface. +type ExprStructField struct { + Name string + Value interfaces.Expr +} + +// ExprFunc is a representation of a function value. This is not a function +// call, that is represented by ExprCall. +// XXX: this is currently not fully implemented, and parts may be incorrect. +type ExprFunc struct { + typ *types.Type + + V func([]types.Value) (types.Value, error) +} + +// String returns a short representation of this expression. +// FIXME: fmt.Sprintf("func(%+v)", obj.V) fails `go vet` (bug?), so wait until +// we have a better printable function value and put that here instead. +func (obj *ExprFunc) String() string { return fmt.Sprintf("func(???)") } // TODO: print nicely + +// Interpolate returns a new node (or itself) once it has been expanded. This +// generally increases the size of the AST when it is used. It calls Interpolate +// on any child elements and builds the new node with those new node contents. +// Here it simply returns itself, as no interpolation is possible. +func (obj *ExprFunc) Interpolate() (interfaces.Expr, error) { return obj, nil } + +// SetScope does nothing for this struct, because it has no child nodes, and it +// does not need to know about the parent scope. +// XXX: this may not be true in the future... +func (obj *ExprFunc) SetScope(*interfaces.Scope) error { return nil } + +// SetType is used to set the type of this expression once it is known. This +// usually happens during type unification, but it can also happen during +// parsing if a type is specified explicitly. Since types are static and don't +// change on expressions, if you attempt to set a different type than what has +// previously been set (when not initially known) this will error. +func (obj *ExprFunc) SetType(typ *types.Type) error { + // TODO: should we ensure this is set to a KindFunc ? + if obj.typ != nil { + return obj.typ.Cmp(typ) // if not set, ensure it doesn't change + } + obj.typ = typ // set + return nil +} + +// Type returns the type of this expression. +func (obj *ExprFunc) Type() (*types.Type, error) { + // TODO: implement speculative type lookup (if not already sufficient) + if obj.typ == nil { + return nil, interfaces.ErrTypeCurrentlyUnknown + } + return obj.typ, nil +} + +// Unify returns the list of invariants that this node produces. It recursively +// calls Unify on any children elements that exist in the AST, and returns the +// collection to the caller. +func (obj *ExprFunc) Unify() ([]interfaces.Invariant, error) { + return nil, fmt.Errorf("not implemented") // XXX: not implemented +} + +// Graph returns the reactive function graph which is expressed by this node. It +// includes any vertices produced by this node, and the appropriate edges to any +// vertices that are produced by its children. Nodes which fulfill the Expr +// interface directly produce vertices (and possible children) where as nodes +// that fulfill the Stmt interface do not produces vertices, where as their +// children might. This returns a graph with a single vertex (itself) in it. +func (obj *ExprFunc) Graph() (*pgraph.Graph, error) { + return nil, fmt.Errorf("not implemented") // XXX: not implemented +} + +// Func returns the reactive stream of values that this expression produces. +func (obj *ExprFunc) Func() (interfaces.Func, error) { + return nil, fmt.Errorf("not implemented") // XXX: not implemented +} + +// SetValue for a func expression is always populated statically, and does not +// ever receive any incoming values (no incoming edges) so this should never be +// called. It has been implemented for uniformity. +func (obj *ExprFunc) SetValue(value types.Value) error { + if err := obj.typ.Cmp(value.Type()); err != nil { + return err + } + return nil +} + +// Value returns the value of this expression in our type system. This will +// usually only be valid once the engine has run and values have been produced. +// This might get called speculatively (early) during unification to learn more. +// This particular value is always known since it is a constant. +func (obj *ExprFunc) Value() (types.Value, error) { + // TODO: implement speculative value lookup (if not already sufficient) + return &types.FuncValue{ + V: obj.V, + T: obj.typ, + }, nil +} + +// ExprCall is a representation of a function call. This does not represent the +// declaration or implementation of a new function value. +type ExprCall struct { + typ *types.Type + + V types.Value // stored result (set with SetValue) + + Name string + Args []interfaces.Expr // list of args in parsed order +} + +// String returns a short representation of this expression. +func (obj *ExprCall) String() string { + var s []string + for _, x := range obj.Args { + s = append(s, fmt.Sprintf("%s", x.String())) + } + return fmt.Sprintf("call:%s(%s)", obj.Name, strings.Join(s, ", ")) +} + +// buildType builds the KindFunc type of this function's signature if it can. It +// might not be able to if type unification hasn't yet been performed on this +// expression, and if SetType hasn't yet been called for the needed expressions. +// XXX: review this function logic please +func (obj *ExprCall) buildType() (*types.Type, error) { + + m := make(map[string]*types.Type) + ord := []string{} + for pos, x := range obj.Args { // function arguments in order + t, err := x.Type() + if err != nil { + return nil, err + } + name := util.NumToAlpha(pos) // assume (incorrectly) for now... + //name := argNames[pos] + m[name] = t + ord = append(ord, name) + } + + out, err := obj.Type() + if err != nil { + return nil, err + } + + return &types.Type{ + Kind: types.KindFunc, + Map: m, + Ord: ord, + Out: out, + }, nil +} + +// buildFunc prepares and returns the function struct object needed for running +// this function execution. +// XXX: review this function logic please +func (obj *ExprCall) buildFunc() (interfaces.Func, error) { + // TODO: if we have locally defined functions that can exist in scope, + // then perhaps we should do a lookup here before we use the built-in. + //fn, exists := obj.scope.Functions[obj.Name] // look for a local function + // Remember that a local function might have Invariants it needs to add! + + fn, err := funcs.Lookup(obj.Name) // lookup the function by name + if err != nil { + return nil, errwrap.Wrapf(err, "func `%s` could not be found", obj.Name) + } + + polyFn, ok := fn.(interfaces.PolyFunc) // is it statically polymorphic? + if !ok { + return fn, nil + } + + // PolyFunc's need more things done! + typ, err := obj.buildType() + if err == nil { // if we've errored, that's okay, this part isn't ready + if err := polyFn.Build(typ); err != nil { + return nil, errwrap.Wrapf(err, "could not build func `%s`", obj.Name) + } + } + return fn, nil +} + +// Interpolate returns a new node (or itself) once it has been expanded. This +// generally increases the size of the AST when it is used. It calls Interpolate +// on any child elements and builds the new node with those new node contents. +func (obj *ExprCall) Interpolate() (interfaces.Expr, error) { + args := []interfaces.Expr{} + for _, x := range obj.Args { + interpolated, err := x.Interpolate() + if err != nil { + return nil, err + } + args = append(args, interpolated) + } + return &ExprCall{ + typ: obj.typ, + Name: obj.Name, + Args: args, + }, nil +} + +// SetScope stores the scope for later use in this resource and it's children, +// which it propogates this downwards to. +func (obj *ExprCall) SetScope(scope *interfaces.Scope) error { + for _, x := range obj.Args { + if err := x.SetScope(scope); err != nil { + return err + } + } + return nil +} + +// SetType is used to set the type of this expression once it is known. This +// usually happens during type unification, but it can also happen during +// parsing if a type is specified explicitly. Since types are static and don't +// change on expressions, if you attempt to set a different type than what has +// previously been set (when not initially known) this will error. Remember that +// for this function expression, the type is the *return type* of the function, +// not the full type of the function signature. +func (obj *ExprCall) SetType(typ *types.Type) error { + if obj.typ != nil { + return obj.typ.Cmp(typ) // if not set, ensure it doesn't change + } + obj.typ = typ // set + return nil +} + +// Type returns the type of this expression, which is the return type of the +// function call. +func (obj *ExprCall) Type() (*types.Type, error) { + fn, err := funcs.Lookup(obj.Name) // lookup the function by name + _, isPoly := fn.(interfaces.PolyFunc) // is it statically polymorphic? + if err == nil && obj.typ == nil && !isPoly { + if info := fn.Info(); info != nil { + if sig := info.Sig; sig != nil { + if typ := sig.Out; typ != nil && !typ.HasVariant() { + return typ, nil // speculate! + } + } + } + } + + if obj.typ == nil { + return nil, interfaces.ErrTypeCurrentlyUnknown + } + return obj.typ, nil +} + +// Unify returns the list of invariants that this node produces. It recursively +// calls Unify on any children elements that exist in the AST, and returns the +// collection to the caller. +func (obj *ExprCall) Unify() ([]interfaces.Invariant, error) { + var invariants []interfaces.Invariant + + // if this was set explicitly by the parser + if obj.typ != nil { + invar := &unification.EqualsInvariant{ + Expr: obj, + Type: obj.typ, + } + invariants = append(invariants, invar) + } + + // collect all the invariants of each sub-expression + for _, x := range obj.Args { + invars, err := x.Unify() + if err != nil { + return nil, err + } + invariants = append(invariants, invars...) + } + + fn, err := obj.buildFunc() // uses obj.Name to build the func + if err != nil { + return nil, err + } + + // XXX: can we put this inside the poly branch or is it needed everywhere? + // XXX: is there code we can pull out of this branch to use for all functions? + argNames := []string{} + mapped := make(map[string]*types.Type) + partialValues := []types.Value{} + for i := range obj.Args { + name := util.NumToAlpha(i) // assume (incorrectly) for now... + argNames = append(argNames, name) + mapped[name] = nil // unknown type + partialValues = append(partialValues, nil) // XXX: is this safe? + + // optimization: if zeroth arg is a static string, specify this! + // TODO: this is a more specialized version of the next check... + if x, ok := obj.Args[0].(*ExprStr); i == 0 && ok { // is static? + mapped[name], _ = x.Type() + partialValues[i], _ = x.Value() // store value + } + + // optimization: if type is already known, specify it now! + if t, err := obj.Args[i].Type(); err == nil { // is known? + mapped[name] = t + // if value is completely static, pass it in now! + if v, err := obj.Args[i].Value(); err == nil { + partialValues[i] = v // store value + } + } + } + + // do we have a special case like the operator or template function? + polyFn, ok := fn.(interfaces.PolyFunc) // is it statically polymorphic? + if ok { + out, err := obj.Type() // do we know the return type yet? + if err != nil { + out = nil // just to make sure... + } + // partial type can have some type components that are nil! + // this means they are not yet known at this time... + partialType := &types.Type{ + Kind: types.KindFunc, + Map: mapped, + Ord: argNames, + Out: out, // possibly nil + } + + results, err := polyFn.Polymorphisms(partialType, partialValues) + if err != nil { + return nil, errwrap.Wrapf(err, "polymorphic signatures for func `%s` could not be found", obj.Name) + } + + ors := []interfaces.Invariant{} // solve only one from this list + // each of these is a different possible signature + for _, typ := range results { + if typ.Kind != types.KindFunc { + panic("overloaded result was not of kind func") + } + + // XXX: how do we deal with template returning a variant? + // XXX: i think we need more invariant types, and if it's + // going to be a variant, just return no results, and the + // defaults from the engine should just match it anyways! + if typ.HasVariant() { // XXX: ¯\_(ツ)_/¯ + //continue // XXX: alternate strategy... + //return nil, fmt.Errorf("variant type not yet supported, got: %+v", typ) // XXX: old strategy + } + if typ.Kind == types.KindVariant { // XXX: ¯\_(ツ)_/¯ + continue // can't deal with raw variant a.t.m. + } + + if i, j := len(typ.Ord), len(obj.Args); i != j { + continue // this signature won't work for us, skip! + } + + // what would a set of invariants for this sig look like? + var invars []interfaces.Invariant + + // use Map and Ord for Input (Kind == Function) + for i, x := range typ.Ord { + if typ.Map[x].HasVariant() { // XXX: ¯\_(ツ)_/¯ + invar := &unification.AnyInvariant{ // XXX: ??? + Expr: obj.Args[i], + } + invars = append(invars, invar) + continue + } + invar := &unification.EqualsInvariant{ + Expr: obj.Args[i], + Type: typ.Map[x], // type of arg + } + invars = append(invars, invar) + } + if typ.Out != nil { + // this expression should equal the output type of the function + if typ.Out.HasVariant() { // XXX: ¯\_(ツ)_/¯ + invar := &unification.AnyInvariant{ // XXX: ??? + Expr: obj, + } + invars = append(invars, invar) + } else { + invar := &unification.EqualsInvariant{ + Expr: obj, + Type: typ.Out, + } + invars = append(invars, invar) + } + } + + // add more invariants to link the partials... + mapped := make(map[string]interfaces.Expr) + ordered := []string{} + for pos, x := range obj.Args { + name := argNames[pos] + mapped[name] = x + ordered = append(ordered, name) + } + + // unused expression, here only for linking... + // TODO: eventually like with proper ExprFunc in lang? + exprFunc := &ExprFunc{} + if !typ.HasVariant() { // XXX: ¯\_(ツ)_/¯ + exprFunc.SetType(typ) + funcInvariant := &unification.EqualsInvariant{ + Expr: exprFunc, + Type: typ, + } + invars = append(invars, funcInvariant) + } + invar := &unification.EqualityWrapFuncInvariant{ + Expr1: exprFunc, + Expr2Map: mapped, + Expr2Ord: ordered, + Expr2Out: obj, // type of expression is return type of function + } + invars = append(invars, invar) + + // all of these need to be true together + and := &unification.ConjunctionInvariant{ + Invariants: invars, + } + + ors = append(ors, and) // one solution added! + } // end results loop + + // don't error here, we might not want to add any invariants! + //if len(results) == 0 { + // return nil, fmt.Errorf("can't find any valid signatures that match func `%s`", obj.Name) + //} + if len(ors) > 0 { + var invar interfaces.Invariant = &unification.ExclusiveInvariant{ + Invariants: ors, // one and only one of these should be true + } + if len(ors) == 1 { + invar = ors[0] // there should only be one + } + invariants = append(invariants, invar) + } + + } else { + sig := fn.Info().Sig + // build the reference to ourself if we have undetermined arg types + mapped := make(map[string]interfaces.Expr) + ordered := []string{} + for pos, x := range obj.Args { + name := argNames[pos] + mapped[name] = x + ordered = append(ordered, name) + } + + // add an unused expression, because we need to link it to the partial + exprFunc := &ExprFunc{} + exprFunc.SetType(sig) + funcInvariant := &unification.EqualsInvariant{ + Expr: exprFunc, + Type: sig, + } + invariants = append(invariants, funcInvariant) + + // note: the usage of this invariant is different from the other wrap* + // invariants, because in this case, the expression type is the return + // type which is produced, where as the entire function itself has its + // own type which includes the types of the input arguments... + invariant := &unification.EqualityWrapFuncInvariant{ + Expr1: exprFunc, // unused placeholder for unification + Expr2Map: mapped, + Expr2Ord: ordered, + Expr2Out: obj, // type of expression is return type of function + } + invariants = append(invariants, invariant) + } + + return invariants, nil +} + +// Graph returns the reactive function graph which is expressed by this node. It +// includes any vertices produced by this node, and the appropriate edges to any +// vertices that are produced by its children. Nodes which fulfill the Expr +// interface directly produce vertices (and possible children) where as nodes +// that fulfill the Stmt interface do not produces vertices, where as their +// children might. This returns a graph with a single vertex (itself) in it, and +// the edges from all of the child graphs to this. +func (obj *ExprCall) Graph() (*pgraph.Graph, error) { + graph, err := pgraph.NewGraph("func") + if err != nil { + return nil, errwrap.Wrapf(err, "could not create graph") + } + graph.AddVertex(obj) + + fn, err := obj.buildFunc() // uses obj.Name to build the func + if err != nil { + return nil, err + } + argNames := fn.Info().Sig.Ord + if len(argNames) != len(obj.Args) { // extra safety... + return nil, fmt.Errorf("func `%s` expected %d args, got %d", obj.Name, len(argNames), len(obj.Args)) + } + + // each function argument needs to point to the final function expression + for pos, x := range obj.Args { // function arguments in order + g, err := x.Graph() + if err != nil { + return nil, err + } + + argName := argNames[pos] + edge := &funcs.Edge{Args: []string{argName}} + + var once bool + edgeGenFn := func(v1, v2 pgraph.Vertex) pgraph.Edge { + if once { + panic(fmt.Sprintf("edgeGenFn for func `%s`, arg `%s` was called twice", obj.Name, argName)) + } + once = true + return edge + } + graph.AddEdgeGraphVertexLight(g, obj, edgeGenFn) // arg -> func + } + + return graph, nil +} + +// Func returns the reactive stream of values that this expression produces. +func (obj *ExprCall) Func() (interfaces.Func, error) { + return obj.buildFunc() // uses obj.Name to build the func +} + +// SetValue here is used to store the result of the last computation of this +// expression node after it has received all the required input values. This +// value is cached and can be retrieved by calling Value. +func (obj *ExprCall) SetValue(value types.Value) error { + if err := obj.typ.Cmp(value.Type()); err != nil { + return err + } + obj.V = value + return nil +} + +// Value returns the value of this expression in our type system. This will +// usually only be valid once the engine has run and values have been produced. +// This might get called speculatively (early) during unification to learn more. +// It is often unlikely that this kind of speculative execution finds something. +// This particular implementation of the function returns the previously stored +// and cached value as received by SetValue. +func (obj *ExprCall) Value() (types.Value, error) { + if obj.V == nil { + return nil, fmt.Errorf("func value does not yet exist") + } + return obj.V, nil +} + +// ExprVar is a representation of a variable lookup. It returns the expression +// that that variable refers to. +type ExprVar struct { + scope *interfaces.Scope // store for referencing this later + typ *types.Type + + Name string // name of the variable +} + +// String returns a short representation of this expression. +func (obj *ExprVar) String() string { return fmt.Sprintf("var(%s)", obj.Name) } + +// Interpolate returns a new node (or itself) once it has been expanded. This +// generally increases the size of the AST when it is used. It calls Interpolate +// on any child elements and builds the new node with those new node contents. +// Here it returns itself, since variable names cannot be interpolated. We don't +// support variable, variables or anything crazy like that. +func (obj *ExprVar) Interpolate() (interfaces.Expr, error) { return obj, nil } + +// SetScope stores the scope for use in this resource. +func (obj *ExprVar) SetScope(scope *interfaces.Scope) error { + if scope == nil { + scope = scope.Empty() + } + obj.scope = scope + return nil +} + +// SetType is used to set the type of this expression once it is known. This +// usually happens during type unification, but it can also happen during +// parsing if a type is specified explicitly. Since types are static and don't +// change on expressions, if you attempt to set a different type than what has +// previously been set (when not initially known) this will error. +func (obj *ExprVar) SetType(typ *types.Type) error { + if obj.typ != nil { + return obj.typ.Cmp(typ) // if not set, ensure it doesn't change + } + obj.typ = typ // set + return nil +} + +// Type returns the type of this expression. +func (obj *ExprVar) Type() (*types.Type, error) { + // return type if it is already known statically... + // it is useful for type unification to have some extra info + expr, exists := obj.scope.Variables[obj.Name] + // if !exists, just ignore the error for now since this is speculation! + // this logic simplifies down to just this! + if exists && obj.typ == nil { + return expr.Type() + } + + if obj.typ == nil { + return nil, interfaces.ErrTypeCurrentlyUnknown + } + return obj.typ, nil +} + +// Unify returns the list of invariants that this node produces. It recursively +// calls Unify on any children elements that exist in the AST, and returns the +// collection to the caller. +func (obj *ExprVar) Unify() ([]interfaces.Invariant, error) { + var invariants []interfaces.Invariant + + // lookup value from scope + expr, exists := obj.scope.Variables[obj.Name] + if !exists { + return nil, fmt.Errorf("var `%s` does not exist in this scope", obj.Name) + } + + // don't recurse because we already got this through the bind statement + //invars, err := expr.Unify() + //if err != nil { + // return nil, err + //} + //invariants = append(invariants, invars...) + + // this expression's type must be the type of what the var is bound to! + // TODO: does this always cause an identical duplicate invariant? + invar := &unification.EqualityInvariant{ + Expr1: obj, + Expr2: expr, + } + invariants = append(invariants, invar) + + return invariants, nil +} + +// Graph returns the reactive function graph which is expressed by this node. It +// includes any vertices produced by this node, and the appropriate edges to any +// vertices that are produced by its children. Nodes which fulfill the Expr +// interface directly produce vertices (and possible children) where as nodes +// that fulfill the Stmt interface do not produces vertices, where as their +// children might. This returns a graph with a single vertex (itself) in it, and +// the edges from all of the child graphs to this. The child graph in this case +// is the graph which is obtained from the bound expression. The edge points +// from that expression to this vertex. The function used for this vertex is a +// simple placeholder which sucks incoming values in and passes them on. This is +// important for filling the logical requirements of the graph type checker, and +// to avoid duplicating production of the incoming input value from the bound +// expression. +func (obj *ExprVar) Graph() (*pgraph.Graph, error) { + graph, err := pgraph.NewGraph("var") + if err != nil { + return nil, errwrap.Wrapf(err, "could not create graph") + } + graph.AddVertex(obj) + + // ??? = $foo (this is the foo) + // lookup value from scope + expr, exists := obj.scope.Variables[obj.Name] + if !exists { + return nil, fmt.Errorf("var `%s` does not exist in this scope", obj.Name) + } + + // should already exist in graph (i think)... + graph.AddVertex(expr) // duplicate additions are ignored and are harmless + + // the expr needs to point to the var lookup expression + g, err := expr.Graph() + if err != nil { + return nil, err + } + + edge := &funcs.Edge{Args: []string{obj.Name}} + + var once bool + edgeGenFn := func(v1, v2 pgraph.Vertex) pgraph.Edge { + if once { + panic(fmt.Sprintf("edgeGenFn for var `%s` was called twice", obj.Name)) + } + once = true + return edge + } + graph.AddEdgeGraphVertexLight(g, obj, edgeGenFn) // expr -> var + + return graph, nil +} + +// Func returns a "pass-through" function which receives the bound value, and +// passes it to the consumer. This is essential for satisfying the type checker +// of the function graph engine. +func (obj *ExprVar) Func() (interfaces.Func, error) { + expr, exists := obj.scope.Variables[obj.Name] + if !exists { + return nil, fmt.Errorf("var `%s` does not exist in scope", obj.Name) + } + + // this is wrong, if we did it this way, this expr wouldn't exist as a + // distinct node in the function graph to relay values through, instead, + // it would be acting as a "substitution/lookup" function, which just + // copies the bound function over into here. As a result, we'd have N + // copies of that function (based on the number of times N that that + // variable is used) instead of having that single bound function as + // input which is sent via N different edges to the multiple locations + // where the variables are used. Since the bound function would still + // have a single unique pointer, this wouldn't actually exist more than + // once in the graph, although since it's illogical, it causes the graph + // type checking (the edge counting in the function graph engine) to + // notice a problem and error. + //return expr.Func() // recurse? + + // instead, return a function which correctly does a lookup in the scope + // and returns *that* stream of values instead. + typ, err := obj.Type() + if err != nil { + return nil, err + } + + f, err := expr.Func() + if err != nil { + return nil, err + } + + // var func + return &structs.VarFunc{ + Type: typ, + Func: f, + Edge: obj.Name, // the edge name used above in Graph is this... + }, nil +} + +// SetValue here is a no-op, because algorithmically when this is called from +// the func engine, the child fields (the dest lookup expr) will have had this +// done to them first, and as such when we try and retrieve the set value from +// this expression by calling `Value`, it will build it from scratch! +func (obj *ExprVar) SetValue(value types.Value) error { + if err := obj.typ.Cmp(value.Type()); err != nil { + return err + } + // noop! + //obj.V = value + return nil +} + +// Value returns the value of this expression in our type system. This will +// usually only be valid once the engine has run and values have been produced. +// This might get called speculatively (early) during unification to learn more. +// This returns the value this variable points to. It is able to do so because +// it can lookup in the previous set scope which expression this points to, and +// then it can call Value on that expression. +func (obj *ExprVar) Value() (types.Value, error) { + expr, exists := obj.scope.Variables[obj.Name] + if !exists { + return nil, fmt.Errorf("var `%s` does not exist in scope", obj.Name) + } + return expr.Value() // recurse +} + +// ExprIf represents an if expression which *must* have both branches, and which +// returns a value. As a result, it has a type. This is different from a StmtIf, +// which does not need to have both branches, and which does not return a value. +type ExprIf struct { + typ *types.Type + + Condition interfaces.Expr + ThenBranch interfaces.Expr // could be an ExprBranch + ElseBranch interfaces.Expr // could be an ExprBranch +} + +// String returns a short representation of this expression. +func (obj *ExprIf) String() string { + return fmt.Sprintf("if(%s)", obj.Condition.String()) // TODO: improve this +} + +// Interpolate returns a new node (or itself) once it has been expanded. This +// generally increases the size of the AST when it is used. It calls Interpolate +// on any child elements and builds the new node with those new node contents. +func (obj *ExprIf) Interpolate() (interfaces.Expr, error) { + condition, err := obj.Condition.Interpolate() + if err != nil { + return nil, errwrap.Wrapf(err, "could not interpolate Condition") + } + thenBranch, err := obj.ThenBranch.Interpolate() + if err != nil { + return nil, errwrap.Wrapf(err, "could not interpolate ThenBranch") + } + elseBranch, err := obj.ElseBranch.Interpolate() + if err != nil { + return nil, errwrap.Wrapf(err, "could not interpolate ElseBranch") + } + return &ExprIf{ + typ: obj.typ, + Condition: condition, + ThenBranch: thenBranch, + ElseBranch: elseBranch, + }, nil +} + +// SetScope stores the scope for later use in this resource and it's children, +// which it propogates this downwards to. +func (obj *ExprIf) SetScope(scope *interfaces.Scope) error { + if err := obj.Condition.SetScope(scope); err != nil { + return err + } + if err := obj.ThenBranch.SetScope(scope); err != nil { + return err + } + if err := obj.ElseBranch.SetScope(scope); err != nil { + return err + } + return nil +} + +// SetType is used to set the type of this expression once it is known. This +// usually happens during type unification, but it can also happen during +// parsing if a type is specified explicitly. Since types are static and don't +// change on expressions, if you attempt to set a different type than what has +// previously been set (when not initially known) this will error. +func (obj *ExprIf) SetType(typ *types.Type) error { + if obj.typ != nil { + return obj.typ.Cmp(typ) // if not set, ensure it doesn't change + } + obj.typ = typ // set + return nil +} + +// Type returns the type of this expression. +func (obj *ExprIf) Type() (*types.Type, error) { + boolValue, err := obj.Condition.Value() // attempt early speculation + if err == nil && obj.typ == nil { + branch := obj.ElseBranch + if boolValue.Bool() { // must not panic + branch = obj.ThenBranch + } + return branch.Type() + } + + if obj.typ == nil { + return nil, interfaces.ErrTypeCurrentlyUnknown + } + return obj.typ, nil +} + +// Unify returns the list of invariants that this node produces. It recursively +// calls Unify on any children elements that exist in the AST, and returns the +// collection to the caller. +func (obj *ExprIf) Unify() ([]interfaces.Invariant, error) { + var invariants []interfaces.Invariant + + // conditional expression might have some children invariants to share + condition, err := obj.Condition.Unify() + if err != nil { + return nil, err + } + invariants = append(invariants, condition...) + + // the condition must ultimately be a boolean + conditionInvar := &unification.EqualsInvariant{ + Expr: obj.Condition, + Type: types.TypeBool, + } + invariants = append(invariants, conditionInvar) + + // recurse into the two branches + thenBranch, err := obj.ThenBranch.Unify() + if err != nil { + return nil, err + } + invariants = append(invariants, thenBranch...) + + elseBranch, err := obj.ElseBranch.Unify() + if err != nil { + return nil, err + } + invariants = append(invariants, elseBranch...) + + // the two branches must be equally typed + branchesInvar := &unification.EqualityInvariant{ + Expr1: obj.ThenBranch, + Expr2: obj.ElseBranch, + } + invariants = append(invariants, branchesInvar) + + return invariants, nil +} + +// Graph returns the reactive function graph which is expressed by this node. It +// includes any vertices produced by this node, and the appropriate edges to any +// vertices that are produced by its children. Nodes which fulfill the Expr +// interface directly produce vertices (and possible children) where as nodes +// that fulfill the Stmt interface do not produces vertices, where as their +// children might. This particular if expression doesn't do anything clever here +// other than adding in both branches of the graph. Since we're functional, this +// shouldn't have any ill effects. +// XXX: is this completely true if we're running technically impure, but safe +// built-in functions on both branches? Can we turn off half of this? +func (obj *ExprIf) Graph() (*pgraph.Graph, error) { + graph, err := pgraph.NewGraph("if") + if err != nil { + return nil, errwrap.Wrapf(err, "could not create graph") + } + graph.AddVertex(obj) + + exprs := map[string]interfaces.Expr{ + "c": obj.Condition, + "a": obj.ThenBranch, + "b": obj.ElseBranch, + } + for _, argName := range []string{"c", "a", "b"} { // deterministic order + x := exprs[argName] + g, err := x.Graph() + if err != nil { + return nil, err + } + + edge := &funcs.Edge{Args: []string{argName}} + + var once bool + edgeGenFn := func(v1, v2 pgraph.Vertex) pgraph.Edge { + if once { + panic(fmt.Sprintf("edgeGenFn for ifexpr edge `%s` was called twice", argName)) + } + once = true + return edge + } + graph.AddEdgeGraphVertexLight(g, obj, edgeGenFn) // branch -> if + } + + return graph, nil +} + +// Func returns a function which returns the correct branch based on the ever +// changing conditional boolean input. +func (obj *ExprIf) Func() (interfaces.Func, error) { + typ, err := obj.Type() + if err != nil { + return nil, err + } + + return &structs.IfFunc{ + Type: typ, // this is the output type of the expression + }, nil +} + +// SetValue here is a no-op, because algorithmically when this is called from +// the func engine, the child fields (the branches expr's) will have had this +// done to them first, and as such when we try and retrieve the set value from +// this expression by calling `Value`, it will build it from scratch! +func (obj *ExprIf) SetValue(value types.Value) error { + if err := obj.typ.Cmp(value.Type()); err != nil { + return err + } + // noop! + //obj.V = value + return nil +} + +// Value returns the value of this expression in our type system. This will +// usually only be valid once the engine has run and values have been produced. +// This might get called speculatively (early) during unification to learn more. +// This particular expression evaluates the condition and returns the correct +// branch's value accordingly. +func (obj *ExprIf) Value() (types.Value, error) { + boolValue, err := obj.Condition.Value() + if err != nil { + return nil, err + } + if boolValue.Bool() { // must not panic + return obj.ThenBranch.Value() + } + return obj.ElseBranch.Value() +} diff --git a/lang/types/doc.go b/lang/types/doc.go new file mode 100644 index 00000000..9a5b9676 --- /dev/null +++ b/lang/types/doc.go @@ -0,0 +1,19 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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 types provides a framework for our language values and types. +package types diff --git a/lang/types/type.go b/lang/types/type.go new file mode 100644 index 00000000..df1fd460 --- /dev/null +++ b/lang/types/type.go @@ -0,0 +1,885 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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 types + +import ( + "fmt" + "reflect" + "strings" + + multierr "github.com/hashicorp/go-multierror" +) + +// Basic types defined here as a convenience for use with Type.Cmp(X). +var ( + TypeBool = NewType("bool") + TypeStr = NewType("str") + TypeInt = NewType("int") + TypeFloat = NewType("float") + TypeVariant = NewType("variant") +) + +//go:generate stringer -type=Kind -output=kind_stringer.go + +// The Kind represents the base type of each value. +type Kind int // this used to be called Type + +// Each Kind represents a type in the language type system. +const ( + KindNil Kind = iota + KindBool + KindStr + KindInt + KindFloat + KindList + KindMap + KindStruct + KindFunc + KindVariant +) + +// Type is the datastructure representing any type. It can be recursive for +// container types like lists, maps, and structs. +// TODO: should we create a `Type` interface? +type Type struct { + Kind Kind + + Val *Type // if Kind == List, use Val only + Key *Type // if Kind == Map, use Val and Key + Map map[string]*Type // if Kind == Struct, use Map and Ord (for order) + Ord []string + Out *Type // if Kind == Func, use Map and Ord for Input, Out for Output + Var *Type // if Kind == Variant, use Var only +} + +// TypeOf takes a reflect.Type and returns an equivalent *Type. It removes any +// pointers since our language does not support pointers. It returns nil if it +// cannot represent the type in our type system. Common examples of things it +// cannot express include reflect.Invalid, reflect.Interface, Reflect.Complex128 +// and more. It is not reversible because some information may be either added +// or lost. For example, reflect.Array and reflect.Slice are both converted to +// a Type of KindList, and KindFunc names the arguments of a func sequentially. +// The lossy inverse of this is Reflect. +func TypeOf(t reflect.Type) (*Type, error) { + typ := t + kind := typ.Kind() + for kind == reflect.Ptr { + typ = typ.Elem() // un-nest one pointer + kind = typ.Kind() + } + + switch kind { // match on destination field kind + case reflect.Bool: + return &Type{ + Kind: KindBool, + }, nil + + case reflect.String: + return &Type{ + Kind: KindStr, + }, nil + + case reflect.Int, reflect.Int64, reflect.Int32, reflect.Int16, reflect.Int8: + fallthrough + case reflect.Uint, reflect.Uint64, reflect.Uint32, reflect.Uint16, reflect.Uint8: + // we have only one kind of int type + return &Type{ + Kind: KindInt, + }, nil + + case reflect.Float64, reflect.Float32: + return &Type{ + Kind: KindFloat, + }, nil + + case reflect.Array, reflect.Slice: + val, err := TypeOf(typ.Elem()) + if err != nil { + return nil, err + } + + return &Type{ + Kind: KindList, + Val: val, + }, nil + + case reflect.Map: + key, err := TypeOf(typ.Key()) // Key returns a map type's key type. + if err != nil { + return nil, err + } + val, err := TypeOf(typ.Elem()) // Elem returns a type's element type. + if err != nil { + return nil, err + } + + return &Type{ + Kind: KindMap, + Key: key, + Val: val, + }, nil + + case reflect.Struct: + m := make(map[string]*Type) + ord := []string{} + + for i := 0; i < typ.NumField(); i++ { + field := typ.Field(i) + tt, err := TypeOf(field.Type) + if err != nil { + return nil, err + } + // TODO: should we skip over fields with field.Anonymous ? + m[field.Name] = tt + ord = append(ord, field.Name) // in order + // NOTE: we discard the field.Tag data + } + + return &Type{ + Kind: KindStruct, + Map: m, + Ord: ord, + }, nil + + case reflect.Func: + m := make(map[string]*Type) + ord := []string{} + + for i := 0; i < typ.NumIn(); i++ { + tt, err := TypeOf(typ.In(i)) + if err != nil { + return nil, err + } + name := fmt.Sprintf("%d", i) // invent a function arg name + m[name] = tt + ord = append(ord, name) // in order + } + + var out *Type + var err error + // we currently leave out nil if there are no return values + if c := typ.NumOut(); c == 1 { + out, err = TypeOf(typ.Out(0)) + if err != nil { + return nil, err + } + } else if c > 1 { + // if we have multiple return values, we could return a + // struct, but for now let's just complain... + return nil, fmt.Errorf("func has %d return values", c) + } + // nothing special to do if type is variadic, it's a list... + //if typ.IsVariadic() { + //} + + return &Type{ + Kind: KindFunc, + Map: m, + Ord: ord, + Out: out, + }, nil + + // TODO: should this return a variant type? + //case reflect.Interface: + + default: + return nil, fmt.Errorf("unable to represent type of %s", typ.String()) + } +} + +// NewType creates the Type from the string representation. +func NewType(s string) *Type { + switch s { + case "bool": + return &Type{ + Kind: KindBool, + } + case "str": + return &Type{ + Kind: KindStr, + } + case "int": + return &Type{ + Kind: KindInt, + } + case "float": + return &Type{ + Kind: KindFloat, + } + } + + // KindList + if strings.HasPrefix(s, "[]") { + val := NewType(s[len("[]"):]) + if val == nil { + return nil + } + return &Type{ + Kind: KindList, + Val: val, + } + } + + // KindMap + if strings.HasPrefix(s, "{") && strings.HasSuffix(s, "}") { + s := s[1 : len(s)-1] + if s == "" { // it is empty + return nil + } + // {: } // map + var found int + var delta int + for i, c := range s { + if c == '{' { // open + delta++ + } + if c == '}' { // close + delta-- + } + if c == ':' && delta == 0 { + found = i + } + } + if found == 0 || delta != 0 { // nope if we fall off the end... + return nil + } + + key := NewType(strings.Trim(s[:found], " ")) + if key == nil { + return nil + } + val := NewType(strings.Trim(s[found+1:], " ")) + if val == nil { + return nil + } + return &Type{ + Kind: KindMap, + Key: key, + Val: val, + } + } + + // KindStruct + if strings.HasPrefix(s, "struct{") && strings.HasSuffix(s, "}") { + s := s[len("struct{") : len(s)-1] + keys := []string{} + tmap := make(map[string]*Type) + + for { // while we still have struct pairs to process... + s = strings.Trim(s, " ") + if s == "" { + break // done + } + + sep := strings.Index(s, " ") + if sep <= 0 { + return nil + } + key := s[:sep] // FIXME: check there are no special chars in key + keys = append(keys, key) + + s = s[sep+1:] // what's next + + var found int + var delta int + for i, c := range s { + if c == '{' { // open + delta++ + } + if c == '}' { // close + delta-- + } + if c == ';' && delta == 0 { // is there nesting? + found = i + break // stop at first semicolon + } + } + if delta != 0 { // nope if we're still nested... + return nil + } + if found == 0 { // no semicolon + found = len(s) - 1 // last char + } + + var trim int + if s[found:found+1] == ";" { + trim = 1 + } + + typ := NewType(strings.Trim(s[:found+1-trim], " ")) + if typ == nil { + return nil + } + tmap[key] = typ // add type + s = s[found+1:] // what's left? + } + + return &Type{ + Kind: KindStruct, + Ord: keys, + Map: tmap, + } + } + + // KindFunc + if strings.HasPrefix(s, "func(") { + // find end of function... + var found int + var delta = 1 // we've got the first open bracket + for i := len("func("); i < len(s); i++ { + c := s[i] + if c == '(' { // open + delta++ + } + if c == ')' { // close + delta-- + } + if delta == 0 { + found = i + break + } + } + if delta != 0 { // nesting is not paired... + return nil + } + + out := strings.Trim(s[found+1:], " ") // return type + s := s[len("func("):found] // contents of function + keys := []string{} + tmap := make(map[string]*Type) + + for { // while we still have function arguments to process... + s = strings.Trim(s, " ") + if s == "" { + break // done + } + var key string + + // arg naming code, which allows for optional arg names + sep := strings.Index(s, " ") + if sep > 0 && !strings.HasSuffix(s[:sep], ",") { + key = s[:sep] // FIXME: check there are no special chars in key + s = s[sep+1:] // what's next + } + + // just name the keys 0, 1, 2, N... + if key == "" { + key = fmt.Sprintf("%d", len(keys)) + } + keys = append(keys, key) + + var found int + var delta int + for i, c := range s { + if c == '(' { // open + delta++ + } + if c == ')' { // close + delta-- + } + if c == ',' && delta == 0 { // is there nesting? + found = i + break // stop at first comma + } + } + if delta != 0 { // nope if we're still nested... + return nil + } + if found == 0 { // no comma + found = len(s) - 1 // last char + } + + var trim int + if s[found:found+1] == "," { + trim = 1 + } + + typ := NewType(strings.Trim(s[:found+1-trim], " ")) + if typ == nil { + return nil + } + tmap[key] = typ // add type + s = s[found+1:] // what's left? + } + + // return type + var tail *Type + if out != "" { // allow functions with no return type (in parser) + tail = NewType(out) + if tail == nil { + return nil + } + } + + return &Type{ + Kind: KindFunc, + Ord: keys, + Map: tmap, + Out: tail, + } + } + + // KindVariant + if s == "variant" { + return &Type{ + Kind: KindVariant, + } + } + + return nil // error (this also matches the empty string as input) +} + +// New creates a new Value of this type. +func (obj *Type) New() Value { + switch obj.Kind { + case KindBool: + return NewBool() + case KindStr: + return NewStr() + case KindInt: + return NewInt() + case KindFloat: + return NewFloat() + case KindList: + return NewList(obj) + case KindMap: + return NewMap(obj) + case KindStruct: + return NewStruct(obj) + case KindFunc: + return NewFunc(obj) + case KindVariant: + return NewVariant(obj) + } + panic("malformed type") +} + +// String returns the textual representation for this type. +func (obj *Type) String() string { + switch obj.Kind { + case KindBool: + return "bool" + case KindStr: + return "str" + case KindInt: + return "int" + case KindFloat: + return "float" + + case KindList: + if obj.Val == nil { + panic("malformed list type") + } + return "[]" + obj.Val.String() + + case KindMap: + if obj.Key == nil || obj.Val == nil { + panic("malformed map type") + } + return fmt.Sprintf("{%s: %s}", obj.Key.String(), obj.Val.String()) + + case KindStruct: // {a bool; b int} + if obj.Map == nil { + panic("malformed struct type") + } + if len(obj.Map) != len(obj.Ord) { + panic("malformed struct length") + } + var s = make([]string, len(obj.Ord)) + for i, k := range obj.Ord { + t, ok := obj.Map[k] + if !ok { + panic("malformed struct order") + } + if t == nil { + panic("malformed struct field") + } + s[i] = fmt.Sprintf("%s %s", k, t.String()) + } + return fmt.Sprintf("struct{%s}", strings.Join(s, "; ")) + + case KindFunc: + if obj.Map == nil { + panic("malformed func type") + } + if len(obj.Map) != len(obj.Ord) { + panic("malformed func length") + } + var s = make([]string, len(obj.Ord)) + for i, k := range obj.Ord { + t, ok := obj.Map[k] + if !ok { + panic("malformed func order") + } + if t == nil { + panic("malformed func field") + } + //s[i] = fmt.Sprintf("%s %s", k, t.String()) // strict + s[i] = t.String() + } + var out string + if obj.Out != nil { + out = fmt.Sprintf(" %s", obj.Out.String()) + } + return fmt.Sprintf("func(%s)%s", strings.Join(s, ", "), out) + + case KindVariant: + return "variant" + } + + panic("malformed type") +} + +// Cmp compares this type to another. +func (obj *Type) Cmp(typ *Type) error { + if obj == nil || typ == nil { + return fmt.Errorf("cannot compare to nil") + } + + // TODO: is this correct? + // recurse into variants if we want base type comparisons + //if obj.Kind == KindVariant { + // return obj.Var.Cmp(t) + //} + //if t.Kind == KindVariant { + // return obj.Cmp(t.Var) + //} + + if obj.Kind != typ.Kind { + return fmt.Errorf("base kind does not match (%+v != %+v)", obj.Kind, typ.Kind) + } + switch obj.Kind { + case KindBool: + return nil + case KindStr: + return nil + case KindInt: + return nil + case KindFloat: + return nil + + case KindList: + if obj.Val == nil || typ.Val == nil { + panic("malformed list type") + } + return obj.Val.Cmp(typ.Val) + + case KindMap: + if obj.Key == nil || obj.Val == nil || typ.Key == nil || typ.Val == nil { + panic("malformed map type") + } + kerr := obj.Key.Cmp(typ.Key) + verr := obj.Val.Cmp(typ.Val) + if kerr != nil && verr != nil { + return multierr.Append(kerr, verr) // two errors + } + if kerr != nil { + return kerr + } + if verr != nil { + return verr + } + return nil + + case KindStruct: // {a bool; b int} + if obj.Map == nil || typ.Map == nil { + panic("malformed struct type") + } + if len(obj.Ord) != len(typ.Ord) { + return fmt.Errorf("struct field count differs") + } + for i, k := range obj.Ord { + if k != typ.Ord[i] { + return fmt.Errorf("struct fields differ") + } + } + for _, k := range obj.Ord { // loop map in deterministic order + t1, ok := obj.Map[k] + if !ok { + panic("malformed struct order") + } + t2, ok := typ.Map[k] + if !ok { + panic("malformed struct order") + } + if t1 == nil || t2 == nil { + panic("malformed struct field") + } + if err := t1.Cmp(t2); err != nil { + return err + } + } + return nil + + case KindFunc: + if obj.Map == nil || typ.Map == nil { + panic("malformed func type") + } + if len(obj.Ord) != len(typ.Ord) { + return fmt.Errorf("func arg count differs") + } + // needed for strict cmp only... + //for i, k := range obj.Ord { + // if k != typ.Ord[i] { + // return fmt.Errorf("func arg differs") + // } + //} + //for _, k := range obj.Ord { // loop map in deterministic order + // t1, ok := obj.Map[k] + // if !ok { + // panic("malformed func order") + // } + // t2, ok := typ.Map[k] + // if !ok { + // panic("malformed func order") + // } + // if t1 == nil || t2 == nil { + // panic("malformed func arg") + // } + // if err := t1.Cmp(t2); err != nil { + // return err + // } + //} + + // if we're not comparing arg names, get the two lists of types + for i := 0; i < len(obj.Ord); i++ { + t1, ok := obj.Map[obj.Ord[i]] + if !ok { + panic("malformed func order") + } + if t1 == nil { + panic("malformed func arg") + } + + t2, ok := typ.Map[typ.Ord[i]] + if !ok { + panic("malformed func order") + } + if t2 == nil { + panic("malformed func arg") + } + + if err := t1.Cmp(t2); err != nil { + return err + } + } + + if obj.Out != nil || typ.Out != nil { + if err := obj.Out.Cmp(typ.Out); err != nil { + return err + } + } + return nil + + // TODO: is this correct? + case KindVariant: + if typ.Kind != KindVariant { + return fmt.Errorf("variant only compares with other variants") + } + // TODO: should we Cmp obj.Var with typ.Var ? -- not necessarily + return nil + } + return fmt.Errorf("unknown kind") +} + +// Copy copies this type so that inplace modification won't affect the original. +func (obj *Type) Copy() *Type { + return NewType(obj.String()) // hack to do this easily +} + +// Reflect returns a representative type satisfying the golang Type Interface. +// The lossy inverse of this is TypeOf. +func (obj *Type) Reflect() reflect.Type { + switch obj.Kind { + case KindBool: + return reflect.TypeOf(bool(false)) + case KindStr: + return reflect.TypeOf(string("")) + case KindInt: + return reflect.TypeOf(int64(0)) + case KindFloat: + return reflect.TypeOf(float64(0)) + + case KindList: + if obj.Val == nil { + panic("malformed list type") + } + return reflect.SliceOf(obj.Val.Reflect()) // recurse + + case KindMap: + if obj.Key == nil || obj.Val == nil { + panic("malformed map type") + } + return reflect.MapOf(obj.Key.Reflect(), obj.Val.Reflect()) // dual recurse + + case KindStruct: // {a bool; b int} + if obj.Map == nil { + panic("malformed struct type") + } + if len(obj.Map) != len(obj.Ord) { + panic("malformed struct length") + } + + fields := []reflect.StructField{} + for _, k := range obj.Ord { + t, ok := obj.Map[k] + if !ok { + panic("malformed struct order") + } + if t == nil { + panic("malformed struct field") + } + + fields = append(fields, reflect.StructField{ + Name: k, // struct field name + Type: t.Reflect(), + //Tag: `mgmt:"foo"`, // unused + }) + } + + return reflect.StructOf(fields) + + case KindFunc: + if obj.Map == nil { + panic("malformed func type") + } + if len(obj.Map) != len(obj.Ord) { + panic("malformed func length") + } + + in := []reflect.Type{} + for _, k := range obj.Ord { + t, ok := obj.Map[k] + if !ok { + panic("malformed func order") + } + if t == nil { + panic("malformed func arg") + } + + in = append(in, t.Reflect()) + } + + out := []reflect.Type{} // only one return arg + if obj.Out != nil { + out = append(out, obj.Out.Reflect()) + } + var variadic = false // we don't support variadic functions atm + + return reflect.FuncOf(in, out, variadic) + + case KindVariant: + var x interface{} + return reflect.TypeOf(x) // TODO: is this correct? + } + + panic("malformed type") +} + +// Underlying returns the underlying type of the type in question. For variants, +// this unpacks them recursively, for everything else this returns itself. +func (obj *Type) Underlying() *Type { + typ := obj // initial exposed type + for { + if typ.Kind != KindVariant { + return typ + } + typ = typ.Var // unpack child type of variant + } +} + +// HasVariant tells us if the type contains any mention of the Variant type. +func (obj *Type) HasVariant() bool { + if obj == nil { + return false + } + switch obj.Kind { + case KindBool: + return false + case KindStr: + return false + case KindInt: + return false + case KindFloat: + return false + + case KindList: + if obj.Val == nil { + panic("malformed list type") + } + return obj.Val.HasVariant() + + case KindMap: + if obj.Key == nil || obj.Val == nil { + panic("malformed map type") + } + return obj.Key.HasVariant() || obj.Val.HasVariant() + + case KindStruct: // {a bool; b int} + if obj.Map == nil { + panic("malformed struct type") + } + if len(obj.Map) != len(obj.Ord) { + panic("malformed struct length") + } + for _, k := range obj.Ord { + t, ok := obj.Map[k] + if !ok { + panic("malformed struct order") + } + if t == nil { + panic("malformed struct field") + } + if t.HasVariant() { + return true + } + } + return false + + case KindFunc: + if obj.Map == nil { + panic("malformed func type") + } + if len(obj.Map) != len(obj.Ord) { + panic("malformed func length") + } + for _, k := range obj.Ord { + t, ok := obj.Map[k] + if !ok { + panic("malformed func order") + } + if t == nil { + panic("malformed func field") + } + if t.HasVariant() { + return true + } + } + if obj.Out != nil { + if obj.Out.HasVariant() { + return true + } + } + return false + + case KindVariant: + return true // found it! + } + + panic("malformed type") +} diff --git a/lang/types/type_test.go b/lang/types/type_test.go new file mode 100644 index 00000000..4b7a21f5 --- /dev/null +++ b/lang/types/type_test.go @@ -0,0 +1,1131 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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 types + +import ( + "testing" +) + +func TestType0(t *testing.T) { + str := "struct{a bool; bb int; ccc str}" + val := &Type{ + Kind: KindStruct, + Ord: []string{ + "a", + "bb", + "ccc", + }, + Map: map[string]*Type{ + "a": { + Kind: KindBool, + }, + "bb": { + Kind: KindInt, + }, + "ccc": { + Kind: KindStr, + }, + }, + } + kind := NewType(str) + if err := kind.Cmp(val); err != nil { + t.Errorf("kind output of `%v` did not match expected: `%v`", str, err) + } +} + +func TestType1(t *testing.T) { + values := map[string]*Type{ + "": nil, // error + "nope": nil, // error + + // basic types + "bool": { + Kind: KindBool, + }, + "str": { + Kind: KindStr, + }, + "int": { + Kind: KindInt, + }, + "float": { + Kind: KindFloat, + }, + + // lists + "[]str": { // list of str's + Kind: KindList, + Val: &Type{ + Kind: KindStr, + }, + }, + "[]int": { + Kind: KindList, + Val: &Type{ + Kind: KindInt, + }, + }, + "[]bool": { + Kind: KindList, + Val: &Type{ + Kind: KindBool, + }, + }, + + // nested lists + "[][]bool": { + Kind: KindList, + Val: &Type{ + Kind: KindList, + Val: &Type{ + Kind: KindBool, + }, + }, + }, + "[][]int": { + Kind: KindList, + Val: &Type{ + Kind: KindList, + Val: &Type{ + Kind: KindInt, + }, + }, + }, + "[][][]str": { + Kind: KindList, + Val: &Type{ + Kind: KindList, + Val: &Type{ + Kind: KindList, + Val: &Type{ + Kind: KindStr, + }, + }, + }, + }, + + // maps + "{}": nil, // invalid + "{str: str}": { + Kind: KindMap, + Key: &Type{ + Kind: KindStr, + }, + Val: &Type{ + Kind: KindStr, + }, + }, + "{str: int}": { + Kind: KindMap, + Key: &Type{ + Kind: KindStr, + }, + Val: &Type{ + Kind: KindInt, + }, + }, + + // nested maps + "{str: {int: bool}}": { + Kind: KindMap, + Key: &Type{ + Kind: KindStr, + }, + Val: &Type{ + Kind: KindMap, + Key: &Type{ + Kind: KindInt, + }, + Val: &Type{ + Kind: KindBool, + }, + }, + }, + "{{int: bool}: str}": { + Kind: KindMap, + Key: &Type{ + Kind: KindMap, + Key: &Type{ + Kind: KindInt, + }, + Val: &Type{ + Kind: KindBool, + }, + }, + Val: &Type{ + Kind: KindStr, + }, + }, + "{{str: int}: {int: bool}}": { + Kind: KindMap, + Key: &Type{ + Kind: KindMap, + Key: &Type{ + Kind: KindStr, + }, + Val: &Type{ + Kind: KindInt, + }, + }, + Val: &Type{ + Kind: KindMap, + Key: &Type{ + Kind: KindInt, + }, + Val: &Type{ + Kind: KindBool, + }, + }, + }, + "{str: {int: {int: bool}}}": { + Kind: KindMap, + Key: &Type{ + Kind: KindStr, + }, + Val: &Type{ + Kind: KindMap, + Key: &Type{ + Kind: KindInt, + }, + Val: &Type{ + Kind: KindMap, + Key: &Type{ + Kind: KindInt, + }, + Val: &Type{ + Kind: KindBool, + }, + }, + }, + }, + + // structs + "struct{}": { + Kind: KindStruct, + Map: map[string]*Type{}, + }, + "struct{a bool}": { + Kind: KindStruct, + Ord: []string{ + "a", + }, + Map: map[string]*Type{ + "a": { + Kind: KindBool, + }, + }, + }, + "struct{a bool; bb int}": { + Kind: KindStruct, + Ord: []string{ + "a", + "bb", + }, + Map: map[string]*Type{ + "a": { + Kind: KindBool, + }, + "bb": { + Kind: KindInt, + }, + }, + }, + "struct{a bool; bb int; ccc str}": { + Kind: KindStruct, + Ord: []string{ + "a", + "bb", + "ccc", + }, + Map: map[string]*Type{ + "a": { + Kind: KindBool, + }, + "bb": { + Kind: KindInt, + }, + "ccc": { + Kind: KindStr, + }, + }, + }, + + // nested structs + "struct{bb struct{z bool}; ccc str}": { + Kind: KindStruct, + Ord: []string{ + "bb", + "ccc", + }, + Map: map[string]*Type{ + "bb": { + Kind: KindStruct, + Ord: []string{ + "z", + }, + Map: map[string]*Type{ + "z": { + Kind: KindBool, + }, + }, + }, + "ccc": { + Kind: KindStr, + }, + }, + }, + "struct{a bool; bb struct{z bool; yy int}; ccc str}": { + Kind: KindStruct, + Ord: []string{ + "a", + "bb", + "ccc", + }, + Map: map[string]*Type{ + "a": { + Kind: KindBool, + }, + "bb": { + Kind: KindStruct, + Ord: []string{ + "z", + "yy", + }, + Map: map[string]*Type{ + "z": { + Kind: KindBool, + }, + "yy": { + Kind: KindInt, + }, + }, + }, + "ccc": { + Kind: KindStr, + }, + }, + }, + "struct{a bool; bb struct{z bool; yy struct{struct int; nested bool}}; ccc str}": { + Kind: KindStruct, + Ord: []string{ + "a", + "bb", + "ccc", + }, + Map: map[string]*Type{ + "a": { + Kind: KindBool, + }, + "bb": { + Kind: KindStruct, + Ord: []string{ + "z", + "yy", + }, + Map: map[string]*Type{ + "z": { + Kind: KindBool, + }, + "yy": { + Kind: KindStruct, + Ord: []string{ + "struct", + "nested", + }, + Map: map[string]*Type{ + "struct": { + Kind: KindInt, + }, + "nested": { + Kind: KindBool, + }, + }, + }, + }, + }, + "ccc": { + Kind: KindStr, + }, + }, + }, + + // mixed nesting + "{str: []struct{a bool; int []bool}}": { + Kind: KindMap, + Key: &Type{ + Kind: KindStr, + }, + Val: &Type{ + Kind: KindList, + Val: &Type{ + Kind: KindStruct, + Ord: []string{ + "a", + "int", + }, + Map: map[string]*Type{ + "a": { + Kind: KindBool, + }, + "int": { + Kind: KindList, + Val: &Type{ + Kind: KindBool, + }, + }, + }, + }, + }, + }, + "struct{a {str: {struct{deeply int; nested bool}: {int: bool}}}; bb struct{z bool; yy int}; ccc str}": { + Kind: KindStruct, + Ord: []string{ + "a", + "bb", + "ccc", + }, + Map: map[string]*Type{ + "a": { + Kind: KindMap, + Key: &Type{ + Kind: KindStr, + }, + Val: &Type{ + Kind: KindMap, + Key: &Type{ + Kind: KindStruct, + Ord: []string{ + "deeply", + "nested", + }, + Map: map[string]*Type{ + "deeply": { + Kind: KindInt, + }, + "nested": { + Kind: KindBool, + }, + }, + }, + Val: &Type{ + Kind: KindMap, + Key: &Type{ + Kind: KindInt, + }, + Val: &Type{ + Kind: KindBool, + }, + }, + }, + }, + "bb": { + Kind: KindStruct, + Ord: []string{ + "z", + "yy", + }, + Map: map[string]*Type{ + "z": { + Kind: KindBool, + }, + "yy": { + Kind: KindInt, + }, + }, + }, + "ccc": { + Kind: KindStr, + }, + }, + }, + + // functions + "func()": { + Kind: KindFunc, + Map: map[string]*Type{}, + Ord: []string{}, + Out: nil, + }, + "func() float": { + Kind: KindFunc, + Map: map[string]*Type{}, + Ord: []string{}, + Out: &Type{ + Kind: KindFloat, + }, + }, + "func(str) bool": { + Kind: KindFunc, + // key names are arbitrary... + Map: map[string]*Type{ + "a0": { + Kind: KindStr, + }, + }, + Ord: []string{ + "a0", // must match + }, + Out: &Type{ + Kind: KindBool, + }, + }, + "func(str, int) bool": { + Kind: KindFunc, + // key names are arbitrary... + Map: map[string]*Type{ + "hello": { + Kind: KindStr, + }, + "answer": { + Kind: KindInt, + }, + }, + Ord: []string{ + "hello", + "answer", + }, + Out: &Type{ + Kind: KindBool, + }, + }, + "func(str, []int, float) bool": { + Kind: KindFunc, + // key names are arbitrary... + Map: map[string]*Type{ + "a0": { + Kind: KindStr, + }, + "a1": { + Kind: KindList, + Val: &Type{ + Kind: KindInt, + }, + }, + "a2": { + Kind: KindFloat, + }, + }, + Ord: []string{ + "a0", + "a1", + "a2", + }, + Out: &Type{ + Kind: KindBool, + }, + }, + } + + for str, val := range values { // run all the tests + + // for debugging + //if str != "func(str, int) bool" { + // continue + //} + + // check the type + typ := NewType(str) + //t.Logf("str: %+v", str) + //t.Logf("typ: %+v", typ) + //if !reflect.DeepEqual(kind, val) { + // t.Errorf("kind output of `%v` did not match expected: `%v`", kind, val) + //} + + if val == nil { // catch error cases + if typ != nil { + t.Errorf("invalid type: `%s` did not match expected nil", str) + } + continue + } + + if err := typ.Cmp(val); err != nil { + t.Errorf("type: `%s` did not match expected: `%v`", str, err) + return + } + + // check the string + if repr := val.String(); repr != str { + t.Errorf("type representation of `%s` did not match expected: `%s`", str, repr) + } + } +} + +func TestType2(t *testing.T) { + // mapping from golang representation to our expected equivalent + values := map[string]*Type{ + // basic types + "bool": { + Kind: KindBool, + }, + "string": { + Kind: KindStr, + }, + "int64": { + Kind: KindInt, + }, + "float64": { + Kind: KindFloat, + }, + + // lists + "[]bool": { + Kind: KindList, + Val: &Type{ + Kind: KindBool, + }, + }, + "[]string": { // list of str's + Kind: KindList, + Val: &Type{ + Kind: KindStr, + }, + }, + "[]int64": { + Kind: KindList, + Val: &Type{ + Kind: KindInt, + }, + }, + + // nested lists + "[][]bool": { + Kind: KindList, + Val: &Type{ + Kind: KindList, + Val: &Type{ + Kind: KindBool, + }, + }, + }, + "[][]int64": { + Kind: KindList, + Val: &Type{ + Kind: KindList, + Val: &Type{ + Kind: KindInt, + }, + }, + }, + "[][][]string": { + Kind: KindList, + Val: &Type{ + Kind: KindList, + Val: &Type{ + Kind: KindList, + Val: &Type{ + Kind: KindStr, + }, + }, + }, + }, + + // maps + "map[string]string": { + Kind: KindMap, + Key: &Type{ + Kind: KindStr, + }, + Val: &Type{ + Kind: KindStr, + }, + }, + "map[string]int64": { + Kind: KindMap, + Key: &Type{ + Kind: KindStr, + }, + Val: &Type{ + Kind: KindInt, + }, + }, + + // nested maps + "map[string]map[int64]bool": { + Kind: KindMap, + Key: &Type{ + Kind: KindStr, + }, + Val: &Type{ + Kind: KindMap, + Key: &Type{ + Kind: KindInt, + }, + Val: &Type{ + Kind: KindBool, + }, + }, + }, + // FIXME: should we prevent this in our implementation as well? + //"map[map[int64]bool]string": &Type{ // no map keys in golang! + // Kind: KindMap, + // Key: &Type{ + // Kind: KindMap, + // Key: &Type{ + // Kind: KindInt, + // }, + // Val: &Type{ + // Kind: KindBool, + // }, + // }, + // Val: &Type{ + // Kind: KindStr, + // }, + //}, + //"map[map[string]int64]map[int64]bool": &Type{ + // Kind: KindMap, + // Key: &Type{ + // Kind: KindMap, + // Key: &Type{ + // Kind: KindStr, + // }, + // Val: &Type{ + // Kind: KindInt, + // }, + // }, + // Val: &Type{ + // Kind: KindMap, + // Key: &Type{ + // Kind: KindInt, + // }, + // Val: &Type{ + // Kind: KindBool, + // }, + // }, + //}, + "map[string]map[int64]map[int64]bool": { + Kind: KindMap, + Key: &Type{ + Kind: KindStr, + }, + Val: &Type{ + Kind: KindMap, + Key: &Type{ + Kind: KindInt, + }, + Val: &Type{ + Kind: KindMap, + Key: &Type{ + Kind: KindInt, + }, + Val: &Type{ + Kind: KindBool, + }, + }, + }, + }, + + // structs + "struct {}": { // requires a space between `struct` and {} + Kind: KindStruct, + Map: map[string]*Type{}, + }, + "struct { A bool }": { // more spaces, and uppercase keys! + Kind: KindStruct, + Ord: []string{ + "A", + }, + Map: map[string]*Type{ + "A": { + Kind: KindBool, + }, + }, + }, + "struct { A bool; Bb int64 }": { + Kind: KindStruct, + Ord: []string{ + "A", + "Bb", + }, + Map: map[string]*Type{ + "A": { + Kind: KindBool, + }, + "Bb": { + Kind: KindInt, + }, + }, + }, + "struct { A bool; Bb int64; Ccc string }": { + Kind: KindStruct, + Ord: []string{ + "A", + "Bb", + "Ccc", + }, + Map: map[string]*Type{ + "A": { + Kind: KindBool, + }, + "Bb": { + Kind: KindInt, + }, + "Ccc": { + Kind: KindStr, + }, + }, + }, + + // nested structs + "struct { Bb struct { Z bool }; Ccc string }": { + Kind: KindStruct, + Ord: []string{ + "Bb", + "Ccc", + }, + Map: map[string]*Type{ + "Bb": { + Kind: KindStruct, + Ord: []string{ + "Z", + }, + Map: map[string]*Type{ + "Z": { + Kind: KindBool, + }, + }, + }, + "Ccc": { + Kind: KindStr, + }, + }, + }, + "struct { A bool; Bb struct { Z bool; Yy int64 }; Ccc string }": { + Kind: KindStruct, + Ord: []string{ + "A", + "Bb", + "Ccc", + }, + Map: map[string]*Type{ + "A": { + Kind: KindBool, + }, + "Bb": { + Kind: KindStruct, + Ord: []string{ + "Z", + "Yy", + }, + Map: map[string]*Type{ + "Z": { + Kind: KindBool, + }, + "Yy": { + Kind: KindInt, + }, + }, + }, + "Ccc": { + Kind: KindStr, + }, + }, + }, + "struct { A bool; Bb struct { Z bool; Yy struct { Struct int64; Nested bool } }; Ccc string }": { + Kind: KindStruct, + Ord: []string{ + "A", + "Bb", + "Ccc", + }, + Map: map[string]*Type{ + "A": { + Kind: KindBool, + }, + "Bb": { + Kind: KindStruct, + Ord: []string{ + "Z", + "Yy", + }, + Map: map[string]*Type{ + "Z": { + Kind: KindBool, + }, + "Yy": { + Kind: KindStruct, + Ord: []string{ + "Struct", + "Nested", + }, + Map: map[string]*Type{ + "Struct": { + Kind: KindInt, + }, + "Nested": { + Kind: KindBool, + }, + }, + }, + }, + }, + "Ccc": { + Kind: KindStr, + }, + }, + }, + + // mixed nesting + "map[string][]struct { A bool; Int64 []bool }": { + Kind: KindMap, + Key: &Type{ + Kind: KindStr, + }, + Val: &Type{ + Kind: KindList, + Val: &Type{ + Kind: KindStruct, + Ord: []string{ + "A", + "Int64", + }, + Map: map[string]*Type{ + "A": { + Kind: KindBool, + }, + "Int64": { + Kind: KindList, + Val: &Type{ + Kind: KindBool, + }, + }, + }, + }, + }, + }, + + "struct { A map[string]map[struct { Deeply int64; Nested bool }]map[int64]bool; Bb struct { Z bool; Yy int64 }; Ccc string }": { + Kind: KindStruct, + Ord: []string{ + "A", + "Bb", + "Ccc", + }, + Map: map[string]*Type{ + "A": { + Kind: KindMap, + Key: &Type{ + Kind: KindStr, + }, + Val: &Type{ + Kind: KindMap, + Key: &Type{ + Kind: KindStruct, + Ord: []string{ + "Deeply", + "Nested", + }, + Map: map[string]*Type{ + "Deeply": { + Kind: KindInt, + }, + "Nested": { + Kind: KindBool, + }, + }, + }, + Val: &Type{ + Kind: KindMap, + Key: &Type{ + Kind: KindInt, + }, + Val: &Type{ + Kind: KindBool, + }, + }, + }, + }, + "Bb": { + Kind: KindStruct, + Ord: []string{ + "Z", + "Yy", + }, + Map: map[string]*Type{ + "Z": { + Kind: KindBool, + }, + "Yy": { + Kind: KindInt, + }, + }, + }, + "Ccc": { + Kind: KindStr, + }, + }, + }, + + // functions + "func()": { + Kind: KindFunc, + Map: map[string]*Type{}, + Ord: []string{}, + Out: nil, + }, + "func() float64": { + Kind: KindFunc, + Map: map[string]*Type{}, + Ord: []string{}, + Out: &Type{ + Kind: KindFloat, + }, + }, + "func(string) bool": { + Kind: KindFunc, + // key names are arbitrary... + Map: map[string]*Type{ + "a0": { + Kind: KindStr, + }, + }, + Ord: []string{ + "a0", // must match + }, + Out: &Type{ + Kind: KindBool, + }, + }, + "func(string, int64) bool": { + Kind: KindFunc, + // key names are arbitrary... + Map: map[string]*Type{ + "hello": { + Kind: KindStr, + }, + "answer": { + Kind: KindInt, + }, + }, + Ord: []string{ + "hello", + "answer", + }, + Out: &Type{ + Kind: KindBool, + }, + }, + "func(string, []int64, float64) bool": { + Kind: KindFunc, + // key names are arbitrary... + Map: map[string]*Type{ + "a0": { + Kind: KindStr, + }, + "a1": { + Kind: KindList, + Val: &Type{ + Kind: KindInt, + }, + }, + "a2": { + Kind: KindFloat, + }, + }, + Ord: []string{ + "a0", + "a1", + "a2", + }, + Out: &Type{ + Kind: KindBool, + }, + }, + } + + for str, typ := range values { // run all the tests + // check the type + reflected := typ.Reflect() + + //t.Logf("reflect: %+v -> %+v", str, reflected.String()) + // check the string + if repr := reflected.String(); repr != str { + t.Errorf("type representation of `%s` did not match expected: `%s`", str, repr) + } + } +} + +func TestType3(t *testing.T) { + // functions with named types... + values := map[string]*Type{ + "func(input str) bool": { + Kind: KindFunc, + Map: map[string]*Type{ + "input": { + Kind: KindStr, + }, + }, + Ord: []string{ + "input", // must match + }, + Out: &Type{ + Kind: KindBool, + }, + }, + "func(aaa str, bb int) bool": { + Kind: KindFunc, + // key names are arbitrary... + Map: map[string]*Type{ + "aaa": { + Kind: KindStr, + }, + "bb": { + Kind: KindInt, + }, + }, + Ord: []string{ + "aaa", + "bb", + }, + Out: &Type{ + Kind: KindBool, + }, + }, + } + + for str, val := range values { // run all the tests + + // for debugging + //if str != "func(aaa str, bb int) bool" { + //continue + //} + + // check the type + typ := NewType(str) + //t.Logf("str: %+v", str) + //t.Logf("typ: %+v", typ) + //if !reflect.DeepEqual(kind, val) { + // t.Errorf("kind output of `%v` did not match expected: `%v`", kind, val) + //} + + if val == nil { // catch error cases + if typ != nil { + t.Errorf("invalid type: `%s` did not match expected nil", str) + } + continue + } + + if err := typ.Cmp(val); err != nil { + t.Errorf("type: `%s` did not match expected: `%v`", str, err) + return + } + } +} + +func TestTypeOf0(t *testing.T) { + // TODO: implement testing of the TypeOf function +} diff --git a/lang/types/value.go b/lang/types/value.go new file mode 100644 index 00000000..8d814413 --- /dev/null +++ b/lang/types/value.go @@ -0,0 +1,979 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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 types + +import ( + "fmt" + "reflect" + "sort" + "strconv" + "strings" + + errwrap "github.com/pkg/errors" +) + +// Value represents an interface to get values out of each type. It is similar +// to the reflection interfaces used in the golang standard library. +type Value interface { + fmt.Stringer // String() string (for display purposes) + Type() *Type + Less(Value) bool // to find the smaller of the two values (for sort) + Cmp(Value) error // error if the two values aren't the same + Copy() Value // returns a copy of this value + Value() interface{} + Bool() bool + Str() string + Int() int64 + Float() float64 + List() []Value + Map() map[Value]Value // keys must all have same type, same for values + Struct() map[string]Value + Func() func([]Value) (Value, error) +} + +// ValueOf takes a reflect.Value and returns an equivalent Value. +func ValueOf(value reflect.Value) Value { + panic("not implemented") // XXX: not implemented +} + +// ValueSlice is a linear list of values. It is used for sorting purposes. +type ValueSlice []Value + +func (vs ValueSlice) Len() int { return len(vs) } +func (vs ValueSlice) Swap(i, j int) { vs[i], vs[j] = vs[j], vs[i] } +func (vs ValueSlice) Less(i, j int) bool { return vs[i].Less(vs[j]) } + +// base implements the missing methods that all types need. +type base struct{} + +// Bool represents the value of this type as a bool if it is one. If this is not +// a bool, then this panics. +func (obj *base) Bool() bool { + panic("not a bool") +} + +// Str represents the value of this type as a string if it is one. If this is +// not a string, then this panics. +func (obj *base) Str() string { + panic("not an str") // yes, i think this is the correct grammar +} + +// Int represents the value of this type as an integer if it is one. If this is +// not an integer, then this panics. +func (obj *base) Int() int64 { + panic("not an int") +} + +// Float represents the value of this type as a float if it is one. If this is +// not a float, then this panics. +func (obj *base) Float() float64 { + panic("not a float") +} + +// List represents the value of this type as a list if it is one. If this is not +// a list, then this panics. +func (obj *base) List() []Value { + panic("not a list") +} + +// Map represents the value of this type as a dictionary if it is one. If this +// is not a map, then this panics. +func (obj *base) Map() map[Value]Value { + panic("not a list") +} + +// Struct represents the value of this type as a struct if it is one. If this is +// not a struct, then this panics. +func (obj *base) Struct() map[string]Value { + panic("not a struct") +} + +// Func represents the value of this type as a function if it is one. If this is +// not a function, then this panics. +func (obj *base) Func() func([]Value) (Value, error) { + panic("not a func") +} + +// Less compares to value and returns true if we're smaller. It is recommended +// that this base implementation of the method be replaced in the specific type. +// This *may* panic if the two types aren't the same. +// NOTE: this can be used as an example template to write your own function. +//func (obj *base) Less(v Value) bool { +// // TODO: cheap less, be smarter in each type eg: int's should cmp as int +// return obj.String() < v.String() +//} + +// Cmp returns an error if this value isn't the same as the arg passed in. This +// implementation uses the base Less implementation and should be replaced. It +// is always nice to implement this properly so that we get better error output. +// NOTE: this can be used as an example template to write your own function. +//func (obj *base) Cmp(v Value) error { +// // if they're both true or both false, then they must be the same, +// // because we expect that if x < & && y < x then x == y +// if obj.Less(v) != v.Less(obj) { +// return fmt.Errorf("values differ according to less") +// } +// return nil +//} + +// BoolValue represents a boolean value. +type BoolValue struct { + base + V bool +} + +// NewBool creates a new boolean value. +func NewBool() *BoolValue { return &BoolValue{} } + +// String returns a visual representation of this value. +func (obj *BoolValue) String() string { + return strconv.FormatBool(obj.V) // true or false + //if obj.V { + // return "true" + //} + //return "false" +} + +// Type returns the type data structure that represents this type. +func (obj *BoolValue) Type() *Type { return NewType("bool") } + +// Less compares to value and returns true if we're smaller. This panics if the +// two types aren't the same. +func (obj *BoolValue) Less(v Value) bool { + //return obj.String() < v.(*BoolValue).String() + if obj.V != v.(*BoolValue).V { // there must be one false + // f, t -> t ; t, f -> f + return !obj.V // TODO: should `false` sort less? + } + return false // they're the same +} + +// Cmp returns an error if this value isn't the same as the arg passed in. +func (obj *BoolValue) Cmp(val Value) error { + if obj == nil || val == nil { + return fmt.Errorf("cannot cmp to nil") + } + if err := obj.Type().Cmp(val.Type()); err != nil { + return errwrap.Wrapf(err, "cannot cmp types") + } + + if obj.V != val.(*BoolValue).V { + return fmt.Errorf("values are different") + } + return nil +} + +// Copy returns a copy of this value. +func (obj *BoolValue) Copy() Value { + return &BoolValue{V: obj.V} +} + +// Value returns the raw value of this type. +func (obj *BoolValue) Value() interface{} { + return obj.V +} + +// Bool represents the value of this type as a bool if it is one. If this is not +// a bool, then this panics. +func (obj *BoolValue) Bool() bool { + return obj.V +} + +// StrValue represents a string value. +type StrValue struct { + base + V string +} + +// NewStr creates a new string value. +func NewStr() *StrValue { return &StrValue{} } + +// String returns a visual representation of this value. +func (obj *StrValue) String() string { + return strconv.Quote(obj.V) // wraps in quotes, turns tabs into \t etc... + //return fmt.Sprintf(`"%s"`, obj.V) +} + +// Type returns the type data structure that represents this type. +func (obj *StrValue) Type() *Type { return NewType("str") } + +// Less compares to value and returns true if we're smaller. This panics if the +// two types aren't the same. +func (obj *StrValue) Less(v Value) bool { + return obj.V < v.(*StrValue).V +} + +// Cmp returns an error if this value isn't the same as the arg passed in. +func (obj *StrValue) Cmp(val Value) error { + if obj == nil || val == nil { + return fmt.Errorf("cannot cmp to nil") + } + if err := obj.Type().Cmp(val.Type()); err != nil { + return errwrap.Wrapf(err, "cannot cmp types") + } + + if obj.V != val.(*StrValue).V { + return fmt.Errorf("values are different") + } + return nil +} + +// Copy returns a copy of this value. +func (obj *StrValue) Copy() Value { + return &StrValue{V: obj.V} +} + +// Value returns the raw value of this type. +func (obj *StrValue) Value() interface{} { + return obj.V +} + +// Str represents the value of this type as a string if it is one. If this is +// not a string, then this panics. +func (obj *StrValue) Str() string { + return obj.V +} + +// IntValue represents an integer value. +type IntValue struct { + base + V int64 +} + +// NewInt creates a new int value. +func NewInt() *IntValue { return &IntValue{} } + +// String returns a visual representation of this value. +func (obj *IntValue) String() string { + return strconv.FormatInt(obj.V, 10) +} + +// Type returns the type data structure that represents this type. +func (obj *IntValue) Type() *Type { return NewType("int") } + +// Less compares to value and returns true if we're smaller. This panics if the +// two types aren't the same. +func (obj *IntValue) Less(v Value) bool { + return obj.V < v.(*IntValue).V +} + +// Cmp returns an error if this value isn't the same as the arg passed in. +func (obj *IntValue) Cmp(val Value) error { + if obj == nil || val == nil { + return fmt.Errorf("cannot cmp to nil") + } + if err := obj.Type().Cmp(val.Type()); err != nil { + return errwrap.Wrapf(err, "cannot cmp types") + } + + if obj.V != val.(*IntValue).V { + return fmt.Errorf("values are different") + } + return nil +} + +// Copy returns a copy of this value. +func (obj *IntValue) Copy() Value { + return &IntValue{V: obj.V} +} + +// Value returns the raw value of this type. +func (obj *IntValue) Value() interface{} { + return obj.V +} + +// Int represents the value of this type as an integer if it is one. If this is +// not an integer, then this panics. +func (obj *IntValue) Int() int64 { + return obj.V +} + +// FloatValue represents an integer value. +type FloatValue struct { + base + V float64 +} + +// NewFloat creates a new float value. +func NewFloat() *FloatValue { return &FloatValue{} } + +// String returns a visual representation of this value. +func (obj *FloatValue) String() string { + // TODO: is this the right display mode? + return strconv.FormatFloat(obj.V, 'f', -1, 64) // -1 for exact precision +} + +// Type returns the type data structure that represents this type. +func (obj *FloatValue) Type() *Type { return NewType("float") } + +// Less compares to value and returns true if we're smaller. This panics if the +// two types aren't the same. +func (obj *FloatValue) Less(v Value) bool { + return obj.V < v.(*FloatValue).V +} + +// Cmp returns an error if this value isn't the same as the arg passed in. +func (obj *FloatValue) Cmp(val Value) error { + if obj == nil || val == nil { + return fmt.Errorf("cannot cmp to nil") + } + if err := obj.Type().Cmp(val.Type()); err != nil { + return errwrap.Wrapf(err, "cannot cmp types") + } + + // FIXME: should we compare with an epsilon? + if obj.V != val.(*FloatValue).V { + return fmt.Errorf("values are different") + } + return nil +} + +// Copy returns a copy of this value. +func (obj *FloatValue) Copy() Value { + return &FloatValue{V: obj.V} +} + +// Value returns the raw value of this type. +func (obj *FloatValue) Value() interface{} { + return obj.V +} + +// Float represents the value of this type as a float if it is one. If this is +// not a float, then this panics. +func (obj *FloatValue) Float() float64 { + return obj.V +} + +// ListValue represents a list value. +type ListValue struct { + base + V []Value // all elements must have type T.Val + T *Type +} + +// NewList creates a new list with the specified list type. +func NewList(t *Type) *ListValue { + if t.Kind != KindList { + return nil // sanity check + } + return &ListValue{ + T: t, + } +} + +// String returns a visual representation of this value. +func (obj *ListValue) String() string { + var s []string + for _, x := range obj.V { + s = append(s, x.String()) + } + return fmt.Sprintf("[%s]", strings.Join(s, ", ")) +} + +// Type returns the type data structure that represents this type. +func (obj *ListValue) Type() *Type { return obj.T } + +// Less compares to value and returns true if we're smaller. This panics if the +// two types aren't the same. +func (obj *ListValue) Less(v Value) bool { + V := v.(*ListValue).V + i, j := len(obj.V), len(V) + + for x := 0; x < i && x < j; x++ { // keep to min count of both lists + if obj.V[x].Less(V[x]) { + return true + } + } + + return i < j // TODO: i think this is correct :) +} + +// Cmp returns an error if this value isn't the same as the arg passed in. +func (obj *ListValue) Cmp(val Value) error { + if obj == nil || val == nil { + return fmt.Errorf("cannot cmp to nil") + } + if err := obj.Type().Cmp(val.Type()); err != nil { + return errwrap.Wrapf(err, "cannot cmp types") + } + + cmp := val.(*ListValue) + + if len(obj.V) != len(cmp.V) { + return fmt.Errorf("values have different lengths") + } + + for i := range obj.V { + if err := obj.V[i].Cmp(cmp.V[i]); err != nil { + return errwrap.Wrapf(err, "index %d did not cmp", i) + } + } + + return nil +} + +// Copy returns a copy of this value. +func (obj *ListValue) Copy() Value { + v := []Value{} + for _, x := range obj.V { + v = append(v, x.Copy()) + } + return &ListValue{ + V: v, + T: obj.T.Copy(), + } +} + +// Value returns the raw value of this type. +func (obj *ListValue) Value() interface{} { + typ := obj.T.Reflect() + // create an empty slice (of len=0) with room for cap=len(obj.V) elements + val := reflect.MakeSlice(typ, 0, len(obj.V)) + + for _, x := range obj.V { + val = reflect.Append(val, reflect.ValueOf(x.Value())) // recurse + } + return val.Interface() +} + +// List represents the value of this type as a list if it is one. If this is not +// a list, then this panics. +func (obj *ListValue) List() []Value { + return obj.V +} + +// Add adds an element to this list. It errors if the type does not match. +func (obj *ListValue) Add(v Value) error { + if obj.T.Val.Kind != KindVariant { // skip cmp if dest is a variant + if err := obj.T.Val.Cmp(v.Type()); err != nil { + return errwrap.Wrapf(err, "value does not match list element type") + } + } + + obj.V = append(obj.V, v) + return nil +} + +// Lookup looks up a value by index. On success it also returns the Value. +func (obj *ListValue) Lookup(index int) (value Value, exists bool) { + if index >= 0 && index < len(obj.V) { + return obj.V[index], true // found + } + return nil, false +} + +// Contains searches for a value in the list. On success it returns the index. +func (obj *ListValue) Contains(v Value) (index int, exists bool) { + for i, x := range obj.V { + if v.Cmp(x) == nil { + return i, true + } + } + return -1, false +} + +// MapValue represents a dictionary value. +type MapValue struct { + base + // the types of all keys and values are represented inside of T + V map[Value]Value + T *Type +} + +// NewMap creates a new map with the specified map type. +func NewMap(t *Type) *MapValue { + if t.Kind != KindMap { + return nil // sanity check + } + return &MapValue{ + V: make(map[Value]Value), + T: t, + } +} + +// String returns a visual representation of this value. +func (obj *MapValue) String() string { + keys := []Value{} + for k := range obj.V { + keys = append(keys, k) + } + sort.Sort(ValueSlice(keys)) // deterministic print order + + var s []string + for _, k := range keys { + s = append(s, fmt.Sprintf("%s: %s", k.String(), obj.V[k].String())) + } + return fmt.Sprintf("{%s}", strings.Join(s, ", ")) +} + +// Type returns the type data structure that represents this type. +func (obj *MapValue) Type() *Type { return obj.T } + +// Less compares to value and returns true if we're smaller. This panics if the +// two types aren't the same. +func (obj *MapValue) Less(v Value) bool { + V := v.(*MapValue) + return obj.String() < V.String() // FIXME: implement a proper less func +} + +// Cmp returns an error if this value isn't the same as the arg passed in. +func (obj *MapValue) Cmp(val Value) error { + if obj == nil || val == nil { + return fmt.Errorf("cannot cmp to nil") + } + if err := obj.Type().Cmp(val.Type()); err != nil { + return errwrap.Wrapf(err, "cannot cmp types") + } + + cmp := val.(*MapValue) + + if len(obj.V) != len(cmp.V) { + return fmt.Errorf("values have different lengths") + } + + for k := range obj.V { + v, exists := cmp.V[k] + if !exists { + return fmt.Errorf("index %s does not exist", k) + } + + if err := obj.V[k].Cmp(v); err != nil { + return errwrap.Wrapf(err, "index %s did not cmp", k) + } + } + + return nil +} + +// Copy returns a copy of this value. +func (obj *MapValue) Copy() Value { + m := map[Value]Value{} + for k, v := range obj.V { + m[k.Copy()] = v.Copy() + } + return &MapValue{ + V: m, + T: obj.T.Copy(), + } +} + +// Value returns the raw value of this type. +func (obj *MapValue) Value() interface{} { + typ := obj.T.Reflect() + val := reflect.MakeMap(typ) + + for k, v := range obj.V { + val.SetMapIndex(reflect.ValueOf(k.Value()), reflect.ValueOf(v.Value())) // dual recurse + } + return val.Interface() +} + +// Map represents the value of this type as a dictionary if it is one. If this +// is not a map, then this panics. +func (obj *MapValue) Map() map[Value]Value { + return obj.V +} + +// Add adds an element to this map. It errors if the types do not match. +func (obj *MapValue) Add(k, v Value) error { // TODO: change method name? + + //if obj.T.Key.Kind != KindVariant { + if err := obj.T.Key.Cmp(k.Type()); err != nil { + return errwrap.Wrapf(err, "key does not match map key type") + } + //} + + if obj.T.Val.Kind != KindVariant { // skip cmp if dest is a variant + if err := obj.T.Val.Cmp(v.Type()); err != nil { + return errwrap.Wrapf(err, "val does not match map val type") + } + } + + obj.V[k] = v + return nil +} + +// Lookup searches the map for a key. On success it also returns the Value. +func (obj *MapValue) Lookup(key Value) (value Value, exists bool) { + //v, exists := obj.V[key] // not what we want! + for k, v := range obj.V { + if k.Cmp(key) == nil { + return v, true // found + } + } + return nil, false +} + +// StructValue represents a struct value. The keys are ordered. +// TODO: if all functions require arg names to call, we don't need to order! +type StructValue struct { + base + V map[string]Value // each field can have a different type + T *Type // contains ordered field types +} + +// NewStruct creates a new struct with the specified field types. +func NewStruct(t *Type) *StructValue { + if t.Kind != KindStruct { + return nil // sanity check + } + v := make(map[string]Value) + for _, k := range t.Ord { + v[k] = t.Map[k].New() // don't leave struct fields uninitialized + } + return &StructValue{ + V: v, + T: t, // TODO: should we allow changes to this after create? + } +} + +// String returns a visual representation of this value. +func (obj *StructValue) String() string { + var s []string + for _, k := range obj.T.Ord { + s = append(s, fmt.Sprintf("%s: %s", k, obj.V[k].String())) + } + return fmt.Sprintf("struct{%s}", strings.Join(s, "; ")) +} + +// Type returns the type data structure that represents this type. +func (obj *StructValue) Type() *Type { return obj.T } + +// Less compares to value and returns true if we're smaller. This panics if the +// two types aren't the same. +func (obj *StructValue) Less(v Value) bool { + V := v.(*StructValue) + return obj.String() < V.String() // FIXME: implement a proper less func +} + +// Cmp returns an error if this value isn't the same as the arg passed in. +func (obj *StructValue) Cmp(val Value) error { + if obj == nil || val == nil { + return fmt.Errorf("cannot cmp to nil") + } + if err := obj.Type().Cmp(val.Type()); err != nil { + return errwrap.Wrapf(err, "cannot cmp types") + } + + cmp := val.(*StructValue) + + // compare values + for k := range obj.V { + if err := obj.V[k].Cmp(cmp.V[k]); err != nil { + return errwrap.Wrapf(err, "field %s did not cmp", k) + } + } + + return nil +} + +// Copy returns a copy of this value. +func (obj *StructValue) Copy() Value { + m := map[string]Value{} + for k, v := range obj.V { + m[k] = v.Copy() + } + return &StructValue{ + V: m, + T: obj.T.Copy(), + } +} + +// Value returns the raw value of this type. +func (obj *StructValue) Value() interface{} { + typ := obj.T.Reflect() + val := reflect.New(typ).Elem() // New returns a PtrTo(typ) + + for _, k := range obj.T.Ord { + val.FieldByName(k).Set(reflect.ValueOf(obj.V[k].Value())) // recurse + } + return val.Interface() +} + +// Struct represents the value of this type as a struct if it is one. If this is +// not a struct, then this panics. +func (obj *StructValue) Struct() map[string]Value { + return obj.V +} + +// Set sets a field to this value. It errors if the types do not match. +func (obj *StructValue) Set(k string, v Value) error { // TODO: change method name? + typ, exists := obj.T.Map[k] + if !exists { + return fmt.Errorf("field %s does not exist", k) + } + + if typ.Kind != KindVariant { // skip cmp if dest is a variant + if err := typ.Cmp(v.Type()); err != nil { + return errwrap.Wrapf(err, "value of type does not match field type") + } + } + + obj.V[k] = v // set + return nil +} + +// Lookup searches the struct for a key. On success it also returns the Value. +func (obj *StructValue) Lookup(k string) (value Value, exists bool) { + v, exists := obj.V[k] // FIXME: should we return zero values if missing? + return v, exists +} + +// FuncValue represents a function value. The defined function takes a list of +// Value arguments and returns a Value. It can also return an error which could +// represent that something went horribly wrong. (Think, an internal panic.) +type FuncValue struct { + base + V func([]Value) (Value, error) + T *Type // contains ordered field types, arg names are a bonus part +} + +// NewFunc creates a new function with the specified type. +func NewFunc(t *Type) *FuncValue { + if t.Kind != KindFunc { + return nil // sanity check + } + v := func([]Value) (Value, error) { + return nil, fmt.Errorf("nil function") // TODO: is this correct? + } + return &FuncValue{ + V: v, + T: t, + } +} + +// String returns a visual representation of this value. +func (obj *FuncValue) String() string { + return fmt.Sprintf("func(%+v)", obj.T) // TODO: can't print obj.V w/o vet warning +} + +// Type returns the type data structure that represents this type. +func (obj *FuncValue) Type() *Type { return obj.T } + +// Less compares to value and returns true if we're smaller. This panics if the +// two types aren't the same. +func (obj *FuncValue) Less(v Value) bool { + V := v.(*FuncValue) + return obj.String() < V.String() // FIXME: implement a proper less func +} + +// Cmp returns an error if this value isn't the same as the arg passed in. +func (obj *FuncValue) Cmp(val Value) error { + if obj == nil || val == nil { + return fmt.Errorf("cannot cmp to nil") + } + if err := obj.Type().Cmp(val.Type()); err != nil { + return errwrap.Wrapf(err, "cannot cmp types") + } + + return fmt.Errorf("cannot cmp funcs") // TODO: can we ? +} + +// Copy returns a copy of this value. +func (obj *FuncValue) Copy() Value { + return &FuncValue{ + V: obj.V, // FIXME: can we copy the function, or do we need to? + T: obj.T.Copy(), + } +} + +// Value returns the raw value of this type. +func (obj *FuncValue) Value() interface{} { + typ := obj.T.Reflect() + + // wrap our function with the translation that is necessary + fn := func(args []reflect.Value) (results []reflect.Value) { // build + innerArgs := []Value{} + for _, x := range args { + v := ValueOf(x) // reflect.Value -> Value + innerArgs = append(innerArgs, v) + } + result, err := obj.V(innerArgs) // call it + if err != nil { + // when calling our function with the Call method, then + // we get the error output and have a chance to decide + // what to do with it, but when calling it from within + // a normal golang function call, the error represents + // that something went horribly wrong, aka a panic... + panic(fmt.Sprintf("function panic: %+v", err)) + } + return []reflect.Value{reflect.ValueOf(result.Value())} // only one result + } + val := reflect.MakeFunc(typ, fn) + return val.Interface() +} + +// Func represents the value of this type as a function if it is one. If this is +// not a function, then this panics. +func (obj *FuncValue) Func() func([]Value) (Value, error) { + return obj.V +} + +// Set sets the function value to be a new function. +func (obj *FuncValue) Set(fn func([]Value) (Value, error)) { // TODO: change method name? + obj.V = fn +} + +// Call runs the function value and returns its result. It returns an error if +// something goes wrong during execution, and panic's if you call this with +// inappropriate input types, or if it returns an inappropriate output type. +func (obj *FuncValue) Call(args []Value) (Value, error) { + // cmp input args type to obj.T + length := len(obj.T.Ord) + if length != len(args) { + panic(fmt.Sprintf("arg length of %d does not match expected of %d", len(args), length)) + } + for i := 0; i < length; i++ { + if err := args[i].Type().Cmp(obj.T.Map[obj.T.Ord[i]]); err != nil { + panic(fmt.Sprintf("cannot cmp input types: %+v", err)) + } + } + + result, err := obj.V(args) // call it + + if err := result.Type().Cmp(obj.T.Out); err != nil { + panic(fmt.Sprintf("cannot cmp return types: %+v", err)) + } + + return result, err +} + +// VariantValue represents a variant value. +type VariantValue struct { + base + V Value // formerly I experimented with using interface{} instead + T *Type +} + +// NewVariant creates a new variant value. +// TODO: I haven't thought about this thoroughly yet. +func NewVariant(t *Type) *VariantValue { + if t.Kind != KindVariant { + return nil // sanity check + } + return &VariantValue{ + T: t, + } +} + +// String returns a visual representation of this value. +func (obj *VariantValue) String() string { + //return fmt.Sprintf("%v", obj.V) + return obj.V.String() +} + +// Type returns the type data structure that represents this type. +func (obj *VariantValue) Type() *Type { return obj.T } + +// Less compares to value and returns true if we're smaller. This panics if the +// two types aren't the same. For variants, the two sub types must be the same. +func (obj *VariantValue) Less(v Value) bool { + //return obj.String() < v.String() // FIXME: implement a proper less func + V := v.(*VariantValue).V + return obj.V.Less(V) +} + +// Cmp returns an error if this value isn't the same as the arg passed in. +func (obj *VariantValue) Cmp(val Value) error { + if obj == nil || val == nil { + return fmt.Errorf("cannot cmp to nil") + } + if err := obj.Type().Cmp(val.Type()); err != nil { + return errwrap.Wrapf(err, "cannot cmp types") + } + + V := val.(*VariantValue).V + + //if !reflect.DeepEqual(obj.V, V) { + // return fmt.Errorf("values are different") + //} + + if err := obj.V.Type().Cmp(V.Type()); err != nil { + return errwrap.Wrapf(err, "cannot cmp sub types") + } + + if err := obj.V.Cmp(V); err != nil { + return errwrap.Wrapf(err, "values are different") + } + return nil +} + +// Copy returns a copy of this value. +func (obj *VariantValue) Copy() Value { + return &VariantValue{ + V: obj.V.Copy(), + T: obj.T.Copy(), + } +} + +// Value returns the raw value of this type. +func (obj *VariantValue) Value() interface{} { + return obj.V.Value() +} + +// Bool represents the value of this type as a bool if it is one. If this is not +// a bool, then this panics. +func (obj *VariantValue) Bool() bool { + //return obj.V.(bool) + return obj.V.Bool() +} + +// Str represents the value of this type as a string if it is one. If this is +// not a string, then this panics. +func (obj *VariantValue) Str() string { + //return obj.V.(string) + return obj.V.Str() +} + +// Int represents the value of this type as an integer if it is one. If this is +// not an integer, then this panics. +func (obj *VariantValue) Int() int64 { + //return obj.V.(int64) + return obj.V.Int() +} + +// Float represents the value of this type as a float if it is one. If this is +// not a float, then this panics. +func (obj *VariantValue) Float() float64 { + //return obj.V.(float64) + return obj.V.Float() +} + +// List represents the value of this type as a list if it is one. If this is not +// a list, then this panics. +func (obj *VariantValue) List() []Value { + return obj.V.List() +} + +// Map represents the value of this type as a dictionary if it is one. If this +// is not a map, then this panics. +func (obj *VariantValue) Map() map[Value]Value { + return obj.V.Map() +} + +// Struct represents the value of this type as a struct if it is one. If this is +// not a struct, then this panics. +func (obj *VariantValue) Struct() map[string]Value { + return obj.V.Struct() +} + +// Func represents the value of this type as a function if it is one. If this is +// not a function, then this panics. +func (obj *VariantValue) Func() func([]Value) (Value, error) { + return obj.V.Func() +} diff --git a/lang/types/value_test.go b/lang/types/value_test.go new file mode 100644 index 00000000..f115b9af --- /dev/null +++ b/lang/types/value_test.go @@ -0,0 +1,588 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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 types + +import ( + "fmt" + "sort" + "testing" +) + +func TestPrint1(t *testing.T) { + values := map[Value]string{ + &BoolValue{V: true}: "true", + &BoolValue{V: false}: "false", + &StrValue{V: ""}: `""`, + &StrValue{V: "hello"}: `"hello"`, + &StrValue{V: "hello\tworld"}: `"hello\tworld"`, + &StrValue{V: "hello\nworld"}: `"hello\nworld"`, + &StrValue{V: "hello\\world"}: `"hello\\world"`, + &StrValue{V: "hello\t\nworld"}: `"hello\t\nworld"`, + &StrValue{V: "\\"}: `"\\"`, + &IntValue{V: 0}: "0", + &IntValue{V: -0}: "0", + &IntValue{V: 42}: "42", + &IntValue{V: -13}: "-13", + &FloatValue{V: 0.0}: "0", // TODO: is this correct? + &FloatValue{V: 0}: "0", // TODO: is this correct? + &FloatValue{V: -4.2}: "-4.2", + &FloatValue{V: 1.2}: "1.2", + &FloatValue{V: -0.0}: "0", // TODO: is this correct? + &ListValue{V: []Value{}}: `[]`, + &ListValue{V: []Value{ + &IntValue{V: 42}, + &IntValue{V: -13}, + &IntValue{V: 0}}, + }: `[42, -13, 0]`, + &ListValue{V: []Value{ + &StrValue{V: "a"}, + &StrValue{V: "bb"}, + &StrValue{V: "ccc"}}, + }: `["a", "bb", "ccc"]`, + &ListValue{V: []Value{ // prints okay, but is actually invalid! + &StrValue{V: "hello"}, + &IntValue{V: 4}, + &BoolValue{V: true}}, + }: `["hello", 4, true]`, + + &ListValue{V: []Value{ + &ListValue{V: []Value{ + &StrValue{V: "a"}, + &StrValue{V: "bb"}, + &StrValue{V: "ccc"}, + }}, + &ListValue{V: []Value{ + &StrValue{V: "d"}, + &StrValue{V: "ee"}, + &StrValue{V: "fff"}, + }}, + &ListValue{V: []Value{ + &StrValue{V: "g"}, + &StrValue{V: "hh"}, + &StrValue{V: "iii"}, + }}, + }}: `[["a", "bb", "ccc"], ["d", "ee", "fff"], ["g", "hh", "iii"]]`, + } + + d0 := NewMap(NewType("{str: int}")) + values[d0] = `{}` + + d1 := NewMap(NewType("{str: int}")) + d1.Add(&StrValue{V: "answer"}, &IntValue{V: 42}) + values[d1] = `{"answer": 42}` + + d2 := NewMap(NewType("{str: int}")) + d2.Add(&StrValue{V: "answer"}, &IntValue{V: 42}) + d2.Add(&StrValue{V: "hello"}, &IntValue{V: 13}) + values[d2] = `{"answer": 42, "hello": 13}` + + s0 := NewStruct(NewType("struct{}")) + values[s0] = `struct{}` + + s1 := NewStruct(NewType("struct{answer int}")) + values[s1] = `struct{answer: 0}` + + s2 := NewStruct(NewType("struct{answer int; truth bool; hello str}")) + values[s2] = `struct{answer: 0; truth: false; hello: ""}` + + s3 := NewStruct(NewType("struct{answer int; truth bool; hello str; nested []int}")) + values[s3] = `struct{answer: 0; truth: false; hello: ""; nested: []}` + + s4 := NewStruct(NewType("struct{answer int; truth bool; hello str; nested []int}")) + if err := s4.Set("answer", &IntValue{V: 42}); err != nil { + t.Errorf("struct could not set key, error: %v", err) + return + } + if err := s4.Set("truth", &BoolValue{V: true}); err != nil { + t.Errorf("struct could not set key, error: %v", err) + return + } + values[s4] = `struct{answer: 42; truth: true; hello: ""; nested: []}` + + for v, exp := range values { // run all the tests + if s := v.String(); s != exp { + t.Errorf("value representation of `%s` did not match expected: `%s`", s, exp) + } + } +} + +func TestReflectValue1(t *testing.T) { + // value string representations in golang can be ambiguous, see below... + values := map[Value]string{ + &BoolValue{V: true}: "true", + &BoolValue{V: false}: "false", + &StrValue{V: ""}: ``, + &StrValue{V: "hello"}: `hello`, + &StrValue{V: "hello\tworld"}: "hello\tworld", + &StrValue{V: "hello\nworld"}: "hello\nworld", + &StrValue{V: "hello\\world"}: "hello\\world", + &StrValue{V: "hello\t\nworld"}: "hello\t\nworld", + &StrValue{V: "\\"}: "\\", + &IntValue{V: 0}: "0", + &IntValue{V: -0}: "0", + &IntValue{V: 42}: "42", + &IntValue{V: -13}: "-13", + &ListValue{ + T: NewType("[]int"), + V: []Value{}, + }: `[]`, + &ListValue{ + T: NewType("[]int"), + V: []Value{ + &IntValue{V: 42}, + &IntValue{V: -13}, + &IntValue{V: 0}, + }, + }: `[42 -13 0]`, + &ListValue{ + T: NewType("[]str"), + V: []Value{ + &StrValue{V: "a"}, + &StrValue{V: "bb"}, + &StrValue{V: "ccc"}, + }, + }: `[a bb ccc]`, + &ListValue{ + T: NewType("[]str"), + V: []Value{ + &StrValue{V: "a bb ccc"}, + }, + }: `[a bb ccc]`, // note how this is ambiguous in golang! + &ListValue{ + T: NewType("[][]str"), + V: []Value{ + &ListValue{ + T: NewType("[]str"), + V: []Value{ + &StrValue{V: "a"}, + &StrValue{V: "bb"}, + &StrValue{V: "ccc"}, + }, + }, + &ListValue{ + T: NewType("[]str"), + V: []Value{ + &StrValue{V: "d"}, + &StrValue{V: "ee"}, + &StrValue{V: "fff"}, + }, + }, + &ListValue{ + T: NewType("[]str"), + V: []Value{ + &StrValue{V: "g"}, + &StrValue{V: "hh"}, + &StrValue{V: "iii"}, + }, + }, + }, + }: `[[a bb ccc] [d ee fff] [g hh iii]]`, + } + + d0 := NewMap(NewType("{str: int}")) + values[d0] = `map[]` + + d1 := NewMap(NewType("{str: int}")) + d1.Add(&StrValue{V: "answer"}, &IntValue{V: 42}) + values[d1] = `map[answer:42]` + + // multiple key maps are tested below since they have multiple outputs + // TODO: https://github.com/golang/go/issues/21095 + + s0 := NewStruct(NewType("struct{}")) + values[s0] = `{}` + + s1 := NewStruct(NewType("struct{Answer int}")) + values[s1] = `{Answer:0}` + + s2 := NewStruct(NewType("struct{Answer int; Truth bool; Hello str}")) + values[s2] = `{Answer:0 Truth:false Hello:}` + + s3 := NewStruct(NewType("struct{Answer int; Truth bool; Hello str; Nested []int}")) + values[s3] = `{Answer:0 Truth:false Hello: Nested:[]}` + + s4 := NewStruct(NewType("struct{Answer int; Truth bool; Hello str; Nested []int}")) + if err := s4.Set("Answer", &IntValue{V: 42}); err != nil { + t.Errorf("struct could not set key, error: %v", err) + return + } + if err := s4.Set("Truth", &BoolValue{V: true}); err != nil { + t.Errorf("struct could not set key, error: %v", err) + return + } + values[s4] = `{Answer:42 Truth:true Hello: Nested:[]}` + + for v, exp := range values { // run all the tests + //t.Logf("expected: %s", exp) + if v == nil { + t.Logf("nil: %s", exp) + continue + } + val := v.Value() + if s := fmt.Sprintf("%+v", val); s != exp { + //t.Errorf("value representation of `%s` did not match expected: `%s`", s, exp) + t.Errorf("value representation of `%s`", s) + t.Errorf("did not match expected: `%s`", exp) + } + } +} + +func TestSort1(t *testing.T) { + type test struct { // an individual test + values []Value + sorted []Value + } + values := []test{ + { + []Value{}, + []Value{}, + }, + { + []Value{ + &BoolValue{V: true}, + }, + []Value{ + &BoolValue{V: true}, + }, + }, + { + []Value{ + &BoolValue{V: true}, + &BoolValue{V: false}, + }, + []Value{ + &BoolValue{V: false}, + &BoolValue{V: true}, + }, + }, + { + []Value{ + &BoolValue{V: false}, + &BoolValue{V: false}, + &BoolValue{V: true}, + &BoolValue{V: false}, + }, + []Value{ + &BoolValue{V: false}, + &BoolValue{V: false}, + &BoolValue{V: false}, + &BoolValue{V: true}, + }, + }, + { + []Value{ + &StrValue{V: "c"}, + &StrValue{V: "a"}, + &StrValue{V: "b"}, + }, + []Value{ + &StrValue{V: "a"}, + &StrValue{V: "b"}, + &StrValue{V: "c"}, + }, + }, + { + []Value{ + &StrValue{V: "c"}, + &StrValue{V: "aa"}, + &StrValue{V: "b"}, + }, + []Value{ + &StrValue{V: "aa"}, + &StrValue{V: "b"}, + &StrValue{V: "c"}, + }, + }, + { + []Value{ + &StrValue{V: "c"}, + &StrValue{V: "bb"}, + &StrValue{V: "a"}, + }, + []Value{ + &StrValue{V: "a"}, + &StrValue{V: "bb"}, + &StrValue{V: "c"}, + }, + }, + { + []Value{ + &IntValue{V: 2}, + &IntValue{V: 0}, + &IntValue{V: 3}, + &IntValue{V: 1}, + }, + []Value{ + &IntValue{V: 0}, + &IntValue{V: 1}, + &IntValue{V: 2}, + &IntValue{V: 3}, + }, + }, + { + []Value{ + &IntValue{V: 2}, + &IntValue{V: 0}, + &IntValue{V: -3}, + &IntValue{V: 1}, + &IntValue{V: 42}, + }, + []Value{ + &IntValue{V: -3}, + &IntValue{V: 0}, + &IntValue{V: 1}, + &IntValue{V: 2}, + &IntValue{V: 42}, + }, + }, + { + []Value{ + &ListValue{ + V: []Value{ + &StrValue{V: "c"}, + }, + T: NewType("[]str"), + }, + &ListValue{ + V: []Value{ + &StrValue{V: "bb"}, + }, + T: NewType("[]str"), + }, + &ListValue{ + V: []Value{ + &StrValue{V: "a"}, + }, + T: NewType("[]str"), + }, + }, + []Value{ + &ListValue{ + V: []Value{ + &StrValue{V: "a"}, + }, + T: NewType("[]str"), + }, + &ListValue{ + V: []Value{ + &StrValue{V: "bb"}, + }, + T: NewType("[]str"), + }, + &ListValue{ + V: []Value{ + &StrValue{V: "c"}, + }, + T: NewType("[]str"), + }, + }, + }, + { + []Value{ + &ListValue{ + V: []Value{ + &StrValue{V: "c"}, + }, + T: NewType("[]str"), + }, + &ListValue{ + V: []Value{ + &StrValue{V: "bb"}, + &StrValue{V: "zz"}, + }, + T: NewType("[]str"), + }, + &ListValue{ + V: []Value{ + &StrValue{V: "a"}, + &StrValue{V: "zzz"}, + }, + T: NewType("[]str"), + }, + }, + []Value{ + &ListValue{ + V: []Value{ + &StrValue{V: "a"}, + &StrValue{V: "zzz"}, + }, + T: NewType("[]str"), + }, + &ListValue{ + V: []Value{ + &StrValue{V: "bb"}, + &StrValue{V: "zz"}, + }, + T: NewType("[]str"), + }, + &ListValue{ + V: []Value{ + &StrValue{V: "c"}, + }, + T: NewType("[]str"), + }, + }, + }, + // FIXME: add map and struct sorting tests + } + + for index, test := range values { // run all the tests + v1, v2 := test.values, test.sorted + sort.Sort(ValueSlice(v1)) // sort it :) + + if l1, l2 := len(v1), len(v2); l1 != l2 { + t.Errorf("sort test #%d: had wrong length got %d, expected %d", index, l1, l2) + continue + } + // cmp two lists each element at a time + for i := 0; i < len(v1); i++ { + if err := v1[i].Cmp(v2[i]); err != nil { + t.Errorf("sort test #%d: value did not match expected: %v", index, err) + t.Errorf("got: `%+v`", v1) + t.Errorf("exp: `%+v`", v2) + break + } + } + } +} + +func TestMapReflectValue1(t *testing.T) { + d := NewMap(NewType("{str: int}")) + d.Add(&StrValue{V: "answer"}, &IntValue{V: 42}) + d.Add(&StrValue{V: "hello"}, &IntValue{V: 13}) + // both are valid, since map's aren't sorted + // imo, golang should at least sort these on display! + // TODO: https://github.com/golang/go/issues/21095 + exp1 := `map[answer:42 hello:13]` + exp2 := `map[hello:13 answer:42]` + + val := d.Value() + if s := fmt.Sprintf("%+v", val); s != exp1 && s != exp2 { + t.Errorf("value representation of `%s`", s) + t.Errorf("did not match expected: `%s`", exp1) + t.Errorf("did not match expected: `%s`", exp2) + } + + d2 := NewMap(NewType("{str: str}")) + d2.Add(&StrValue{V: "answer"}, &StrValue{V: "42 hello:13"}) + val2 := d2.Value() + + if v1, v2 := fmt.Sprintf("%+v", val), fmt.Sprintf("%+v", val2); v1 == v2 { + t.Logf("golang maps are ambiguous") + } else { + //t.Errorf("golang maps are broken ?") + //t.Errorf("val1: %s", v1) + //t.Errorf("val2: %s", v2) + } +} + +func TestList1(t *testing.T) { + l := NewList(NewType("[]int")) + v := &IntValue{V: 42} + if err := l.Add(v); err != nil { + t.Errorf("list could not add value: %s", v) + } + + value, exists := l.Lookup(0) // the index! + if !exists { + t.Errorf("list did not contain our value") + return + } + + if err := value.Cmp(&IntValue{V: 42}); err != nil { + t.Errorf("value did not match our list value") + } +} + +func TestMapLookup1(t *testing.T) { + d := NewMap(NewType("{str: int}")) + k := &StrValue{V: "answer"} + v := &IntValue{V: 42} + if err := d.Add(k, v); err != nil { + t.Errorf("map could not add key %s, val: %s", k, v) + } + + //value, exists := d.Lookup(k) // not what we want, but would work! + value, exists := d.Lookup(&StrValue{V: "answer"}) // different pointer! + if !exists { + t.Errorf("map did not contain our key") + return + } + + if err := value.Cmp(&IntValue{V: 42}); err != nil { + t.Errorf("value did not match our map key") + } +} + +func TestStruct1(t *testing.T) { + s := NewStruct(NewType("struct{answer int; truth bool; hello str; nested []int}")) + if err := s.Set("answer", &IntValue{V: 42}); err != nil { + t.Errorf("struct could not set key, error: %v", err) + } + + if _, exists := s.Lookup("missing"); exists { + t.Errorf("struct incorrectly contained our field") + return + } + + value, exists := s.Lookup("answer") // different pointer! + if !exists { + t.Errorf("struct did not contain our field") + return + } + + if err := value.Cmp(&IntValue{V: 42}); err != nil { + t.Errorf("value did not match our struct field") + } +} + +func TestStruct2(t *testing.T) { + st := NewStruct(NewType("struct{Answer int; Truth bool; Hello str; Nested []int}")) + if err := st.Set("Answer", &IntValue{V: 42}); err != nil { + t.Errorf("struct could not set key, error: %v", err) + return + } + if err := st.Set("Truth", &BoolValue{V: true}); err != nil { + t.Errorf("struct could not set key, error: %v", err) + return + } + v := st.Value() + //v.Answer = -13 // won't work, this is an interface! + if val := fmt.Sprintf("%+v", v); val != `{Answer:42 Truth:true Hello: Nested:[]}` { + t.Errorf("struct displayed wrong value: %s", val) + } + if typ := fmt.Sprintf("%T", v); typ != `struct { Answer int64; Truth bool; Hello string; Nested []int64 }` { + t.Errorf("struct displayed type value: %s", typ) + } + + // show that golang structs are ambiguous + st2 := NewStruct(NewType("struct{Answer str}")) + if err := st2.Set("Answer", &StrValue{V: "42 Truth:true Hello: Nested:[]"}); err != nil { + t.Errorf("struct could not set key, error: %v", err) + return + } + v2 := st2.Value() + + if val1, val2 := fmt.Sprintf("%+v", v), fmt.Sprintf("%+v", v2); val1 == val2 { + t.Logf("golang structs are ambiguous") + } else { + //t.Errorf("golang structs are broken ?") + //t.Errorf("val1: %s", val1) + //t.Errorf("val2: %s", val2) + } +} diff --git a/lang/unification/simplesolver.go b/lang/unification/simplesolver.go new file mode 100644 index 00000000..08c09e68 --- /dev/null +++ b/lang/unification/simplesolver.go @@ -0,0 +1,562 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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 unification // TODO: can we put this solver in a sub-package? + +import ( + "fmt" + + "github.com/purpleidea/mgmt/lang/interfaces" + "github.com/purpleidea/mgmt/lang/types" + + errwrap "github.com/pkg/errors" +) + +const ( + // Name is the prefix for our solver log messages. + Name = "solver: simple" +) + +// SimpleInvariantSolverLogger is a wrapper which returns a +// SimpleInvariantSolver with the log parameter of your choice specified. The +// result satisfies the correct signature for the solver parameter of the +// Unification function. +func SimpleInvariantSolverLogger(logf func(format string, v ...interface{})) func([]interfaces.Invariant) (*InvariantSolution, error) { + return func(invariants []interfaces.Invariant) (*InvariantSolution, error) { + return SimpleInvariantSolver(invariants, logf) + } +} + +// SimpleInvariantSolver is an iterative invariant solver for AST expressions. +// It is intended to be very simple, even if it's computationally inefficient. +func SimpleInvariantSolver(invariants []interfaces.Invariant, logf func(format string, v ...interface{})) (*InvariantSolution, error) { + logf("%s: invariants:", Name) + for i, x := range invariants { + logf("invariant(%d): %T: %s", i, x, x) + } + + solved := make(map[interfaces.Expr]*types.Type) + equalities := []interfaces.Invariant{} + exclusives := []*ExclusiveInvariant{} + // iterate through all invariants, flattening and sorting the list... + for _, x := range invariants { + switch invariant := x.(type) { + case *EqualsInvariant: + equalities = append(equalities, invariant) + + case *EqualityInvariant: + equalities = append(equalities, invariant) + + case *EqualityInvariantList: + // de-construct this list variant into a series + // of equality variants so that our solver can + // be implemented more simply... + if len(invariant.Exprs) < 2 { + return nil, fmt.Errorf("list invariant needs at least two elements") + } + for i := 0; i < len(invariant.Exprs)-1; i++ { + invar := &EqualityInvariant{ + Expr1: invariant.Exprs[i], + Expr2: invariant.Exprs[i+1], + } + equalities = append(equalities, invar) + } + + case *EqualityWrapListInvariant: + equalities = append(equalities, invariant) + + case *EqualityWrapMapInvariant: + equalities = append(equalities, invariant) + + case *EqualityWrapStructInvariant: + equalities = append(equalities, invariant) + + case *EqualityWrapFuncInvariant: + equalities = append(equalities, invariant) + + // contains a list of invariants which this represents + case *ConjunctionInvariant: + for _, invar := range invariant.Invariants { + equalities = append(equalities, invar) + } + + case *ExclusiveInvariant: + // these are special, note the different list + if len(invariant.Invariants) > 0 { + exclusives = append(exclusives, invariant) + } + + case *AnyInvariant: + equalities = append(equalities, invariant) + + default: + return nil, fmt.Errorf("unknown invariant type: %T", x) + } + } + + listPartials := make(map[interfaces.Expr]map[interfaces.Expr]*types.Type) + mapPartials := make(map[interfaces.Expr]map[interfaces.Expr]*types.Type) + structPartials := make(map[interfaces.Expr]map[interfaces.Expr]*types.Type) + funcPartials := make(map[interfaces.Expr]map[interfaces.Expr]*types.Type) + + logf("%s: starting loop with %d equalities", Name, len(equalities)) + // run until we're solved, stop consuming equalities, or type clash + for { + logf("%s: iterate...", Name) + if len(equalities) == 0 && len(exclusives) == 0 { + break // we're done, nothing left + } + used := []int{} + for i, x := range equalities { + logf("%s: match(%T): %+v", Name, x, x) + + // TODO: could each of these cases be implemented as a + // method on the Invariant type to simplify this code? + switch eq := x.(type) { + // trivials + case *EqualsInvariant: + typ, exists := solved[eq.Expr] + if !exists { + solved[eq.Expr] = eq.Type // yay, we learned something! + used = append(used, i) // mark equality as used up + logf("%s: solved trivial equality", Name) + continue + } + // we already specified this, so check the repeat is consistent + if err := typ.Cmp(eq.Type); err != nil { + // this error shouldn't happen unless we purposefully + // try to trick the solver, or we're in a recursive try + return nil, errwrap.Wrapf(err, "can't unify, invariant illogicality with equals") + } + used = append(used, i) // mark equality as duplicate + logf("%s: duplicate trivial equality", Name) + continue + + // partials + case *EqualityWrapListInvariant: + if _, exists := listPartials[eq.Expr1]; !exists { + listPartials[eq.Expr1] = make(map[interfaces.Expr]*types.Type) + } + + if typ, exists := solved[eq.Expr1]; exists { + // wow, now known, so tell the partials! + listPartials[eq.Expr1][eq.Expr2Val] = typ.Val + } + + // can we add to partials ? + for _, y := range []interfaces.Expr{eq.Expr2Val} { + typ, exists := solved[y] + if !exists { + continue + } + t, exists := listPartials[eq.Expr1][y] + if !exists { + listPartials[eq.Expr1][y] = typ // learn! + continue + } + if err := t.Cmp(typ); err != nil { + return nil, errwrap.Wrapf(err, "can't unify, invariant illogicality with partial list val") + } + } + + // can we solve anything? + var ready = true // assume ready + typ := &types.Type{ + Kind: types.KindList, + } + valTyp, exists := listPartials[eq.Expr1][eq.Expr2Val] + if !exists { + ready = false // nope! + } else { + typ.Val = valTyp // build up typ + } + if ready { + if t, exists := solved[eq.Expr1]; exists { + if err := t.Cmp(typ); err != nil { + return nil, errwrap.Wrapf(err, "can't unify, invariant illogicality with list") + } + } + // sub checks + if t, exists := solved[eq.Expr2Val]; exists { + if err := t.Cmp(typ.Val); err != nil { + return nil, errwrap.Wrapf(err, "can't unify, invariant illogicality with list val") + } + } + + solved[eq.Expr1] = typ // yay, we learned something! + solved[eq.Expr2Val] = typ.Val // yay, we learned something! + used = append(used, i) // mark equality as used up + logf("%s: solved list wrap partial", Name) + continue + } + + case *EqualityWrapMapInvariant: + if _, exists := mapPartials[eq.Expr1]; !exists { + mapPartials[eq.Expr1] = make(map[interfaces.Expr]*types.Type) + } + + if typ, exists := solved[eq.Expr1]; exists { + // wow, now known, so tell the partials! + mapPartials[eq.Expr1][eq.Expr2Key] = typ.Key + mapPartials[eq.Expr1][eq.Expr2Val] = typ.Val + } + + // can we add to partials ? + for _, y := range []interfaces.Expr{eq.Expr2Key, eq.Expr2Val} { + typ, exists := solved[y] + if !exists { + continue + } + t, exists := mapPartials[eq.Expr1][y] + if !exists { + mapPartials[eq.Expr1][y] = typ // learn! + continue + } + if err := t.Cmp(typ); err != nil { + return nil, errwrap.Wrapf(err, "can't unify, invariant illogicality with partial map key/val") + } + } + + // can we solve anything? + var ready = true // assume ready + typ := &types.Type{ + Kind: types.KindMap, + } + keyTyp, exists := mapPartials[eq.Expr1][eq.Expr2Key] + if !exists { + ready = false // nope! + } else { + typ.Key = keyTyp // build up typ + } + valTyp, exists := mapPartials[eq.Expr1][eq.Expr2Val] + if !exists { + ready = false // nope! + } else { + typ.Val = valTyp // build up typ + } + if ready { + if t, exists := solved[eq.Expr1]; exists { + if err := t.Cmp(typ); err != nil { + return nil, errwrap.Wrapf(err, "can't unify, invariant illogicality with map") + } + } + // sub checks + if t, exists := solved[eq.Expr2Key]; exists { + if err := t.Cmp(typ.Key); err != nil { + return nil, errwrap.Wrapf(err, "can't unify, invariant illogicality with map key") + } + } + if t, exists := solved[eq.Expr2Val]; exists { + if err := t.Cmp(typ.Val); err != nil { + return nil, errwrap.Wrapf(err, "can't unify, invariant illogicality with map val") + } + } + + solved[eq.Expr1] = typ // yay, we learned something! + solved[eq.Expr2Key] = typ.Key // yay, we learned something! + solved[eq.Expr2Val] = typ.Val // yay, we learned something! + used = append(used, i) // mark equality as used up + logf("%s: solved map wrap partial", Name) + continue + } + + case *EqualityWrapStructInvariant: + if _, exists := structPartials[eq.Expr1]; !exists { + structPartials[eq.Expr1] = make(map[interfaces.Expr]*types.Type) + } + + if typ, exists := solved[eq.Expr1]; exists { + // wow, now known, so tell the partials! + for i, name := range eq.Expr2Ord { + expr := eq.Expr2Map[name] // assume key exists + structPartials[eq.Expr1][expr] = typ.Map[typ.Ord[i]] // assume key exists + } + } + + // can we add to partials ? + for name, y := range eq.Expr2Map { + typ, exists := solved[y] + if !exists { + continue + } + t, exists := structPartials[eq.Expr1][y] + if !exists { + structPartials[eq.Expr1][y] = typ // learn! + continue + } + if err := t.Cmp(typ); err != nil { + return nil, errwrap.Wrapf(err, "can't unify, invariant illogicality with partial struct field: %s", name) + } + } + + // can we solve anything? + var ready = true // assume ready + typ := &types.Type{ + Kind: types.KindStruct, + } + typ.Map = make(map[string]*types.Type) + for name, y := range eq.Expr2Map { + t, exists := structPartials[eq.Expr1][y] + if !exists { + ready = false // nope! + break + } + typ.Map[name] = t // build up typ + } + if ready { + typ.Ord = eq.Expr2Ord // known order + + if t, exists := solved[eq.Expr1]; exists { + if err := t.Cmp(typ); err != nil { + return nil, errwrap.Wrapf(err, "can't unify, invariant illogicality with struct") + } + } + // sub checks + for name, y := range eq.Expr2Map { + if t, exists := solved[y]; exists { + if err := t.Cmp(typ.Map[name]); err != nil { + return nil, errwrap.Wrapf(err, "can't unify, invariant illogicality with struct field: %s", name) + } + } + } + + solved[eq.Expr1] = typ // yay, we learned something! + // we should add the other expr's in too... + for name, y := range eq.Expr2Map { + solved[y] = typ.Map[name] // yay, we learned something! + } + used = append(used, i) // mark equality as used up + logf("%s: solved struct wrap partial", Name) + continue + } + + case *EqualityWrapFuncInvariant: + if _, exists := funcPartials[eq.Expr1]; !exists { + funcPartials[eq.Expr1] = make(map[interfaces.Expr]*types.Type) + } + + if typ, exists := solved[eq.Expr1]; exists { + // wow, now known, so tell the partials! + for i, name := range eq.Expr2Ord { + expr := eq.Expr2Map[name] // assume key exists + funcPartials[eq.Expr1][expr] = typ.Map[typ.Ord[i]] // assume key exists + } + funcPartials[eq.Expr1][eq.Expr2Out] = typ.Out + } + + // can we add to partials ? + for name, y := range eq.Expr2Map { + typ, exists := solved[y] + if !exists { + continue + } + t, exists := funcPartials[eq.Expr1][y] + if !exists { + funcPartials[eq.Expr1][y] = typ // learn! + continue + } + if err := t.Cmp(typ); err != nil { + return nil, errwrap.Wrapf(err, "can't unify, invariant illogicality with partial func arg: %s", name) + } + } + for _, y := range []interfaces.Expr{eq.Expr2Out} { + typ, exists := solved[y] + if !exists { + continue + } + t, exists := funcPartials[eq.Expr1][y] + if !exists { + funcPartials[eq.Expr1][y] = typ // learn! + continue + } + if err := t.Cmp(typ); err != nil { + return nil, errwrap.Wrapf(err, "can't unify, invariant illogicality with partial func arg") + } + } + + // can we solve anything? + var ready = true // assume ready + typ := &types.Type{ + Kind: types.KindFunc, + } + typ.Map = make(map[string]*types.Type) + for name, y := range eq.Expr2Map { + t, exists := funcPartials[eq.Expr1][y] + if !exists { + ready = false // nope! + break + } + typ.Map[name] = t // build up typ + } + outTyp, exists := funcPartials[eq.Expr1][eq.Expr2Out] + if !exists { + ready = false // nope! + } else { + typ.Out = outTyp // build up typ + } + if ready { + typ.Ord = eq.Expr2Ord // known order + + if t, exists := solved[eq.Expr1]; exists { + if err := t.Cmp(typ); err != nil { + return nil, errwrap.Wrapf(err, "can't unify, invariant illogicality with func") + } + } + // sub checks + for name, y := range eq.Expr2Map { + if t, exists := solved[y]; exists { + if err := t.Cmp(typ.Map[name]); err != nil { + return nil, errwrap.Wrapf(err, "can't unify, invariant illogicality with func arg: %s", name) + } + } + } + if t, exists := solved[eq.Expr2Out]; exists { + if err := t.Cmp(typ.Out); err != nil { + return nil, errwrap.Wrapf(err, "can't unify, invariant illogicality with func out") + } + } + + solved[eq.Expr1] = typ // yay, we learned something! + // we should add the other expr's in too... + for name, y := range eq.Expr2Map { + solved[y] = typ.Map[name] // yay, we learned something! + } + solved[eq.Expr2Out] = typ.Out // yay, we learned something! + used = append(used, i) // mark equality as used up + logf("%s: solved func wrap partial", Name) + continue + } + + // regular matching + case *EqualityInvariant: + typ1, exists1 := solved[eq.Expr1] + typ2, exists2 := solved[eq.Expr2] + + if !exists1 && !exists2 { // neither equality connects + // can't learn more from this equality yet + // nothing is known about either side of it + continue + } + if exists1 && exists2 { // both equalities already connect + // both sides are already known-- are they the same? + if err := typ1.Cmp(typ2); err != nil { + return nil, errwrap.Wrapf(err, "can't unify, invariant illogicality with equality") + } + used = append(used, i) // mark equality as used up + logf("%s: duplicate regular equality", Name) + continue + } + if exists1 && !exists2 { // first equality already connects + solved[eq.Expr2] = typ1 // yay, we learned something! + used = append(used, i) // mark equality as used up + logf("%s: solved regular equality", Name) + continue + } + if exists2 && !exists1 { // second equality already connects + solved[eq.Expr1] = typ2 // yay, we learned something! + used = append(used, i) // mark equality as used up + logf("%s: solved regular equality", Name) + continue + } + + panic("reached unexpected code") + + // wtf matching + case *AnyInvariant: + // this basically ensures that the expr gets solved + if _, exists := solved[eq.Expr]; exists { + used = append(used, i) // mark equality as used up + logf("%s: solved `any` equality", Name) + } + continue + + default: + return nil, fmt.Errorf("unknown invariant type: %T", x) + } + } // end inner for loop + if len(used) == 0 { + // looks like we're now ambiguous, but if we have any + // exclusives, recurse into each possibility to see if + // one of them can help solve this! first one wins. add + // in the exclusive to the current set of equalities! + + // what have we learned for sure so far? + partialSolutions := []interfaces.Invariant{} + logf("%s: %d solved, %d unsolved, and %d exclusives left", Name, len(solved), len(equalities), len(exclusives)) + if len(exclusives) > 0 { + // FIXME: can we do this loop in a deterministic, sorted way? + for expr, typ := range solved { + invar := &EqualsInvariant{ + Expr: expr, + Type: typ, + } + partialSolutions = append(partialSolutions, invar) + logf("%s: solved: %+v", Name, invar) + } + + // also include anything that hasn't been solved yet + for _, x := range equalities { + partialSolutions = append(partialSolutions, x) + logf("%s: unsolved: %+v", Name, x) + } + } + + // let's try each combination, one at a time... + for i, ex := range exclusivesProduct(exclusives) { // [][]interfaces.Invariant + logf("%s: exclusive(%d):\n%+v", Name, i, ex) + // we could waste a lot of cpu, and start from + // the beginning, but instead we could use the + // list of known solutions found and continue! + // TODO: make sure none of these edit partialSolutions + recursiveInvariants := []interfaces.Invariant{} + recursiveInvariants = append(recursiveInvariants, partialSolutions...) + recursiveInvariants = append(recursiveInvariants, ex...) + logf("%s: recursing...", Name) + solution, err := SimpleInvariantSolver(recursiveInvariants, logf) + if err != nil { + logf("%s: recursive solution failed: %+v", Name, err) + continue // no solution found here... + } + // solution found! + logf("%s: recursive solution found!", Name) + return solution, nil + } + + // TODO: print ambiguity + return nil, fmt.Errorf("can't unify, no equalities were consumed, we're ambiguous") + } + // delete used equalities, in reverse order to preserve indexing! + for i := len(used) - 1; i >= 0; i-- { + ix := used[i] // delete index that was marked as used! + equalities = append(equalities[:ix], equalities[ix+1:]...) + } + } // end giant for loop + + // build final solution + solutions := []*EqualsInvariant{} + // FIXME: can we do this loop in a deterministic, sorted way? + for expr, typ := range solved { + invar := &EqualsInvariant{ + Expr: expr, + Type: typ, + } + solutions = append(solutions, invar) + } + return &InvariantSolution{ + Solutions: solutions, + }, nil +} diff --git a/lang/unification/unification.go b/lang/unification/unification.go new file mode 100644 index 00000000..df650145 --- /dev/null +++ b/lang/unification/unification.go @@ -0,0 +1,285 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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 unification + +import ( + "fmt" + "strings" + + "github.com/purpleidea/mgmt/lang/interfaces" + "github.com/purpleidea/mgmt/lang/types" +) + +// Unify takes an AST expression tree and attempts to assign types to every node +// using the specified solver. The expression tree returns a list of invariants +// (or constraints) which must be met in order to find a unique value for the +// type of each expression. This list of invariants is passed into the solver, +// which hopefully finds a solution. If it cannot find a unique solution, then +// it will return an error. The invariants are available in different flavours +// which describe different constraint scenarios. The simplest expresses that a +// a particular node id (it's pointer) must be a certain type. More complicated +// invariants might express that two different node id's must have the same +// type. This function and logic was invented after the author could not find +// any proper literature or examples describing a well-known implementation of +// this process. Improvements and polite recommendations are welcome. +func Unify(ast interfaces.Stmt, solver func([]interfaces.Invariant) (*InvariantSolution, error)) error { + //log.Printf("unification: tree: %+v", ast) // debug + if ast == nil { + return fmt.Errorf("AST is nil") + } + + invariants, err := ast.Unify() + if err != nil { + return err + } + + solved, err := solver(invariants) + if err != nil { + return err + } + + // TODO: ideally we would know how many different expressions need their + // types set in the AST and then ensure we have this many unique + // solutions, and if not, then fail. This would ensure we don't have an + // AST that is only partially populated with the correct types. + + //log.Printf("unification: found a solution!") // TODO: get a logf function passed in... + // solver has found a solution, apply it... + // we're modifying the AST, so code can't error now... + for _, x := range solved.Solutions { + //log.Printf("unification: solution: %p => %+v\t(%+v)", x.Expr, x.Type, x.Expr.String()) // debug + // apply this to each AST node + if err := x.Expr.SetType(x.Type); err != nil { + // programming error! + panic(fmt.Sprintf("error setting type: %+v, error: %+v", x.Expr, err)) + } + } + return nil +} + +// EqualsInvariant is an invariant that symbolizes that the expression has a +// known type. +// TODO: is there a better name than EqualsInvariant +type EqualsInvariant struct { + Expr interfaces.Expr + Type *types.Type +} + +// String returns a representation of this invariant. +func (obj *EqualsInvariant) String() string { + return fmt.Sprintf("%p == %s", obj.Expr, obj.Type) +} + +// EqualityInvariant is an invariant that symbolizes that the two expressions +// must have equivalent types. +// TODO: is there a better name than EqualityInvariant +type EqualityInvariant struct { + Expr1 interfaces.Expr + Expr2 interfaces.Expr +} + +// String returns a representation of this invariant. +func (obj *EqualityInvariant) String() string { + return fmt.Sprintf("%p == %p", obj.Expr1, obj.Expr2) +} + +// EqualityInvariantList is an invariant that symbolizes that all the +// expressions listed must have equivalent types. +type EqualityInvariantList struct { + Exprs []interfaces.Expr +} + +// String returns a representation of this invariant. +func (obj *EqualityInvariantList) String() string { + var a []string + for _, x := range obj.Exprs { + a = append(a, fmt.Sprintf("%p", x)) + } + return fmt.Sprintf("[%s]", strings.Join(a, ", ")) +} + +// EqualityWrapListInvariant expresses that a list in Expr1 must have elements +// that have the same type as the expression in Expr2Val. +type EqualityWrapListInvariant struct { + Expr1 interfaces.Expr + Expr2Val interfaces.Expr +} + +// String returns a representation of this invariant. +func (obj *EqualityWrapListInvariant) String() string { + return fmt.Sprintf("%p == [%p]", obj.Expr1, obj.Expr2Val) +} + +// EqualityWrapMapInvariant expresses that a map in Expr1 must have keys that +// match the type of the expression in Expr2Key and values that match the type +// of the expression in Expr2Val. +type EqualityWrapMapInvariant struct { + Expr1 interfaces.Expr + Expr2Key interfaces.Expr + Expr2Val interfaces.Expr +} + +// String returns a representation of this invariant. +func (obj *EqualityWrapMapInvariant) String() string { + return fmt.Sprintf("%p == {%p: %p}", obj.Expr1, obj.Expr2Key, obj.Expr2Val) +} + +// EqualityWrapStructInvariant expresses that a struct in Expr1 must have fields +// that match the type of the expressions listed in Expr2Map. +type EqualityWrapStructInvariant struct { + Expr1 interfaces.Expr + Expr2Map map[string]interfaces.Expr + Expr2Ord []string +} + +// String returns a representation of this invariant. +func (obj *EqualityWrapStructInvariant) String() string { + var s = make([]string, len(obj.Expr2Ord)) + for i, k := range obj.Expr2Ord { + t, ok := obj.Expr2Map[k] + if !ok { + panic("malformed struct order") + } + if t == nil { + panic("malformed struct field") + } + s[i] = fmt.Sprintf("%s %p", k, t) + } + return fmt.Sprintf("%p == struct{%s}", obj.Expr1, strings.Join(s, "; ")) +} + +// EqualityWrapFuncInvariant expresses that a func in Expr1 must have args that +// match the type of the expressions listed in Expr2Map and a return value that +// matches the type of the expression in Expr2Out. +// TODO: should this be named EqualityWrapCallInvariant or not? +type EqualityWrapFuncInvariant struct { + Expr1 interfaces.Expr + Expr2Map map[string]interfaces.Expr + Expr2Ord []string + Expr2Out interfaces.Expr +} + +// String returns a representation of this invariant. +func (obj *EqualityWrapFuncInvariant) String() string { + var s = make([]string, len(obj.Expr2Ord)) + for i, k := range obj.Expr2Ord { + t, ok := obj.Expr2Map[k] + if !ok { + panic("malformed func order") + } + if t == nil { + panic("malformed func field") + } + s[i] = fmt.Sprintf("%s %p", k, t) + } + return fmt.Sprintf("%p == func{%s} %p", obj.Expr1, strings.Join(s, "; "), obj.Expr2Out) +} + +// ConjunctionInvariant represents a list of invariants which must all be true +// together. In other words, it's a grouping construct for a set of invariants. +type ConjunctionInvariant struct { + Invariants []interfaces.Invariant +} + +// String returns a representation of this invariant. +func (obj *ConjunctionInvariant) String() string { + var a []string + for _, x := range obj.Invariants { + s := x.String() + a = append(a, s) + } + return fmt.Sprintf("[%s]", strings.Join(a, ", ")) +} + +// ExclusiveInvariant represents a list of invariants where one and *only* one +// should hold true. To combine multiple invariants in one of the list elements, +// you can group multiple invariants together using a ConjunctionInvariant. Do +// note that the solver might not verify that only one of the invariants in the +// list holds true, as it might choose to be lazy and pick the first solution +// found. +type ExclusiveInvariant struct { + Invariants []interfaces.Invariant +} + +// String returns a representation of this invariant. +func (obj *ExclusiveInvariant) String() string { + var a []string + for _, x := range obj.Invariants { + s := x.String() + a = append(a, s) + } + return fmt.Sprintf("[%s]", strings.Join(a, ", ")) +} + +// exclusivesProduct returns a list of different products produced from the +// combinatorial product of the list of exclusives. Each ExclusiveInvariant +// must contain between one and more Invariants. This takes every combination of +// Invariants (choosing one from each ExclusiveInvariant) and returns that list. +// In other words, if you have three exclusives, with invariants named (A1, B1), +// (A2), and (A3, B3, C3) you'll get: (A1, A2, A3), (A1, A2, B3), (A1, A2, C3), +// (B1, A2, A3), (B1, A2, B3), (B1, A2, C3) as results for this function call. +func exclusivesProduct(exclusives []*ExclusiveInvariant) [][]interfaces.Invariant { + if len(exclusives) == 0 { + return nil + } + + length := func(i int) int { return len(exclusives[i].Invariants) } + + // NextIx sets ix to the lexicographically next value, + // such that for each i > 0, 0 <= ix[i] < length(i). + NextIx := func(ix []int) { + for i := len(ix) - 1; i >= 0; i-- { + ix[i]++ + if i == 0 || ix[i] < length(i) { + return + } + ix[i] = 0 + } + } + + results := [][]interfaces.Invariant{} + + for ix := make([]int, len(exclusives)); ix[0] < length(0); NextIx(ix) { + x := []interfaces.Invariant{} + for j, k := range ix { + x = append(x, exclusives[j].Invariants[k]) + } + results = append(results, x) + } + + return results +} + +// AnyInvariant is an invariant that symbolizes that the expression can be any +// type. It is sometimes used to ensure that an expr actually gets a solution +// type so that it is not left unreferenced, and as a result, unsolved. +// TODO: is there a better name than AnyInvariant +type AnyInvariant struct { + Expr interfaces.Expr +} + +// String returns a representation of this invariant. +func (obj *AnyInvariant) String() string { + return fmt.Sprintf("%p == *", obj.Expr) +} + +// InvariantSolution lists a trivial set of EqualsInvariant mappings so that you +// can populate your AST with SetType calls in a simple loop. +type InvariantSolution struct { + Solutions []*EqualsInvariant // list of trivial solutions for each node +} diff --git a/lang/unification_test.go b/lang/unification_test.go new file mode 100644 index 00000000..02e139d7 --- /dev/null +++ b/lang/unification_test.go @@ -0,0 +1,567 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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 lang + +import ( + "fmt" + "testing" + + "github.com/purpleidea/mgmt/lang/interfaces" + "github.com/purpleidea/mgmt/lang/types" + "github.com/purpleidea/mgmt/lang/unification" +) + +func TestUnification1(t *testing.T) { + type test struct { // an individual test + name string + ast interfaces.Stmt // raw AST + fail bool + expect map[interfaces.Expr]*types.Type + } + values := []test{} + + // this causes a panic, so it can't be used + //{ + // values = append(values, test{ + // "nil", + // nil, + // true, // expect error + // nil, // no AST + // }) + //} + { + expr := &ExprStr{V: "hello"} + stmt := &StmtProg{ + Prog: []interfaces.Stmt{ + &StmtRes{ + Kind: "test", + Name: &ExprStr{V: "t1"}, + Fields: []*StmtResField{ + { + Field: "str", + Value: expr, + }, + }, + }, + }, + } + values = append(values, test{ + name: "one res", + ast: stmt, + fail: false, + expect: map[interfaces.Expr]*types.Type{ + expr: types.TypeStr, + }, + }) + } + { + v1 := &ExprStr{} + v2 := &ExprStr{} + v3 := &ExprStr{} + expr := &ExprList{ + Elements: []interfaces.Expr{ + v1, + v2, + v3, + }, + } + stmt := &StmtProg{ + Prog: []interfaces.Stmt{ + &StmtRes{ + Kind: "test", + Name: &ExprStr{V: "test"}, + Fields: []*StmtResField{ + { + Field: "slicestring", + Value: expr, + }, + }, + }, + }, + } + values = append(values, test{ + name: "list of strings", + ast: stmt, + fail: false, + expect: map[interfaces.Expr]*types.Type{ + v1: types.TypeStr, + v2: types.TypeStr, + v3: types.TypeStr, + expr: types.NewType("[]str"), + }, + }) + } + { + k1 := &ExprInt{} + k2 := &ExprInt{} + k3 := &ExprInt{} + v1 := &ExprFloat{} + v2 := &ExprFloat{} + v3 := &ExprFloat{} + expr := &ExprMap{ + KVs: []*ExprMapKV{ + {Key: k1, Val: v1}, + {Key: k2, Val: v2}, + {Key: k3, Val: v3}, + }, + } + stmt := &StmtProg{ + Prog: []interfaces.Stmt{ + &StmtRes{ + Kind: "test", + Name: &ExprStr{V: "test"}, + Fields: []*StmtResField{ + { + Field: "mapintfloat", + Value: expr, + }, + }, + }, + }, + } + values = append(values, test{ + name: "map of int->float", + ast: stmt, + fail: false, + expect: map[interfaces.Expr]*types.Type{ + k1: types.TypeInt, + k2: types.TypeInt, + k3: types.TypeInt, + v1: types.TypeFloat, + v2: types.TypeFloat, + v3: types.TypeFloat, + expr: types.NewType("{int: float}"), + }, + }) + } + { + b := &ExprBool{} + s := &ExprStr{} + i := &ExprInt{} + f := &ExprFloat{} + expr := &ExprStruct{ + Fields: []*ExprStructField{ + {Name: "somebool", Value: b}, + {Name: "somestr", Value: s}, + {Name: "someint", Value: i}, + {Name: "somefloat", Value: f}, + }, + } + stmt := &StmtProg{ + Prog: []interfaces.Stmt{ + &StmtRes{ + Kind: "test", + Name: &ExprStr{V: "test"}, + Fields: []*StmtResField{ + { + Field: "mixedstruct", + Value: expr, + }, + }, + }, + }, + } + values = append(values, test{ + name: "simple struct", + ast: stmt, + fail: false, + expect: map[interfaces.Expr]*types.Type{ + b: types.TypeBool, + s: types.TypeStr, + i: types.TypeInt, + f: types.TypeFloat, + expr: types.NewType("struct{somebool bool; somestr str; someint int; somefloat float}"), + }, + }) + } + { + // test "n1" { + // int64ptr => 13 + 42, + //} + expr := &ExprCall{ + Name: operatorFuncName, + Args: []interfaces.Expr{ + &ExprStr{ + V: "+", + }, + &ExprInt{ + V: 13, + }, + + &ExprInt{ + V: 42, + }, + }, + } + stmt := &StmtProg{ + Prog: []interfaces.Stmt{ + &StmtRes{ + Kind: "test", + Name: &ExprStr{ + V: "n1", + }, + Fields: []*StmtResField{ + { + Field: "int64ptr", + Value: expr, // func + }, + }, + }, + }, + } + values = append(values, test{ + name: "func call", + ast: stmt, + fail: false, + expect: map[interfaces.Expr]*types.Type{ + expr: types.NewType("int"), + }, + }) + } + { + //test "n1" { + // int64ptr => 13 + 42 - 4, + //} + innerFunc := &ExprCall{ + Name: operatorFuncName, + Args: []interfaces.Expr{ + &ExprStr{ + V: "-", + }, + &ExprInt{ + V: 42, + }, + &ExprInt{ + V: 4, + }, + }, + } + expr := &ExprCall{ + Name: operatorFuncName, + Args: []interfaces.Expr{ + &ExprStr{ + V: "+", + }, + &ExprInt{ + V: 13, + }, + innerFunc, // nested func, can we unify? + }, + } + stmt := &StmtProg{ + Prog: []interfaces.Stmt{ + &StmtRes{ + Kind: "test", + Name: &ExprStr{ + V: "n1", + }, + Fields: []*StmtResField{ + { + Field: "int64ptr", + Value: expr, + }, + }, + }, + }, + } + values = append(values, test{ + name: "func call, multiple ints", + ast: stmt, + fail: false, + expect: map[interfaces.Expr]*types.Type{ + innerFunc: types.NewType("int"), + expr: types.NewType("int"), + }, + }) + } + { + //test "n1" { + // float32 => -25.38789 + 32.6 + 13.7, + //} + innerFunc := &ExprCall{ + Name: operatorFuncName, + Args: []interfaces.Expr{ + &ExprStr{ + V: "+", + }, + &ExprFloat{ + V: 32.6, + }, + &ExprFloat{ + V: 13.7, + }, + }, + } + expr := &ExprCall{ + Name: operatorFuncName, + Args: []interfaces.Expr{ + &ExprStr{ + V: "+", + }, + &ExprFloat{ + V: -25.38789, + }, + innerFunc, // nested func, can we unify? + }, + } + stmt := &StmtProg{ + Prog: []interfaces.Stmt{ + &StmtRes{ + Kind: "test", + Name: &ExprStr{ + V: "n1", + }, + Fields: []*StmtResField{ + { + Field: "float32", + Value: expr, + }, + }, + }, + }, + } + values = append(values, test{ + name: "func call, multiple floats", + ast: stmt, + fail: false, + expect: map[interfaces.Expr]*types.Type{ + innerFunc: types.NewType("float"), + expr: types.NewType("float"), + }, + }) + } + { + //$x = 42 - 13 + innerFunc := &ExprCall{ + Name: operatorFuncName, + Args: []interfaces.Expr{ + &ExprStr{ + V: "-", + }, + &ExprInt{ + V: 42, + }, + &ExprInt{ + V: 13, + }, + }, + } + stmt := &StmtProg{ + Prog: []interfaces.Stmt{ + &StmtBind{ + Ident: "x", + Value: innerFunc, + }, + }, + } + values = append(values, test{ + name: "assign from func call or two ints", + ast: stmt, + fail: false, + expect: map[interfaces.Expr]*types.Type{ + innerFunc: types.NewType("int"), + }, + }) + } + { + //$x = template("hello", 42) + innerFunc := &ExprCall{ + Name: "template", + Args: []interfaces.Expr{ + &ExprStr{ + V: "hello", + }, + &ExprInt{ + V: 42, + }, + }, + } + stmt := &StmtProg{ + Prog: []interfaces.Stmt{ + &StmtBind{ + Ident: "x", + Value: innerFunc, + }, + }, + } + values = append(values, test{ + name: "simple template", + ast: stmt, + fail: false, + expect: map[interfaces.Expr]*types.Type{ + innerFunc: types.NewType("str"), + }, + }) + } + { + //$v = 42 + //$x = template("hello", $v) # redirect var for harder unification + innerFunc := &ExprCall{ + Name: "template", + Args: []interfaces.Expr{ + &ExprStr{ + V: "hello", // whatever... + }, + &ExprVar{ + Name: "x", + }, + }, + } + stmt := &StmtProg{ + Prog: []interfaces.Stmt{ + &StmtBind{ + Ident: "v", + Value: &ExprInt{ + V: 42, + }, + }, + &StmtBind{ + Ident: "x", + Value: innerFunc, + }, + }, + } + values = append(values, test{ + name: "complex template", + ast: stmt, + fail: false, + expect: map[interfaces.Expr]*types.Type{ + innerFunc: types.NewType("str"), + }, + }) + } + { + //test "t1" { + // stringptr => datetime(), # bad (str vs. int) + //} + expr := &ExprCall{ + Name: "datetime", + Args: []interfaces.Expr{}, + } + stmt := &StmtProg{ + Prog: []interfaces.Stmt{ + &StmtRes{ + Kind: "test", + Name: &ExprStr{V: "t1"}, + Fields: []*StmtResField{ + { + Field: "stringptr", + Value: expr, + }, + }, + }, + }, + } + values = append(values, test{ + name: "single fact unification", + ast: stmt, + fail: true, + }) + } + + for index, test := range values { // run all the tests + name, ast, fail, expect := test.name, test.ast, test.fail, test.expect + + if name == "" { + name = "" + } + + //if index != 3 { // hack to run a subset (useful for debugging) + //if test.name != "nil" { + // continue + //} + + t.Logf("\n\ntest #%d (%s) ----------------\n\n", index, name) + + //str := strings.NewReader(code) + //ast, err := LexParse(str) + //if err != nil { + // t.Errorf("test #%d: lex/parse failed with: %+v", index, err) + // continue + //} + // TODO: print out the AST's so that we can see the types + t.Logf("\n\ntest #%d: AST (before): %+v\n", index, ast) + + // skip interpolation in this test so that the node pointers + // aren't changed and so we can compare directly to expected + //astInterpolated, err := ast.Interpolate() // interpolate strings in ast + //if err != nil { + // t.Errorf("test #%d: interpolate failed with: %+v", index, err) + // continue + //} + //t.Logf("test #%d: astInterpolated: %+v", index, astInterpolated) + + // top-level, built-in, initial global scope + scope := &interfaces.Scope{ + Variables: map[string]interfaces.Expr{ + "purpleidea": &ExprStr{V: "hello world!"}, // james says hi + }, + } + // propagate the scope down through the AST... + if err := ast.SetScope(scope); err != nil { + t.Errorf("test #%d: set scope failed with: %+v", index, err) + continue + } + + // apply type unification + logf := func(format string, v ...interface{}) { + t.Logf(fmt.Sprintf("test #%d", index)+": unification: "+format, v...) + } + err := unification.Unify(ast, unification.SimpleInvariantSolverLogger(logf)) + + // TODO: print out the AST's so that we can see the types + t.Logf("\n\ntest #%d: AST (after): %+v\n", index, ast) + + if !fail && err != nil { + t.Errorf("test #%d: unification failed with: %+v", index, err) + continue + } + if fail && err == nil { + t.Errorf("test #%d: unification passed, expected fail", index) + continue + } + if fail { + continue + } + // continue the test + + if expect == nil { + continue + } + // TODO: do this in sorted order + var failed bool + for expr, exptyp := range expect { + typ, err := expr.Type() // lookup type + if err != nil { + t.Errorf("test #%d: type lookup of %+v failed with: %+v", index, expr, err) + failed = true + break + } + + if err := typ.Cmp(exptyp); err != nil { + t.Errorf("test #%d: type cmp failed with: %+v", index, err) + failed = true + break + } + } + if failed { + continue + } + + } +} diff --git a/lib/cli.go b/lib/cli.go index 2f13fdff..db7841a1 100644 --- a/lib/cli.go +++ b/lib/cli.go @@ -22,17 +22,26 @@ import ( "log" "os" "os/signal" + "sort" "syscall" "github.com/purpleidea/mgmt/bindata" - "github.com/purpleidea/mgmt/hcl" - "github.com/purpleidea/mgmt/puppet" - "github.com/purpleidea/mgmt/yamlgraph" - "github.com/purpleidea/mgmt/yamlgraph2" + "github.com/purpleidea/mgmt/gapi" + "github.com/spf13/afero" "github.com/urfave/cli" ) +// Fs is a simple wrapper to a memory backed file system to be used for +// standalone deploys. This is basically a pass-through so that we fulfill the +// same interface that the deploy mechanism uses. +type Fs struct { + *afero.Afero +} + +// URI returns the unique URI of this filesystem. It returns the root path. +func (obj *Fs) URI() string { return fmt.Sprintf("%s://"+"/", obj.Name()) } + // run is the main run target. func run(c *cli.Context) error { @@ -56,49 +65,36 @@ func run(c *cli.Context) error { obj.TmpPrefix = c.Bool("tmp-prefix") obj.AllowTmpPrefix = c.Bool("allow-tmp-prefix") - if _ = c.String("code"); c.IsSet("code") { - if obj.GAPI != nil { - return fmt.Errorf("can't combine code GAPI with existing GAPI") - } - // TODO: implement DSL GAPI - //obj.GAPI = &dsl.GAPI{ - // Code: &s, - //} - return fmt.Errorf("the Code GAPI is not implemented yet") // TODO: DSL + // add the versions GAPIs + names := []string{} + for name := range gapi.RegisteredGAPIs { + names = append(names, name) } - if y := c.String("yaml"); c.IsSet("yaml") { - if obj.GAPI != nil { - return fmt.Errorf("can't combine YAML GAPI with existing GAPI") + sort.Strings(names) // ensure deterministic order when parsing + + // create a memory backed temporary filesystem for storing runtime data + mmFs := afero.NewMemMapFs() + afs := &afero.Afero{Fs: mmFs} // wrap so that we're implementing ioutil + standaloneFs := &Fs{afs} + obj.DeployFs = standaloneFs + + for _, name := range names { + fn := gapi.RegisteredGAPIs[name] + deployObj, err := fn().Cli(c, standaloneFs) + if err != nil { + log.Printf("GAPI cli parse error: %v", err) + //return cli.NewExitError(err.Error(), 1) // TODO: ? + return cli.NewExitError("", 1) } - obj.GAPI = &yamlgraph.GAPI{ - File: &y, - } - } - if y := c.String("yaml2"); c.IsSet("yaml2") { - if obj.GAPI != nil { - return fmt.Errorf("can't combine YAMLv2 GAPI with existing GAPI") - } - obj.GAPI = &yamlgraph2.GAPI{ - File: &y, - } - } - if p := c.String("puppet"); c.IsSet("puppet") { - if obj.GAPI != nil { - return fmt.Errorf("can't combine puppet GAPI with existing GAPI") - } - obj.GAPI = &puppet.GAPI{ - PuppetParam: &p, - PuppetConf: c.String("puppet-conf"), - } - } - if h := c.String("hcl"); c.IsSet("hcl") { - if obj.GAPI != nil { - return fmt.Errorf("can't combine hcl GAPI with existing GAPI") - } - obj.GAPI = &hcl.GAPI{ - File: &h, + if deployObj == nil { // not used + continue } + if obj.Deploy != nil { // already set one + return fmt.Errorf("can't combine `%s` GAPI with existing GAPI", name) + } + obj.Deploy = deployObj } + obj.Remotes = c.StringSlice("remote") // FIXME: GAPI-ify somehow? obj.NoWatch = c.Bool("no-watch") @@ -168,9 +164,8 @@ func run(c *cli.Context) error { }() if err := obj.Run(); err != nil { - return err //return cli.NewExitError(err.Error(), 1) // TODO: ? - //return cli.NewExitError("", 1) // TODO: ? + return cli.NewExitError("", 1) } return nil } @@ -182,6 +177,208 @@ func CLI(program, version string, flags Flags) error { if program == "" || version == "" { return fmt.Errorf("program was not compiled correctly, see Makefile") } + + runFlags := []cli.Flag{ + // useful for testing multiple instances on same machine + cli.StringFlag{ + Name: "hostname", + Value: "", + Usage: "hostname to use", + }, + + cli.StringFlag{ + Name: "prefix", + Usage: "specify a path to the working prefix directory", + EnvVar: "MGMT_PREFIX", + }, + cli.BoolFlag{ + Name: "tmp-prefix", + Usage: "request a pseudo-random, temporary prefix to be used", + }, + cli.BoolFlag{ + Name: "allow-tmp-prefix", + Usage: "allow creation of a new temporary prefix if main prefix is unavailable", + }, + + cli.StringSliceFlag{ + Name: "remote", + Value: &cli.StringSlice{}, + Usage: "list of remote graph definitions to run", + }, + + cli.BoolFlag{ + Name: "no-watch", + Usage: "do not update graph under any switch events", + }, + cli.BoolFlag{ + Name: "no-config-watch", + Usage: "do not update graph on config switch events", + }, + cli.BoolFlag{ + Name: "no-stream-watch", + Usage: "do not update graph on stream switch events", + }, + + cli.BoolFlag{ + Name: "noop", + Usage: "globally force all resources into no-op mode", + }, + cli.IntFlag{ + Name: "sema", + Value: -1, + Usage: "globally add a semaphore to all resources with this lock count", + }, + cli.StringFlag{ + Name: "graphviz, g", + Value: "", + Usage: "output file for graphviz data", + }, + cli.StringFlag{ + Name: "graphviz-filter, gf", + Value: "", + Usage: "graphviz filter to use", + }, + cli.IntFlag{ + Name: "converged-timeout, t", + Value: -1, + Usage: "exit after approximately this many seconds in a converged state", + EnvVar: "MGMT_CONVERGED_TIMEOUT", + }, + cli.IntFlag{ + Name: "max-runtime", + Value: 0, + Usage: "exit after a maximum of approximately this many seconds", + EnvVar: "MGMT_MAX_RUNTIME", + }, + + // if empty, it will startup a new server + cli.StringSliceFlag{ + Name: "seeds, s", + Value: &cli.StringSlice{}, // empty slice + Usage: "default etc client endpoint", + EnvVar: "MGMT_SEEDS", + }, + // port 2379 and 4001 are common + cli.StringSliceFlag{ + Name: "client-urls", + Value: &cli.StringSlice{}, + Usage: "list of URLs to listen on for client traffic", + EnvVar: "MGMT_CLIENT_URLS", + }, + // port 2380 and 7001 are common + cli.StringSliceFlag{ + Name: "server-urls, peer-urls", + Value: &cli.StringSlice{}, + Usage: "list of URLs to listen on for server (peer) traffic", + EnvVar: "MGMT_SERVER_URLS", + }, + // port 2379 and 4001 are common + cli.StringSliceFlag{ + Name: "advertise-client-urls", + Value: &cli.StringSlice{}, + Usage: "list of URLs to listen on for client traffic", + EnvVar: "MGMT_ADVERTISE_CLIENT_URLS", + }, + // port 2380 and 7001 are common + cli.StringSliceFlag{ + Name: "advertise-server-urls, advertise-peer-urls", + Value: &cli.StringSlice{}, + Usage: "list of URLs to listen on for server (peer) traffic", + EnvVar: "MGMT_ADVERTISE_SERVER_URLS", + }, + cli.IntFlag{ + Name: "ideal-cluster-size", + Value: -1, + Usage: "ideal number of server peers in cluster; only read by initial server", + EnvVar: "MGMT_IDEAL_CLUSTER_SIZE", + }, + cli.BoolFlag{ + Name: "no-server", + Usage: "do not let other servers peer with me", + }, + + cli.IntFlag{ + Name: "cconns", + Value: 0, + Usage: "number of maximum concurrent remote ssh connections to run; 0 for unlimited", + EnvVar: "MGMT_CCONNS", + }, + cli.BoolFlag{ + Name: "allow-interactive", + Usage: "allow interactive prompting, such as for remote passwords", + }, + cli.StringFlag{ + Name: "ssh-priv-id-rsa", + Value: "~/.ssh/id_rsa", + Usage: "default path to ssh key file, set empty to never touch", + EnvVar: "MGMT_SSH_PRIV_ID_RSA", + }, + cli.BoolFlag{ + Name: "no-caching", + Usage: "don't allow remote caching of remote execution binary", + }, + cli.IntFlag{ + Name: "depth", + Hidden: true, // internal use only + Value: 0, + Usage: "specify depth in remote hierarchy", + }, + cli.BoolFlag{ + Name: "no-pgp", + Usage: "don't create pgp keys", + }, + cli.StringFlag{ + Name: "pgp-key-path", + Value: "", + Usage: "path for instance key pair", + }, + cli.StringFlag{ + Name: "pgp-identity", + Value: "", + Usage: "default identity used for generation", + }, + cli.BoolFlag{ + Name: "prometheus", + Usage: "start a prometheus instance", + }, + cli.StringFlag{ + Name: "prometheus-listen", + Value: "", + Usage: "specify prometheus instance binding", + }, + } + + subCommands := []cli.Command{} // build deploy sub commands + + names := []string{} + for name := range gapi.RegisteredGAPIs { + names = append(names, name) + } + sort.Strings(names) // ensure deterministic order when parsing + for _, x := range names { + name := x // create a copy in this scope + fn := gapi.RegisteredGAPIs[name] + gapiObj := fn() + flags := gapiObj.CliFlags() // []cli.Flag + + runFlags = append(runFlags, flags...) + + command := cli.Command{ + Name: name, + Usage: fmt.Sprintf("deploy using the `%s` frontend", name), + Action: func(c *cli.Context) error { + if err := deploy(c, name, gapiObj); err != nil { + log.Printf("Deploy: Error: %v", err) + //return cli.NewExitError(err.Error(), 1) // TODO: ? + return cli.NewExitError("", 1) + } + return nil + }, + Flags: flags, + } + subCommands = append(subCommands, command) + } + app := cli.NewApp() app.Name = program // App.name and App.version pass these values through app.Version = version @@ -222,77 +419,22 @@ func CLI(program, version string, flags Flags) error { Aliases: []string{"r"}, Usage: "run", Action: run, + Flags: runFlags, + }, + { + Name: "deploy", + Aliases: []string{"d"}, + Usage: "deploy", + Subcommands: subCommands, Flags: []cli.Flag{ - // useful for testing multiple instances on same machine - cli.StringFlag{ - Name: "hostname", - Value: "", - Usage: "hostname to use", - }, - - cli.StringFlag{ - Name: "prefix", - Usage: "specify a path to the working prefix directory", - EnvVar: "MGMT_PREFIX", - }, - cli.BoolFlag{ - Name: "tmp-prefix", - Usage: "request a pseudo-random, temporary prefix to be used", - }, - cli.BoolFlag{ - Name: "allow-tmp-prefix", - Usage: "allow creation of a new temporary prefix if main prefix is unavailable", - }, - - cli.StringFlag{ - Name: "code, c", - Value: "", - Usage: "code definition to run", - }, - cli.StringFlag{ - Name: "yaml", - Value: "", - Usage: "yaml graph definition to run", - }, - cli.StringFlag{ - Name: "yaml2", - Value: "", - Usage: "yaml graph definition to run (parser v2)", - }, - cli.StringFlag{ - Name: "hcl", - Value: "", - Usage: "hcl graph definition to run", - }, - cli.StringFlag{ - Name: "puppet, p", - Value: "", - Usage: "load graph from puppet, optionally takes a manifest or path to manifest file", - }, - cli.StringFlag{ - Name: "puppet-conf", - Value: "", - Usage: "the path to an alternate puppet.conf file", - }, cli.StringSliceFlag{ - Name: "remote", - Value: &cli.StringSlice{}, - Usage: "list of remote graph definitions to run", - }, - - cli.BoolFlag{ - Name: "no-watch", - Usage: "do not update graph under any switch events", - }, - cli.BoolFlag{ - Name: "no-config-watch", - Usage: "do not update graph on config switch events", - }, - cli.BoolFlag{ - Name: "no-stream-watch", - Usage: "do not update graph on stream switch events", + Name: "seeds, s", + Value: &cli.StringSlice{}, // empty slice + Usage: "default etc client endpoint", + EnvVar: "MGMT_SEEDS", }, + // common flags which all can use cli.BoolFlag{ Name: "noop", Usage: "globally force all resources into no-op mode", @@ -302,123 +444,14 @@ func CLI(program, version string, flags Flags) error { Value: -1, Usage: "globally add a semaphore to all resources with this lock count", }, - cli.StringFlag{ - Name: "graphviz, g", - Value: "", - Usage: "output file for graphviz data", - }, - cli.StringFlag{ - Name: "graphviz-filter, gf", - Value: "", - Usage: "graphviz filter to use", - }, - cli.IntFlag{ - Name: "converged-timeout, t", - Value: -1, - Usage: "exit after approximately this many seconds in a converged state", - EnvVar: "MGMT_CONVERGED_TIMEOUT", - }, - cli.IntFlag{ - Name: "max-runtime", - Value: 0, - Usage: "exit after a maximum of approximately this many seconds", - EnvVar: "MGMT_MAX_RUNTIME", - }, - // if empty, it will startup a new server - cli.StringSliceFlag{ - Name: "seeds, s", - Value: &cli.StringSlice{}, // empty slice - Usage: "default etc client endpoint", - EnvVar: "MGMT_SEEDS", - }, - // port 2379 and 4001 are common - cli.StringSliceFlag{ - Name: "client-urls", - Value: &cli.StringSlice{}, - Usage: "list of URLs to listen on for client traffic", - EnvVar: "MGMT_CLIENT_URLS", - }, - // port 2380 and 7001 are common - cli.StringSliceFlag{ - Name: "server-urls, peer-urls", - Value: &cli.StringSlice{}, - Usage: "list of URLs to listen on for server (peer) traffic", - EnvVar: "MGMT_SERVER_URLS", - }, - // port 2379 and 4001 are common - cli.StringSliceFlag{ - Name: "advertise-client-urls", - Value: &cli.StringSlice{}, - Usage: "list of URLs to listen on for client traffic", - EnvVar: "MGMT_ADVERTISE_CLIENT_URLS", - }, - // port 2380 and 7001 are common - cli.StringSliceFlag{ - Name: "advertise-server-urls, advertise-peer-urls", - Value: &cli.StringSlice{}, - Usage: "list of URLs to listen on for server (peer) traffic", - EnvVar: "MGMT_ADVERTISE_SERVER_URLS", - }, - cli.IntFlag{ - Name: "ideal-cluster-size", - Value: -1, - Usage: "ideal number of server peers in cluster; only read by initial server", - EnvVar: "MGMT_IDEAL_CLUSTER_SIZE", + cli.BoolFlag{ + Name: "no-git", + Usage: "don't look at git commit id for safe deploys", }, cli.BoolFlag{ - Name: "no-server", - Usage: "do not let other servers peer with me", - }, - - cli.IntFlag{ - Name: "cconns", - Value: 0, - Usage: "number of maximum concurrent remote ssh connections to run; 0 for unlimited", - EnvVar: "MGMT_CCONNS", - }, - cli.BoolFlag{ - Name: "allow-interactive", - Usage: "allow interactive prompting, such as for remote passwords", - }, - cli.StringFlag{ - Name: "ssh-priv-id-rsa", - Value: "~/.ssh/id_rsa", - Usage: "default path to ssh key file, set empty to never touch", - EnvVar: "MGMT_SSH_PRIV_ID_RSA", - }, - cli.BoolFlag{ - Name: "no-caching", - Usage: "don't allow remote caching of remote execution binary", - }, - cli.IntFlag{ - Name: "depth", - Hidden: true, // internal use only - Value: 0, - Usage: "specify depth in remote hierarchy", - }, - cli.BoolFlag{ - Name: "no-pgp", - Usage: "don't create pgp keys", - }, - cli.StringFlag{ - Name: "pgp-key-path", - Value: "", - Usage: "path for instance key pair", - }, - cli.StringFlag{ - Name: "pgp-identity", - Value: "", - Usage: "default identity used for generation", - }, - cli.BoolFlag{ - Name: "prometheus", - Usage: "start a prometheus instance", - }, - cli.StringFlag{ - Name: "prometheus-listen", - Value: "", - Usage: "specify prometheus instance binding", + Name: "force", + Usage: "force a new deploy, even if the safety chain would break", }, }, }, diff --git a/lib/deploy.go b/lib/deploy.go new file mode 100644 index 00000000..942ba395 --- /dev/null +++ b/lib/deploy.go @@ -0,0 +1,157 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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 lib + +import ( + "fmt" + "log" + "os" + + "github.com/purpleidea/mgmt/etcd" + etcdfs "github.com/purpleidea/mgmt/etcd/fs" + "github.com/purpleidea/mgmt/gapi" + + // these imports are so that GAPIs register themselves in init() + _ "github.com/purpleidea/mgmt/hcl" + _ "github.com/purpleidea/mgmt/lang" + _ "github.com/purpleidea/mgmt/puppet" + _ "github.com/purpleidea/mgmt/yamlgraph" + _ "github.com/purpleidea/mgmt/yamlgraph2" + + "github.com/google/uuid" + errwrap "github.com/pkg/errors" + "github.com/urfave/cli" + git "gopkg.in/src-d/go-git.v4" +) + +const ( + // MetadataPrefix is the etcd prefix where all our fs superblocks live. + MetadataPrefix = etcd.NS + "/fs" + // StoragePrefix is the etcd prefix where all our fs data lives. + StoragePrefix = etcd.NS + "/storage" +) + +// deploy is the cli target to manage deploys to our cluster. +func deploy(c *cli.Context, name string, gapiObj gapi.GAPI) error { + program, version := c.App.Name, c.App.Version + var flags Flags + if val, exists := c.App.Metadata["flags"]; exists { + if f, ok := val.(Flags); ok { + flags = f + } + } + hello(program, version, flags) // say hello! + + var hash, pHash string + if !c.GlobalBool("no-git") { + wd, err := os.Getwd() + if err != nil { + return errwrap.Wrapf(err, "could not get current working directory") + } + repo, err := git.PlainOpen(wd) + if err != nil { + return errwrap.Wrapf(err, "could not open git repo") + } + + head, err := repo.Head() + if err != nil { + return errwrap.Wrapf(err, "could not read git HEAD") + } + + hash = head.Hash().String() // current commit id + log.Printf("Deploy: Hash: %s", hash) + + lo := &git.LogOptions{ + From: head.Hash(), + } + commits, err := repo.Log(lo) + if err != nil { + return errwrap.Wrapf(err, "could not read git log") + } + if _, err := commits.Next(); err != nil { // skip over HEAD + return errwrap.Wrapf(err, "could not read HEAD in git log") // weird! + } + commit, err := commits.Next() + if err == nil { // errors are okay, we might be empty + pHash = commit.Hash.String() // previous commit id + } + log.Printf("Deploy: Previous deploy hash: %s", pHash) + if c.GlobalBool("force") { + pHash = "" // don't check this :( + } + if hash == "" { + return errwrap.Wrapf(err, "could not get git deploy hash") + } + } + + uniqueid := uuid.New() // panic's if it can't generate one :P + + etcdClient := &etcd.ClientEtcd{ + Seeds: c.GlobalStringSlice("seeds"), // endpoints + } + if err := etcdClient.Connect(); err != nil { + return errwrap.Wrapf(err, "client connection error") + } + defer etcdClient.Destroy() + + // TODO: this was all implemented super inefficiently, fix up for perf! + deploys, err := etcd.GetDeploys(etcdClient) // get previous deploys + if err != nil { + return errwrap.Wrapf(err, "error getting previous deploys") + } + // find the latest id + var max uint64 + for i := range deploys { + if i > max { + max = i + } + } + var id = max + 1 // next id + log.Printf("Deploy: Previous deploy id: %d", max) + + etcdFs := &etcdfs.Fs{ + Client: etcdClient.GetClient(), + // TODO: using a uuid is meant as a temporary measure, i hate them + Metadata: MetadataPrefix + fmt.Sprintf("/deploy/%d-%s", id, uniqueid), + DataPrefix: StoragePrefix, + } + + deploy, err := gapiObj.Cli(c, etcdFs) + if err != nil { + return errwrap.Wrapf(err, "cli parse error") + } + if deploy == nil { // not used + return fmt.Errorf("not enough information specified") + } + + // redundant + deploy.Noop = c.GlobalBool("noop") + deploy.Sema = c.GlobalInt("sema") + + str, err := deploy.ToB64() + if err != nil { + return errwrap.Wrapf(err, "encoding error") + } + + // this nominally checks the previous git hash matches our expectation + if err := etcd.AddDeploy(etcdClient, id, hash, pHash, &str); err != nil { + return errwrap.Wrapf(err, "could not create deploy id `%d`", id) + } + log.Printf("Deploy: Success, id: %d", id) + return nil +} diff --git a/lib/hello.go b/lib/hello.go new file mode 100644 index 00000000..87a0d364 --- /dev/null +++ b/lib/hello.go @@ -0,0 +1,48 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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 lib + +import ( + "log" + "os" + "time" + + "github.com/coreos/pkg/capnslog" +) + +func hello(program, version string, flags Flags) { + var start = time.Now().UnixNano() + + var logFlags int + if flags.Debug || true { // TODO: remove || true + logFlags = log.LstdFlags | log.Lshortfile + } + logFlags = (logFlags - log.Ldate) // remove the date for now + log.SetFlags(logFlags) + + // un-hijack from capnslog... + log.SetOutput(os.Stderr) + if flags.Verbose { + capnslog.SetFormatter(capnslog.NewLogFormatter(os.Stderr, "(etcd) ", logFlags)) + } else { + capnslog.SetFormatter(capnslog.NewNilFormatter()) + } + + log.Printf("This is: %s, version: %s", program, version) + log.Printf("Main: Start: %v", start) +} diff --git a/lib/main.go b/lib/main.go index f59dd016..d0a63449 100644 --- a/lib/main.go +++ b/lib/main.go @@ -23,6 +23,7 @@ import ( "log" "os" "path" + "sync" "time" "github.com/purpleidea/mgmt/converger" @@ -37,7 +38,6 @@ import ( "github.com/purpleidea/mgmt/util" etcdtypes "github.com/coreos/etcd/pkg/types" - "github.com/coreos/pkg/capnslog" multierr "github.com/hashicorp/go-multierror" errwrap "github.com/pkg/errors" ) @@ -62,8 +62,9 @@ type Main struct { TmpPrefix bool // request a pseudo-random, temporary prefix to be used AllowTmpPrefix bool // allow creation of a new temporary prefix if main prefix is unavailable - GAPI gapi.GAPI // graph API interface struct - Remotes []string // list of remote graph definitions to run + Deploy *gapi.Deploy // deploy object including GAPI for static deploys + DeployFs resources.Fs // used for static deploys + Remotes []string // list of remote graph definitions to run NoWatch bool // do not change graph under any circumstances NoConfigWatch bool // do not update graph due to config changes @@ -202,25 +203,7 @@ func (obj *Main) Exit(err error) { // Run is the main execution entrypoint to run mgmt. func (obj *Main) Run() error { - var start = time.Now().UnixNano() - - var flags int - if obj.Flags.Debug || true { // TODO: remove || true - flags = log.LstdFlags | log.Lshortfile - } - flags = (flags - log.Ldate) // remove the date for now - log.SetFlags(flags) - - // un-hijack from capnslog... - log.SetOutput(os.Stderr) - if obj.Flags.Verbose { - capnslog.SetFormatter(capnslog.NewLogFormatter(os.Stderr, "(etcd) ", flags)) - } else { - capnslog.SetFormatter(capnslog.NewNilFormatter()) - } - - log.Printf("This is: %s, version: %s", obj.Program, obj.Version) - log.Printf("Main: Start: %v", start) + hello(obj.Program, obj.Version, obj.Flags) // say hello! hostname, err := os.Hostname() // a sensible default // allow passing in the hostname, instead of using the system setting @@ -338,6 +321,8 @@ func (obj *Main) Run() error { nil, // stateFn gets added in by EmbdEtcd ) go converger.Loop(true) // main loop for converger, true to start paused + // TODO: will this shut things down prematurely? + converger.Start() // better start this anyways... // embedded etcd if len(obj.seeds) == 0 { @@ -370,13 +355,16 @@ func (obj *Main) Run() error { } // wait for etcd server to be ready before continuing... - select { - case <-EmbdEtcd.ServerReady(): - log.Printf("Main: Etcd: Server: Ready!") - // pass - case <-time.After(((etcd.MaxStartServerTimeout * etcd.MaxStartServerRetries) + 1) * time.Second): - obj.Exit(fmt.Errorf("Main: Etcd: Startup timeout")) - } + // XXX: this is wrong if we're not going to be a server! we'll block!!! + // select { + // case <-EmbdEtcd.ServerReady(): + // log.Printf("Main: Etcd: Server: Ready!") + // // pass + // case <-time.After(((etcd.MaxStartServerTimeout * etcd.MaxStartServerRetries) + 1) * time.Second): + // obj.Exit(fmt.Errorf("Main: Etcd: Startup timeout")) + // } + + time.Sleep(1 * time.Second) // XXX: temporary workaround convergerStateFn := func(b bool) error { // exit if we are using the converged timeout and we are the @@ -399,8 +387,15 @@ func (obj *Main) Run() error { // implementation of the World API (alternates can be substituted in) world := &etcd.World{ - Hostname: hostname, - EmbdEtcd: EmbdEtcd, + Hostname: hostname, + EmbdEtcd: EmbdEtcd, + MetadataPrefix: MetadataPrefix, + StoragePrefix: StoragePrefix, + StandaloneFs: obj.DeployFs, // used for static deploys + Debug: obj.Flags.Debug, + Logf: func(format string, v ...interface{}) { + log.Printf("world: etcd: "+format, v...) + }, } graph.Data = &resources.ResData{ @@ -410,35 +405,96 @@ func (obj *Main) Run() error { World: world, Prefix: pgraphPrefix, Debug: obj.Flags.Debug, - } - - var gapiChan chan gapi.Next // stream events contain some instructions! - if obj.GAPI != nil { - data := gapi.Data{ - Hostname: hostname, - World: world, - Noop: obj.Noop, - //NoWatch: obj.NoWatch, - NoConfigWatch: obj.NoConfigWatch, - NoStreamWatch: obj.NoStreamWatch, - } - if err := obj.GAPI.Init(data); err != nil { - obj.Exit(fmt.Errorf("Main: GAPI: Init failed: %v", err)) - } else { - // this must generate at least one event for it to work - gapiChan = obj.GAPI.Next() // stream of graph switch events! - } + Logf: func(format string, v ...interface{}) { + log.Printf("resources: "+format, v...) + }, } exitchan := make(chan struct{}) // exit on close + wg := &sync.WaitGroup{} // waitgroup for inner loop (go routine) + + deployChan := make(chan *gapi.Deploy) + var gapiImpl gapi.GAPI // active GAPI implementation + gapiImpl = nil // starts off missing + + var gapiChan chan gapi.Next // stream events contain some instructions! + gapiChan = nil // starts off blocked + wg.Add(1) go func() { + defer wg.Done() first := true // first loop or not + var mainDeploy *gapi.Deploy for { log.Println("Main: Waiting...") // The GAPI should always kick off an event on Next() at // startup when (and if) it indeed has a graph to share! fastPause := false select { + case deploy, ok := <-deployChan: + if !ok { // channel closed + if obj.Flags.Debug { + log.Printf("Main: Deploy: exited") + } + deployChan = nil // disable it + + if gapiImpl != nil { // currently running... + gapiChan = nil + if err := gapiImpl.Close(); err != nil { + err = errwrap.Wrapf(err, "the GAPI closed poorly") + log.Printf("Main: Deploy: GAPI: Final close failed: %+v", err) + } + } + return // this is the only place we exit + } + if deploy == nil { + log.Printf("Main: Deploy: Received empty deploy") + continue + } + mainDeploy = deploy // save this one + gapiObj := mainDeploy.GAPI + if gapiObj == nil { + log.Printf("Main: Deploy: Received empty GAPI") + continue + } + + if gapiImpl != nil { // currently running... + gapiChan = nil + if err := gapiImpl.Close(); err != nil { + err = errwrap.Wrapf(err, "the GAPI closed poorly") + log.Printf("Main: Deploy: GAPI: Close failed: %+v", err) + } + } + gapiImpl = gapiObj // copy it to active + + data := gapi.Data{ + Program: obj.Program, + Hostname: hostname, + World: world, + Noop: mainDeploy.Noop, + // FIXME: should the below flags come from the deploy struct? + //NoWatch: obj.NoWatch, + NoConfigWatch: obj.NoConfigWatch, + NoStreamWatch: obj.NoStreamWatch, + Debug: obj.Flags.Debug, + Logf: func(format string, v ...interface{}) { + log.Printf("gapi: "+format, v...) + }, + } + if obj.Flags.Debug { + log.Printf("Main: GAPI: Init...") + } + if err := gapiImpl.Init(data); err != nil { + log.Printf("Main: GAPI: Init failed: %+v", err) + // TODO: consider running previous GAPI? + } else { + if obj.Flags.Debug { + log.Printf("Main: GAPI: Next...") + } + // this must generate at least one event for it to work + gapiChan = gapiImpl.Next() // stream of graph switch events! + } + continue + case next, ok := <-gapiChan: if !ok { // channel closed if obj.Flags.Debug { @@ -449,6 +505,8 @@ func (obj *Main) Run() error { } // if we've been asked to exit... + // TODO: do we want to block exits and wait? + // TODO: we might want to wait for the next GAPI if next.Exit { obj.Exit(next.Err) // trigger exit continue // wait for exitchan @@ -464,15 +522,18 @@ func (obj *Main) Run() error { fastPause = next.Fast // should we pause fast? - case <-exitchan: - return + //case <-exitchan: // we only exit on deployChan close! + // return } - if obj.GAPI == nil { + if gapiImpl == nil { // TODO: can this ever happen anymore? log.Printf("Main: GAPI is empty!") continue } + if first { + converger.Pause() // it's already started! + } // we need the vertices to be paused to work on them, so // run graph vertex LOCK... if !first { // TODO: we can flatten this check out I think @@ -483,7 +544,7 @@ func (obj *Main) Run() error { } // make the graph from yaml, lib, puppet->yaml, or dsl! - newGraph, err := obj.GAPI.Graph() // generate graph! + newGraph, err := gapiImpl.Graph() // generate graph! if err != nil { log.Printf("Main: Error creating new graph: %v", err) // unpause! @@ -503,14 +564,14 @@ func (obj *Main) Run() error { for _, v := range newGraph.Vertices() { m := resources.VtoR(v).Meta() // apply the global noop parameter if requested - if obj.Noop { - m.Noop = obj.Noop + if mainDeploy.Noop { + m.Noop = mainDeploy.Noop } // append the semaphore to each resource - if obj.Sema > 0 { // NOTE: size == 0 would block + if mainDeploy.Sema > 0 { // NOTE: size == 0 would block // a semaphore with an empty id is valid - m.Sema = append(m.Sema, fmt.Sprintf(":%d", obj.Sema)) + m.Sema = append(m.Sema, fmt.Sprintf(":%d", mainDeploy.Sema)) } } @@ -660,22 +721,93 @@ func (obj *Main) Run() error { // obj.Exit(fmt.Errorf("Main: Remotes: Run timeout")) } - if obj.GAPI == nil { - converger.Start() // better start this for empty graphs + if obj.Deploy != nil { + deploy := obj.Deploy + // redundant + deploy.Noop = obj.Noop + deploy.Sema = obj.Sema + + select { + case deployChan <- deploy: + // send + case <-exitchan: + // pass + } + + // don't inline this, because when we close the deployChan it's + // the signal to tell the engine to actually shutdown... + go func() { + defer close(deployChan) // no more are coming ever! + select { // wait until we're ready to shutdown + case <-exitchan: + return + } + }() + } else { + // etcd based deploy + go func() { + defer close(deployChan) + startChan := make(chan struct{}) // start signal + close(startChan) // kick it off! + for { + select { + case <-startChan: // kick the loop once at start + startChan = nil // disable + + case err, ok := <-etcd.WatchDeploy(EmbdEtcd): + if !ok { + obj.Exit(nil) // regular shutdown + return + } + if err != nil { + // TODO: it broke, can we restart? + obj.Exit(fmt.Errorf("Main: Deploy: Watch error")) + return + } + + case <-exitchan: + return + } + + if obj.Flags.Debug { + log.Printf("Main: Deploy: Got activity") + } + str, err := etcd.GetDeploy(EmbdEtcd, 0) // 0 means get the latest one + if err != nil { + log.Printf("Main: Deploy: Error getting deploy %+v", err) + continue + } + if str == "" { // no available deploys exist yet + continue + } + + // decode the deploy (incl. GAPI) and send it! + deploy, err := gapi.NewDeployFromB64(str) + if err != nil { + log.Printf("Main: Deploy: Error decoding deploy %+v", err) + continue + } + + select { + case deployChan <- deploy: + // send + if obj.Flags.Debug { + log.Printf("Main: Deploy: Sending new GAPI") + } + + case <-exitchan: + return + } + } + }() } + log.Println("Main: Running...") reterr := <-obj.exit // wait for exit signal log.Println("Main: Destroy...") - if obj.GAPI != nil { - if err := obj.GAPI.Close(); err != nil { - err = errwrap.Wrapf(err, "the GAPI closed poorly") - reterr = multierr.Append(reterr, err) // list of errors - } - } - configWatcher.Close() // stop sending file changes to remotes if err := remotes.Exit(); err != nil { // tell all the remote connections to shutdown; waits! err = errwrap.Wrapf(err, "the Remote exited poorly") @@ -684,6 +816,7 @@ func (obj *Main) Run() error { // tell inner main loop to exit close(exitchan) + wg.Wait() graph.Exit() // tells all the children to exit, and waits for them to do so @@ -704,8 +837,9 @@ func (obj *Main) Run() error { if obj.Flags.Debug { log.Printf("Main: Graph: %v", graph) } - - // TODO: wait for each vertex to exit... + if reterr != nil { + log.Printf("Main: Error: %v", reterr) + } log.Println("Goodbye!") return reterr } diff --git a/misc/delta-cpu.sh b/misc/delta-cpu.sh index 30fd2435..32fc0049 100755 --- a/misc/delta-cpu.sh +++ b/misc/delta-cpu.sh @@ -7,7 +7,7 @@ count=1 # initial count factor=3 function output() { count=$1 # arg! -cat << EOF > ~/code/mgmt/examples/virt4.yaml +cat << EOF > ~/code/mgmt/examples/yaml/virt4.yaml --- graph: mygraph resources: diff --git a/misc/header.sh b/misc/header.sh new file mode 100755 index 00000000..7812c45c --- /dev/null +++ b/misc/header.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +if [ "$1" = "" ] || [ "$1" = "--help" ]; then + echo "usage: append standard header to file" + echo "./$(basename "$0") | --help" + exit 1 +fi + +ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd )" # dir! +FILE="${ROOT}/main.go" # file headers should match main.go +COUNT=0 +while IFS='' read -r line; do # find what header should look like + echo "$line" | grep -q '^//' || break + COUNT=`expr $COUNT + 1` +done < "$FILE" +#cd "${ROOT}" + +COUNT=`expr $COUNT + 1` # add one extra newline + +# detect if header is correct before blasting another one in +if diff -q <( head -n $COUNT "$1" ) <( head -n $COUNT "$FILE" ) &>/dev/null; then + exit 0 +fi + +tmpfile=`mktemp` # get a temp file +# the output of the main.go header, dumped onto the file +head -n $COUNT "$FILE" | cat - "$1" > "$tmpfile" && mv "$tmpfile" "$1" diff --git a/misc/make-deps.sh b/misc/make-deps.sh index b97f52ba..995888fd 100755 --- a/misc/make-deps.sh +++ b/misc/make-deps.sh @@ -70,7 +70,10 @@ if go version | grep 'go1\.[012345]\.'; then exit 1 fi -go get -d ./... # get all the go dependencies +echo "running 'go get -v -d ./...' from `pwd`" +go get -v -t -d ./... # get all the go dependencies +echo "done running 'go get -v -t -d ./...'" + [ -e "$GOBIN/mgmt" ] && rm -f "$GOBIN/mgmt" # the `go get` version has no -X # vet is built-in in go 1.6 - we check for go vet command go vet 1> /dev/null 2>&1 @@ -78,6 +81,8 @@ ret=$? if [[ $ret != 0 ]]; then go get golang.org/x/tools/cmd/vet # add in `go vet` for travis fi +go get github.com/blynn/nex # for lexing +go get golang.org/x/tools/cmd/goyacc # formerly `go tool yacc` go get golang.org/x/tools/cmd/stringer # for automatic stringer-ing go get github.com/jteeuwen/go-bindata/go-bindata # for compiling in non golang files go get github.com/golang/lint/golint # for `golint`-ing diff --git a/pgraph/pgraph.go b/pgraph/pgraph.go index e7626f5f..981a0adf 100644 --- a/pgraph/pgraph.go +++ b/pgraph/pgraph.go @@ -20,7 +20,9 @@ package pgraph import ( "fmt" + "log" "sort" + "strings" errwrap "github.com/pkg/errors" ) @@ -181,6 +183,19 @@ func (g *Graph) Adjacency() map[Vertex]map[Vertex]Edge { return g.adjacency } +// FindEdge returns the edge from v1 -> v2 if it exists. Otherwise nil. +func (g *Graph) FindEdge(v1, v2 Vertex) Edge { + x, exists := g.adjacency[v1] + if !exists { + return nil // not found + } + edge, exists := x[v2] + if !exists { + return nil + } + return edge +} + // Vertices returns a randomly sorted slice of all vertices in the graph. // The order is random, because the map implementation is intentionally so! func (g *Graph) Vertices() []Vertex { @@ -191,6 +206,18 @@ func (g *Graph) Vertices() []Vertex { return vertices } +// Edges returns a randomly sorted slice of all edges in the graph. +// The order is random, because the map implementation is intentionally so! +func (g *Graph) Edges() []Edge { + var edges []Edge + for vertex := range g.adjacency { + for _, edge := range g.adjacency[vertex] { + edges = append(edges, edge) + } + } + return edges +} + // VerticesChan returns a channel of all vertices in the graph. func (g *Graph) VerticesChan() chan Vertex { ch := make(chan Vertex) @@ -223,9 +250,40 @@ func (g *Graph) VerticesSorted() []Vertex { // String makes the graph pretty print. func (g *Graph) String() string { + if g == nil { // don't panic if we're printing a nil graph + return fmt.Sprintf("%v", nil) // prints a + } return fmt.Sprintf("Vertices(%d), Edges(%d)", g.NumVertices(), g.NumEdges()) } +// Sprint prints a full graph in textual form out to a string. To log this you +// might want to use Logf, which will keep everything aligned with whatever your +// logging prefix is. +func (g *Graph) Sprint() string { + var str string + for v := range g.Adjacency() { + str += fmt.Sprintf("Vertex: %s\n", v) + } + for v1 := range g.Adjacency() { + for v2, e := range g.Adjacency()[v1] { + str += fmt.Sprintf("Edge: %s -> %s # %s\n", v1, v2, e) + } + } + return strings.TrimSuffix(str, "\n") // trim off trailing \n if it exists +} + +// Logf logs a printed representation of the graph with the printf-format string +// prefix of your choice. This is helpful to ensure each line of logged output +// has the prefix you asked for. +func (g *Graph) Logf(format string, v ...interface{}) { + w := []interface{}{} + w = append(w, v...) + for _, x := range strings.Split(g.Sprint(), "\n") { + a := append(w, x) // x must be the last arg + log.Printf(format+"%s", a...) + } +} + // IncomingGraphVertices returns an array (slice) of all directed vertices to // vertex v (??? -> v). OKTimestamp should probably use this. func (g *Graph) IncomingGraphVertices(v Vertex) []Vertex { @@ -502,6 +560,12 @@ func (g *Graph) VertexMatchFn(fn func(Vertex) (bool, error)) (Vertex, error) { // equivalent vertices, and edges. // FIXME: add more test cases func (g *Graph) GraphCmp(graph *Graph, vertexCmpFn func(Vertex, Vertex) (bool, error), edgeCmpFn func(Edge, Edge) (bool, error)) error { + if graph == nil || g == nil { + if graph != g { + return fmt.Errorf("one graph is nil") + } + return nil + } n1, n2 := g.NumVertices(), graph.NumVertices() if n1 != n2 { return fmt.Errorf("base graph has %d vertices, while input graph has %d", n1, n2) diff --git a/pgraph/pgraph_test.go b/pgraph/pgraph_test.go index 4af300ac..04bbde3f 100644 --- a/pgraph/pgraph_test.go +++ b/pgraph/pgraph_test.go @@ -639,6 +639,19 @@ func TestGraphCmp1(t *testing.T) { } } +// FIXME: i think we should allow equivalent elements in the graph to compare... +// FIXME: currently this fails :( +//func TestGraphCmp2(t *testing.T) { +// g1 := &Graph{} +// g2 := &Graph{} +// g1.AddVertex(NV("v1"), NV("v1")) +// g2.AddVertex(NV("v1"), NV("v1")) +// +// if err := g1.GraphCmp(g2, strVertexCmpFn, strEdgeCmpFn); err != nil { +// t.Errorf("should have no error during GraphCmp, but got: %v", err) +// } +//} + func TestSort0(t *testing.T) { vs := []Vertex{} s := Sort(vs) diff --git a/puppet/gapi.go b/puppet/gapi.go index 31fd6c2b..87793c94 100644 --- a/puppet/gapi.go +++ b/puppet/gapi.go @@ -19,32 +19,139 @@ package puppet import ( "fmt" + "io/ioutil" "log" + "os" + "strings" "sync" "time" "github.com/purpleidea/mgmt/gapi" "github.com/purpleidea/mgmt/pgraph" + "github.com/purpleidea/mgmt/resources" + "github.com/purpleidea/mgmt/util" + + errwrap "github.com/pkg/errors" + "github.com/urfave/cli" ) +const ( + // Name is the name of this frontend. + Name = "puppet" + // PuppetFile is the entry point filename that we use. It is arbitrary. + PuppetFile = "/file.pp" + // PuppetConf is the entry point config filename that we use. + PuppetConf = "/puppet.conf" + // PuppetSite is the entry point folder that we use. + PuppetSite = "/puppet/" +) + +func init() { + gapi.Register(Name, func() gapi.GAPI { return &GAPI{} }) // register +} + // GAPI implements the main puppet GAPI interface. type GAPI struct { - PuppetParam *string // puppet mode to run; nil if undefined - PuppetConf string // the path to an alternate puppet.conf file + InputURI string + Mode string // agent, file, string, dir - data gapi.Data - initialized bool - closeChan chan struct{} - wg sync.WaitGroup // sync group for tunnel go routines + puppetFile string + puppetString string + puppetDir string + puppetConf string // the path to an alternate puppet.conf file + data gapi.Data + initialized bool + closeChan chan struct{} + wg sync.WaitGroup // sync group for tunnel go routines } -// NewGAPI creates a new puppet GAPI struct and calls Init(). -func NewGAPI(data gapi.Data, puppetParam *string, puppetConf string) (*GAPI, error) { - obj := &GAPI{ - PuppetParam: puppetParam, - PuppetConf: puppetConf, +// Cli takes a cli.Context, and returns our GAPI if activated. All arguments +// should take the prefix of the registered name. On activation, if there are +// any validation problems, you should return an error. If this was not +// activated, then you should return a nil GAPI and a nil error. +func (obj *GAPI) Cli(c *cli.Context, fs resources.Fs) (*gapi.Deploy, error) { + if s := c.String(Name); c.IsSet(Name) { + if s == "" { + return nil, fmt.Errorf("%s input is empty", Name) + } + + isDir := func(p string) (bool, error) { + if !strings.HasPrefix(p, "/") { + return false, nil + } + if !strings.HasSuffix(s, "/") { + return false, nil + } + fi, err := os.Stat(p) + if err != nil { + return false, err + } + return fi.IsDir(), nil + } + + var mode string + if s == "agent" { + mode = "agent" + + } else if strings.HasSuffix(s, ".pp") { + mode = "file" + if err := gapi.CopyFileToFs(fs, s, PuppetFile); err != nil { + return nil, errwrap.Wrapf(err, "can't copy code from `%s` to `%s`", s, PuppetFile) + } + + } else if exists, err := isDir(s); err != nil { + return nil, errwrap.Wrapf(err, "can't read dir `%s`", s) + + } else if err == nil && exists { // from the isDir result... + // we have a whole directory of files to run + mode = "dir" + // TODO: this code path is untested! test and then rm this notice + if err := gapi.CopyDirToFs(fs, s, PuppetSite); err != nil { + return nil, errwrap.Wrapf(err, "can't copy code to `%s`", PuppetSite) + } + + } else { + mode = "string" + if err := gapi.CopyStringToFs(fs, s, PuppetFile); err != nil { + return nil, errwrap.Wrapf(err, "can't copy code to `%s`", PuppetFile) + } + } + + // TODO: do we want to include this if we have mode == "dir" ? + if pc := c.String("puppet-conf"); c.IsSet("puppet-conf") { + if err := gapi.CopyFileToFs(fs, pc, PuppetConf); err != nil { + return nil, errwrap.Wrapf(err, "can't copy puppet conf from `%s`") + } + } + + return &gapi.Deploy{ + Name: Name, + Noop: c.GlobalBool("noop"), + Sema: c.GlobalInt("sema"), + GAPI: &GAPI{ + InputURI: fs.URI(), + Mode: mode, + // TODO: add properties here... + }, + }, nil + } + return nil, nil // we weren't activated! +} + +// CliFlags returns a list of flags used by this deploy subcommand. +func (obj *GAPI) CliFlags() []cli.Flag { + return []cli.Flag{ + cli.StringFlag{ + Name: "puppet, p", + Value: "", + Usage: "load graph from puppet, optionally takes a manifest or path to manifest file", + }, + cli.StringFlag{ + Name: "puppet-conf", + Value: "", + Usage: "the path to an alternate puppet.conf file", + }, } - return obj, obj.Init(data) } // Init initializes the puppet GAPI struct. @@ -52,10 +159,83 @@ func (obj *GAPI) Init(data gapi.Data) error { if obj.initialized { return fmt.Errorf("already initialized") } - if obj.PuppetParam == nil { - return fmt.Errorf("the PuppetParam param must be specified") + if obj.InputURI == "" { + return fmt.Errorf("the InputURI param must be specified") + } + switch obj.Mode { + case "agent", "file", "string", "dir": + // pass + default: + return fmt.Errorf("the Mode param is invalid") } obj.data = data // store for later + + fs, err := obj.data.World.Fs(obj.InputURI) // open the remote file system + if err != nil { + return errwrap.Wrapf(err, "can't load data from file system `%s`", obj.InputURI) + } + + if obj.Mode == "file" { + b, err := fs.ReadFile(PuppetFile) // read the single file out of it + if err != nil { + return errwrap.Wrapf(err, "can't read code from file `%s`", PuppetFile) + } + + // store the puppet file on disk for other binaries to see and use + prefix := fmt.Sprintf("%s-%s-%s", data.Program, data.Hostname, strings.Replace(PuppetFile, "/", "", -1)) + tmpfile, err := ioutil.TempFile("", prefix) + if err != nil { + return errwrap.Wrapf(err, "can't create temp file") + } + obj.puppetFile = tmpfile.Name() // path to temp file + defer tmpfile.Close() + if _, err := tmpfile.Write(b); err != nil { + return errwrap.Wrapf(err, "can't write file") + } + + } else if obj.Mode == "string" { + b, err := fs.ReadFile(PuppetFile) // read the single code string out of it + if err != nil { + return errwrap.Wrapf(err, "can't read code from file `%s`", PuppetFile) + } + obj.puppetString = string(b) + + } else if obj.Mode == "dir" { + // store the puppet files on disk for other binaries to see and use + prefix := fmt.Sprintf("%s-%s-%s", data.Program, data.Hostname, strings.Replace(PuppetSite, "/", "", -1)) + tmpdirName, err := ioutil.TempDir("", prefix) + if err != nil { + return errwrap.Wrapf(err, "can't create temp dir") + } + if tmpdirName == "" || tmpdirName == "/" { + return fmt.Errorf("bad tmpdir created") + } + obj.puppetDir = tmpdirName // path to temp dir + // TODO: this code path is untested! test and then rm this notice + if err := util.CopyFsToDisk(fs, PuppetSite, tmpdirName, false); err != nil { + return errwrap.Wrapf(err, "can't copy dir") + } + } + + if fi, err := fs.Stat(PuppetConf); err == nil && !fi.IsDir() { // if exists? + b, err := fs.ReadFile(PuppetConf) // read the single file out of it + if err != nil { + return errwrap.Wrapf(err, "can't read config from file `%s`", PuppetConf) + } + + // store the puppet conf on disk for other binaries to see and use + prefix := fmt.Sprintf("%s-%s-%s", data.Program, data.Hostname, strings.Replace(PuppetConf, "/", "", -1)) + tmpfile, err := ioutil.TempFile("", prefix) + if err != nil { + return errwrap.Wrapf(err, "can't create temp file") + } + obj.puppetConf = tmpfile.Name() // path to temp file + defer tmpfile.Close() + if _, err := tmpfile.Write(b); err != nil { + return errwrap.Wrapf(err, "can't write file") + } + } + obj.closeChan = make(chan struct{}) obj.initialized = true return nil @@ -64,9 +244,9 @@ func (obj *GAPI) Init(data gapi.Data) error { // Graph returns a current Graph. func (obj *GAPI) Graph() (*pgraph.Graph, error) { if !obj.initialized { - return nil, fmt.Errorf("the puppet GAPI is not initialized") + return nil, fmt.Errorf("%s: GAPI is not initialized", Name) } - config := ParseConfigFromPuppet(*obj.PuppetParam, obj.PuppetConf) + config := obj.ParseConfigFromPuppet() if config == nil { return nil, fmt.Errorf("function ParseConfigFromPuppet returned nil") } @@ -77,7 +257,7 @@ func (obj *GAPI) Graph() (*pgraph.Graph, error) { // Next returns nil errors every time there could be a new graph. func (obj *GAPI) Next() chan gapi.Next { puppetChan := func() <-chan time.Time { // helper function - return time.Tick(time.Duration(RefreshInterval(obj.PuppetConf)) * time.Second) + return time.Tick(time.Duration(obj.refreshInterval()) * time.Second) } ch := make(chan gapi.Next) obj.wg.Add(1) @@ -86,7 +266,7 @@ func (obj *GAPI) Next() chan gapi.Next { defer close(ch) // this will run before the obj.wg.Done() if !obj.initialized { next := gapi.Next{ - Err: fmt.Errorf("the puppet GAPI is not initialized"), + Err: fmt.Errorf("%s: GAPI is not initialized", Name), Exit: true, // exit, b/c programming error? } ch <- next @@ -117,7 +297,7 @@ func (obj *GAPI) Next() chan gapi.Next { return } - log.Printf("Puppet: Generating new graph...") + log.Printf("%s: Generating new graph...", Name) if obj.data.NoStreamWatch { pChan = nil } else { @@ -141,8 +321,21 @@ func (obj *GAPI) Next() chan gapi.Next { // Close shuts down the Puppet GAPI. func (obj *GAPI) Close() error { if !obj.initialized { - return fmt.Errorf("the puppet GAPI is not initialized") + return fmt.Errorf("%s: GAPI is not initialized", Name) } + + if obj.puppetFile != "" { + os.Remove(obj.puppetFile) // clean up, don't bother with error + } + // make this as safe as possible, check we're removing a tempdir too! + if obj.puppetDir != "" && obj.puppetDir != "/" && strings.HasPrefix(obj.puppetDir, os.TempDir()) { + os.RemoveAll(obj.puppetDir) + } + obj.puppetString = "" // free! + if obj.puppetConf != "" { + os.Remove(obj.puppetConf) + } + close(obj.closeChan) obj.wg.Wait() obj.initialized = false // closed = true diff --git a/puppet/puppet.go b/puppet/puppet.go index 0f31f4c6..a1d580c7 100644 --- a/puppet/puppet.go +++ b/puppet/puppet.go @@ -20,6 +20,7 @@ package puppet import ( "bufio" + "fmt" "io" "log" "os/exec" @@ -38,22 +39,22 @@ const ( func runPuppetCommand(cmd *exec.Cmd) ([]byte, error) { if Debug { - log.Printf("Puppet: running command: %v", cmd) + log.Printf("%s: running command: %v", Name, cmd) } stdout, err := cmd.StdoutPipe() if err != nil { - log.Printf("Puppet: Error opening pipe to puppet command: %v", err) + log.Printf("%s: Error opening pipe to puppet command: %v", Name, err) return nil, err } stderr, err := cmd.StderrPipe() if err != nil { - log.Printf("Puppet: Error opening error pipe to puppet command: %v", err) + log.Printf("%s: Error opening error pipe to puppet command: %v", Name, err) return nil, err } if err := cmd.Start(); err != nil { - log.Printf("Puppet: Error starting puppet command: %v", err) + log.Printf("%s: Error starting puppet command: %v", Name, err) return nil, err } @@ -65,7 +66,7 @@ func runPuppetCommand(cmd *exec.Cmd) ([]byte, error) { var count int count, err = stdout.Read(data) if err != nil && err != io.EOF { - log.Printf("Puppet: Error reading YAML data from puppet: %v", err) + log.Printf("%s: Error reading YAML data from puppet: %v", Name, err) return nil, err } // Slicing down to the number of actual bytes is important, the YAML parser @@ -73,44 +74,50 @@ func runPuppetCommand(cmd *exec.Cmd) ([]byte, error) { result = append(result, data[0:count]...) } if Debug { - log.Printf("Puppet: read %v bytes of data from puppet", len(result)) + log.Printf("%s: read %d bytes of data from puppet", Name, len(result)) } for scanner := bufio.NewScanner(stderr); scanner.Scan(); { - log.Printf("Puppet: (output) %v", scanner.Text()) + log.Printf("%s: (output) %v", Name, scanner.Text()) } if err := cmd.Wait(); err != nil { - log.Printf("Puppet: Error: puppet command did not complete: %v", err) + log.Printf("%s: Error: puppet command did not complete: %v", Name, err) return nil, err } return result, nil } -// ParseConfigFromPuppet takes a special puppet param string and config and -// returns the graph configuration structure. -func ParseConfigFromPuppet(puppetParam, puppetConf string) *yamlgraph.GraphConfig { +// ParseConfigFromPuppet returns the graph configuration structure from the mode +// and input values, including possibly some file and directory paths. +func (obj *GAPI) ParseConfigFromPuppet() *yamlgraph.GraphConfig { var args []string - if puppetParam == "agent" { + switch obj.Mode { + case "agent": args = []string{"mgmtgraph", "print"} - } else if strings.HasSuffix(puppetParam, ".pp") { - args = []string{"mgmtgraph", "print", "--manifest", puppetParam} - } else { - args = []string{"mgmtgraph", "print", "--code", puppetParam} + case "file": + args = []string{"mgmtgraph", "print", "--manifest", obj.puppetFile} + case "string": + args = []string{"mgmtgraph", "print", "--code", obj.puppetString} + case "dir": + // TODO: run the code from the obj.puppetDir directory path + return nil // XXX: not implemented + default: + panic(fmt.Sprintf("%s: unhandled case: %s", Name, obj.Mode)) } - if puppetConf != "" { - args = append(args, "--config="+puppetConf) + if obj.puppetConf != "" { + args = append(args, "--config="+obj.puppetConf) } cmd := exec.Command("puppet", args...) - log.Println("Puppet: launching translator") + log.Printf("%s: launching translator", Name) var config yamlgraph.GraphConfig if data, err := runPuppetCommand(cmd); err != nil { return nil } else if err := config.Parse(data); err != nil { - log.Printf("Puppet: Error: Could not parse YAML output with Parse: %v", err) + log.Printf("%s: Error: Could not parse YAML output with Parse: %v", Name, err) return nil } @@ -118,28 +125,28 @@ func ParseConfigFromPuppet(puppetParam, puppetConf string) *yamlgraph.GraphConfi } // RefreshInterval returns the graph refresh interval from the puppet configuration. -func RefreshInterval(puppetConf string) int { +func (obj *GAPI) refreshInterval() int { if Debug { - log.Printf("Puppet: determining graph refresh interval") + log.Printf("%s: determining graph refresh interval", Name) } var cmd *exec.Cmd - if puppetConf != "" { - cmd = exec.Command("puppet", "config", "print", "runinterval", "--config", puppetConf) + if obj.puppetConf != "" { + cmd = exec.Command("puppet", "config", "print", "runinterval", "--config", obj.puppetConf) } else { cmd = exec.Command("puppet", "config", "print", "runinterval") } - log.Println("Puppet: inspecting runinterval configuration") + log.Printf("%s: inspecting runinterval configuration", Name) interval := 1800 data, err := runPuppetCommand(cmd) if err != nil { - log.Printf("Puppet: could not determine configured run interval (%v), using default of %v", err, interval) + log.Printf("%s: could not determine configured run interval (%v), using default of %v", Name, err, interval) return interval } result, err := strconv.ParseInt(strings.TrimSpace(string(data)), 10, 0) if err != nil { - log.Printf("Puppet: error reading numeric runinterval value (%v), using default of %v", err, interval) + log.Printf("%s: error reading numeric runinterval value (%v), using default of %v", Name, err, interval) return interval } diff --git a/remote/remote.go b/remote/remote.go index 130a28c5..b1c48a29 100644 --- a/remote/remote.go +++ b/remote/remote.go @@ -743,8 +743,13 @@ func NewRemotes(clientURLs, remoteURLs []string, noop bool, remotes []string, fi // NewSSH is a helper function that does the initial parsing into an SSH obj. // It takes as input the path to a graph definition file. func (obj *Remotes) NewSSH(file string) (*SSH, error) { - // first do the parsing... - config := yamlgraph.ParseConfigFromFile(file) // FIXME: GAPI-ify somehow? + // first read in the file and do the parsing... + data, err := ioutil.ReadFile(file) + if err != nil { + return nil, errwrap.Wrapf(err, "Remote: Error reading file: %s", file) + } + + config := yamlgraph.ParseConfigFromFile(data) // FIXME: GAPI-ify somehow? if config == nil { return nil, fmt.Errorf("Remote: Error parsing remote graph: %s", file) } diff --git a/resources/file.go b/resources/file.go index d96557f9..440c119d 100644 --- a/resources/file.go +++ b/resources/file.go @@ -44,20 +44,21 @@ func init() { // FileRes is a file and directory resource. type FileRes struct { - BaseRes `yaml:",inline"` - Path string `yaml:"path"` // path variable (should default to name) - Dirname string `yaml:"dirname"` - Basename string `yaml:"basename"` - Content *string `yaml:"content"` // nil to mark as undefined - Source string `yaml:"source"` // file path for source content - State string `yaml:"state"` // state: exists/present?, absent, (undefined?) - Owner string `yaml:"owner"` - Group string `yaml:"group"` - Mode string `yaml:"mode"` - Recurse bool `yaml:"recurse"` - Force bool `yaml:"force"` - path string // computed path - isDir bool // computed isDir + BaseRes `yaml:",inline"` + Path string `yaml:"path"` // path variable (should default to name) + Dirname string `yaml:"dirname"` + Basename string `yaml:"basename"` + Content *string `yaml:"content"` // nil to mark as undefined + Source string `yaml:"source"` // file path for source content + State string `yaml:"state"` // state: exists/present?, absent, (undefined?) + Owner string `yaml:"owner"` + Group string `yaml:"group"` + Mode string `yaml:"mode"` + Recurse bool `yaml:"recurse"` + Force bool `yaml:"force"` + + path string // computed path + isDir bool // computed isDir sha256sum string recWatcher *recwatch.RecWatcher } diff --git a/resources/interfaces.go b/resources/interfaces.go new file mode 100644 index 00000000..062b56a0 --- /dev/null +++ b/resources/interfaces.go @@ -0,0 +1,89 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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 resources + +import ( + "os" + + "github.com/purpleidea/mgmt/etcd/scheduler" + + "github.com/spf13/afero" +) + +// World is an interface to the rest of the different graph state. It allows +// the GAPI to store state and exchange information throughout the cluster. It +// is the interface each machine uses to communicate with the rest of the world. +type World interface { // TODO: is there a better name for this interface? + ResWatch() chan error + ResExport([]Res) error + // FIXME: should this method take a "filter" data struct instead of many args? + ResCollect(hostnameFilter, kindFilter []string) ([]Res, error) + + StrWatch(namespace string) chan error + StrIsNotExist(error) bool + StrGet(namespace string) (string, error) + StrSet(namespace, value string) error + StrDel(namespace string) error + + // XXX: add the exchange primitives in here directly? + StrMapWatch(namespace string) chan error + StrMapGet(namespace string) (map[string]string, error) + StrMapSet(namespace, value string) error + StrMapDel(namespace string) error + + Scheduler(namespace string, opts ...scheduler.Option) (*scheduler.Result, error) + + Fs(uri string) (Fs, error) +} + +// from the ioutil package: +// NopCloser(r io.Reader) io.ReadCloser // not implemented here +// ReadAll(r io.Reader) ([]byte, error) +// ReadDir(dirname string) ([]os.FileInfo, error) +// ReadFile(filename string) ([]byte, error) +// TempDir(dir, prefix string) (name string, err error) +// TempFile(dir, prefix string) (f *os.File, err error) // slightly different here +// WriteFile(filename string, data []byte, perm os.FileMode) error + +// Fs is an interface that represents this file system API that we support. +// TODO: this should be in the gapi package or elsewhere. +type Fs interface { + //fmt.Stringer // TODO: add this method? + afero.Fs // TODO: why doesn't this interface exist in the os pkg? + URI() string // returns the URI for this file system + + //DirExists(path string) (bool, error) + //Exists(path string) (bool, error) + //FileContainsAnyBytes(filename string, subslices [][]byte) (bool, error) + //FileContainsBytes(filename string, subslice []byte) (bool, error) + //FullBaseFsPath(basePathFs *BasePathFs, relativePath string) string + //GetTempDir(subPath string) string + //IsDir(path string) (bool, error) + //IsEmpty(path string) (bool, error) + //NeuterAccents(s string) string + //ReadAll(r io.Reader) ([]byte, error) // not needed + ReadDir(dirname string) ([]os.FileInfo, error) + ReadFile(filename string) ([]byte, error) + //SafeWriteReader(path string, r io.Reader) (err error) + TempDir(dir, prefix string) (name string, err error) + TempFile(dir, prefix string) (f afero.File, err error) // slightly different from upstream + //UnicodeSanitize(s string) string + //Walk(root string, walkFn filepath.WalkFunc) error + WriteFile(filename string, data []byte, perm os.FileMode) error + //WriteReader(path string, r io.Reader) (err error) +} diff --git a/resources/kv.go b/resources/kv.go index 3c685202..8c5663eb 100644 --- a/resources/kv.go +++ b/resources/kv.go @@ -47,7 +47,8 @@ const ( // The one exception is that when this resource receives a refresh signal, then // it will set the value to be the exact one if they are not identical already. type KVRes struct { - BaseRes `yaml:",inline"` + BaseRes `yaml:",inline"` + // XXX: shouldn't the name be the key? Key string `yaml:"key"` // key to set Value *string `yaml:"value"` // value to set (nil to delete) SkipLessThan bool `yaml:"skiplessthan"` // skip updates as long as stored value is greater diff --git a/resources/mgraph.go b/resources/mgraph.go index 215319af..bee54204 100644 --- a/resources/mgraph.go +++ b/resources/mgraph.go @@ -150,7 +150,7 @@ func (obj *MGraph) Start(first bool) { // start or continue unpause = false // doesn't need unpausing on first start obj.wg.Add(1) // must pass in value to avoid races... - // see: https://ttboj.wordpress.com/2015/07/27/golang-parallelism-issues-causing-too-many-open-files-error/ + // see: https://purpleidea.com/blog/2015/07/27/golang-parallelism-issues-causing-too-many-open-files-error/ go func(vv pgraph.Vertex) { defer obj.wg.Done() // unset Worker() running flag just before exit diff --git a/resources/noop.go b/resources/noop.go index c6f76666..02db5e38 100644 --- a/resources/noop.go +++ b/resources/noop.go @@ -29,7 +29,7 @@ func init() { // NoopRes is a no-op resource that does nothing. type NoopRes struct { BaseRes `yaml:",inline"` - Comment string `yaml:"comment"` // extra field for example purposes + Comment string `lang:"comment" yaml:"comment"` // extra field for example purposes } // Default returns some sensible defaults for this resource. diff --git a/resources/nspawn.go b/resources/nspawn.go index 1aa0d0a1..e5f96b30 100644 --- a/resources/nspawn.go +++ b/resources/nspawn.go @@ -157,7 +157,7 @@ func (obj *NspawnRes) Init() error { // Watch for state changes and sends a message to the bus if there is a change. func (obj *NspawnRes) Watch() error { - // this resource depends on systemd ensure that it's running + // this resource depends on systemd to ensure that it's running if !systemdUtil.IsRunningSystemd() { return fmt.Errorf("systemd is not running") } @@ -227,7 +227,7 @@ func (obj *NspawnRes) Watch() error { // necessary changes to reach the desired state. This is run before Watch and // again if Watch finds a change occurring to the state. func (obj *NspawnRes) CheckApply(apply bool) (checkOK bool, err error) { - // this resource depends on systemd ensure that it's running + // this resource depends on systemd to ensure that it's running if !systemdUtil.IsRunningSystemd() { return false, errors.New("systemd is not running") } diff --git a/resources/print.go b/resources/print.go new file mode 100644 index 00000000..d0062fe2 --- /dev/null +++ b/resources/print.go @@ -0,0 +1,166 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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 resources + +import ( + "fmt" + "log" +) + +func init() { + RegisterResource("print", func() Res { return &PrintRes{} }) +} + +// PrintRes is a resource that is useful for printing a message to the screen. +// It will also display a message when it receives a notification. It supports +// automatic grouping. +type PrintRes struct { + BaseRes `lang:"" yaml:",inline"` + + Msg string `lang:"msg" yaml:"msg"` // the message to display +} + +// Default returns some sensible defaults for this resource. +func (obj *PrintRes) Default() Res { + return &PrintRes{ + BaseRes: BaseRes{ + MetaParams: DefaultMetaParams, // force a default + }, + } +} + +// Validate if the params passed in are valid data. +func (obj *PrintRes) Validate() error { + return obj.BaseRes.Validate() +} + +// Init runs some startup code for this resource. +func (obj *PrintRes) Init() error { + return obj.BaseRes.Init() // call base init, b/c we're overriding +} + +// Watch is the primary listener for this resource and it outputs events. +func (obj *PrintRes) Watch() error { + // notify engine that we're running + if err := obj.Running(); err != nil { + return err // bubble up a NACK... + } + + var send = false // send event? + var exit *error + for { + select { + case event := <-obj.Events(): + // we avoid sending events on unpause + if exit, send = obj.ReadEvent(event); exit != nil { + return *exit // exit + } + } + + // do all our event sending all together to avoid duplicate msgs + if send { + send = false + obj.Event() + } + } +} + +// CheckApply method for Print resource. Does nothing, returns happy! +func (obj *PrintRes) CheckApply(apply bool) (checkOK bool, err error) { + log.Printf("%s: CheckApply: %t", obj, apply) + if obj.Refresh() { + log.Printf("%s: Received a notification!", obj) + } + log.Printf("%s: Msg: %s", obj, obj.Msg) + if g := obj.GetGroup(); len(g) > 0 { // add any grouped elements + for _, x := range g { + print, ok := x.(*PrintRes) // convert from Res + if !ok { + log.Fatalf("grouped member %v is not a %s", x, obj.GetKind()) + } + log.Printf("%s: Msg: %s", print, print.Msg) + } + } + return true, nil // state is always okay +} + +// PrintUID is the UID struct for PrintRes. +type PrintUID struct { + BaseUID + name string +} + +// UIDs includes all params to make a unique identification of this object. +// Most resources only return one, although some resources can return multiple. +func (obj *PrintRes) UIDs() []ResUID { + x := &PrintUID{ + BaseUID: BaseUID{Name: obj.GetName(), Kind: obj.GetKind()}, + name: obj.Name, + } + return []ResUID{x} +} + +// GroupCmp returns whether two resources can be grouped together or not. +func (obj *PrintRes) GroupCmp(r Res) bool { + _, ok := r.(*PrintRes) + if !ok { + return false + } + return true // grouped together if we were asked to +} + +// Compare two resources and return if they are equivalent. +func (obj *PrintRes) Compare(r Res) bool { + // we can only compare PrintRes to others of the same resource kind + res, ok := r.(*PrintRes) + if !ok { + return false + } + // calling base Compare is probably unneeded for the print res, but do it + if !obj.BaseRes.Compare(res) { // call base Compare + return false + } + if obj.Name != res.Name { + return false + } + + if obj.Msg != res.Msg { + return false + } + return true +} + +// UnmarshalYAML is the custom unmarshal handler for this struct. +// It is primarily useful for setting the defaults. +func (obj *PrintRes) UnmarshalYAML(unmarshal func(interface{}) error) error { + type rawRes PrintRes // indirection to avoid infinite recursion + + def := obj.Default() // get the default + res, ok := def.(*PrintRes) // put in the right format + if !ok { + return fmt.Errorf("could not convert to PrintRes") + } + raw := rawRes(*res) // convert; the defaults go here + + if err := unmarshal(&raw); err != nil { + return err + } + + *obj = PrintRes(raw) // restore from indirection with type conversion! + return nil +} diff --git a/resources/resources.go b/resources/resources.go index 43112d2a..b60b4617 100644 --- a/resources/resources.go +++ b/resources/resources.go @@ -104,27 +104,6 @@ const ( const refreshPathToken = "refresh" -// World is an interface to the rest of the different graph state. It allows -// the GAPI to store state and exchange information throughout the cluster. It -// is the interface each machine uses to communicate with the rest of the world. -type World interface { // TODO: is there a better name for this interface? - ResWatch() chan error - ResExport([]Res) error - // FIXME: should this method take a "filter" data struct instead of many args? - ResCollect(hostnameFilter, kindFilter []string) ([]Res, error) - - StrWatch(namespace string) chan error - StrIsNotExist(error) bool - StrGet(namespace string) (string, error) - StrSet(namespace, value string) error - StrDel(namespace string) error - - StrMapWatch(namespace string) chan error - StrMapGet(namespace string) (map[string]string, error) - StrMapSet(namespace, value string) error - StrMapDel(namespace string) error -} - // ResData is the set of input values passed into the pgraph for the resources. type ResData struct { Hostname string // uuid for the host @@ -134,6 +113,7 @@ type ResData struct { World World Prefix string // the prefix to be used for the pgraph namespace Debug bool + Logf func(format string, v ...interface{}) // NOTE: we can add more fields here if needed for the resources. } @@ -193,7 +173,7 @@ type Res interface { Watch() error // send on channel to signal process() events CheckApply(apply bool) (checkOK bool, err error) AutoEdges() (AutoEdge, error) - Compare(Res) bool + Compare(Res) bool // FIXME: rename to: `Cmp(Res) error` CollectPattern(string) // XXX: temporary until Res collection is more advanced //UnmarshalYAML(unmarshal func(interface{}) error) error // optional } @@ -507,6 +487,12 @@ func (obj *BaseRes) AutoEdges() (AutoEdge, error) { // Compare is the base compare method, which also handles the metaparams cmp. func (obj *BaseRes) Compare(res Res) bool { + + // FIXME: shouldn't we compare each property to default if one is nil? + if (obj.Meta() == nil) != (res.Meta() == nil) { // xor + return false + } + // TODO: should the AutoEdge values be compared? if obj.Meta().AutoEdge != res.Meta().AutoEdge { return false diff --git a/resources/sendrecv.go b/resources/sendrecv.go index 8968423d..5742ca53 100644 --- a/resources/sendrecv.go +++ b/resources/sendrecv.go @@ -215,7 +215,7 @@ func (obj *BaseRes) SendRecv(res Res) (map[string]bool, error) { } // if the types don't match, we can't use send->recv - // TODO: do we want to relax this for string -> *string ? + // FIXME: do we want to relax this for string -> *string ? if e := TypeCmp(value1, value2); e != nil { e := errwrap.Wrapf(e, "type mismatch between %s and %s", v.Res, obj) err = multierr.Append(err, e) // list of errors diff --git a/resources/test.go b/resources/test.go new file mode 100644 index 00000000..4be901d0 --- /dev/null +++ b/resources/test.go @@ -0,0 +1,397 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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 resources + +import ( + "fmt" + "log" + "reflect" +) + +func init() { + RegisterResource("test", func() Res { return &TestRes{} }) +} + +// TestRes is a resource that is mostly harmless and is used for internal tests. +type TestRes struct { + BaseRes `lang:"" yaml:",inline"` + + Bool bool `lang:"bool" yaml:"bool"` + Str string `lang:"str" yaml:"str"` // can't name it String because of String() + + Int int `lang:"int" yaml:"int"` + Int8 int8 `lang:"int8" yaml:"int8"` + Int16 int16 `lang:"int16" yaml:"int16"` + Int32 int32 `lang:"int32" yaml:"int32"` + Int64 int64 `lang:"int64" yaml:"int64"` + + Uint uint `lang:"uint" yaml:"uint"` + Uint8 uint8 `lang:"uint8" yaml:"uint8"` + Uint16 uint16 `lang:"uint16" yaml:"uint16"` + Uint32 uint32 `lang:"uint32" yaml:"uint32"` + Uint64 uint64 `lang:"uint64" yaml:"uint64"` + + //Uintptr uintptr `yaml:"uintptr"` + Byte byte `lang:"byte" yaml:"byte"` // alias for uint8 + Rune rune `lang:"rune" yaml:"rune"` // alias for int32, represents a Unicode code point + + Float32 float32 `lang:"float32" yaml:"float32"` + Float64 float64 `lang:"float64" yaml:"float64"` + Complex64 complex64 `lang:"complex64" yaml:"complex64"` + Complex128 complex128 `lang:"complex128" yaml:"complex128"` + + BoolPtr *bool `lang:"boolptr" yaml:"bool_ptr"` + StringPtr *string `lang:"stringptr" yaml:"string_ptr"` // TODO: tag name? + Int64Ptr *int64 `lang:"int64ptr" yaml:"int64ptr"` + Int8Ptr *int8 `lang:"int8ptr" yaml:"int8ptr"` + Uint8Ptr *uint8 `lang:"uint8ptr" yaml:"uint8ptr"` + + // probably makes no sense, but is legal + Int8PtrPtrPtr ***int8 `lang:"int8ptrptrptr" yaml:"int8ptrptrptr"` + + SliceString []string `lang:"slicestring" yaml:"slicestring"` + MapIntFloat map[int64]float64 `lang:"mapintfloat" yaml:"mapintfloat"` + MixedStruct struct { + somebool bool + somestr string + someint int64 + somefloat float64 + } `lang:"mixedstruct" yaml:"mixedstruct"` + Interface interface{} `lang:"interface" yaml:"interface"` + + AnotherStr string `lang:"anotherstr" yaml:"anotherstr"` + + ValidateBool bool `lang:"validatebool" yaml:"validate_bool"` // set to true to cause a validate error + ValidateError string `lang:"validateerror" yaml:"validate_error"` // set to cause a validate error + AlwaysGroup bool `lang:"alwaysgroup" yaml:"always_group"` // set to true to cause auto grouping + CompareFail bool `lang:"comparefail" yaml:"compare_fail"` // will compare fail? + + // TODO: add more fun properties! + + Comment string `lang:"comment" yaml:"comment"` +} + +// Default returns some sensible defaults for this resource. +func (obj *TestRes) Default() Res { + return &TestRes{ + BaseRes: BaseRes{ + MetaParams: DefaultMetaParams, // force a default + }, + } +} + +// Validate if the params passed in are valid data. +func (obj *TestRes) Validate() error { + if obj.ValidateBool { + return fmt.Errorf("the validate param was set to true") + } + if s := obj.ValidateError; s != "" { + return fmt.Errorf("the validate error param was set to: %s", s) + } + return obj.BaseRes.Validate() +} + +// Init runs some startup code for this resource. +func (obj *TestRes) Init() error { + return obj.BaseRes.Init() // call base init, b/c we're overriding +} + +// Watch is the primary listener for this resource and it outputs events. +func (obj *TestRes) Watch() error { + // notify engine that we're running + if err := obj.Running(); err != nil { + return err // bubble up a NACK... + } + + var send = false // send event? + var exit *error + for { + select { + case event := <-obj.Events(): + // we avoid sending events on unpause + if exit, send = obj.ReadEvent(event); exit != nil { + return *exit // exit + } + } + + // do all our event sending all together to avoid duplicate msgs + if send { + send = false + obj.Event() + } + } +} + +// CheckApply method for Test resource. Does nothing, returns happy! +func (obj *TestRes) CheckApply(apply bool) (checkOK bool, err error) { + log.Printf("%s: CheckApply: %t", obj, apply) + if obj.Refresh() { + log.Printf("%s: Received a notification!", obj) + } + + log.Printf("%s: Bool: %v", obj, obj.Bool) + log.Printf("%s: Str: %v", obj, obj.Str) + + log.Printf("%s: Int: %v", obj, obj.Int) + log.Printf("%s: Int8: %v", obj, obj.Int8) + log.Printf("%s: Int16: %v", obj, obj.Int16) + log.Printf("%s: Int32: %v", obj, obj.Int32) + log.Printf("%s: Int64: %v", obj, obj.Int64) + + log.Printf("%s: Uint: %v", obj, obj.Uint) + log.Printf("%s: Uint8: %v", obj, obj.Uint) + log.Printf("%s: Uint16: %v", obj, obj.Uint) + log.Printf("%s: Uint32: %v", obj, obj.Uint) + log.Printf("%s: Uint64: %v", obj, obj.Uint) + + //log.Printf("%s: Uintptr: %v", obj, obj.Uintptr) + log.Printf("%s: Byte: %v", obj, obj.Byte) + log.Printf("%s: Rune: %v", obj, obj.Rune) + + log.Printf("%s: Float32: %v", obj, obj.Float32) + log.Printf("%s: Float64: %v", obj, obj.Float64) + log.Printf("%s: Complex64: %v", obj, obj.Complex64) + log.Printf("%s: Complex128: %v", obj, obj.Complex128) + + log.Printf("%s: BoolPtr: %v", obj, obj.BoolPtr) + log.Printf("%s: StringPtr: %v", obj, obj.StringPtr) + log.Printf("%s: Int64Ptr: %v", obj, obj.Int64Ptr) + log.Printf("%s: Int8Ptr: %v", obj, obj.Int8Ptr) + log.Printf("%s: Uint8Ptr: %v", obj, obj.Uint8Ptr) + + log.Printf("%s: Int8PtrPtrPtr: %v", obj, obj.Int8PtrPtrPtr) + + log.Printf("%s: SliceString: %v", obj, obj.SliceString) + log.Printf("%s: MapIntFloat: %v", obj, obj.MapIntFloat) + log.Printf("%s: MixedStruct: %v", obj, obj.MixedStruct) + log.Printf("%s: Interface: %v", obj, obj.Interface) + + log.Printf("%s: AnotherStr: %v", obj, obj.AnotherStr) + + return true, nil // state is always okay +} + +// TestUID is the UID struct for TestRes. +type TestUID struct { + BaseUID + name string +} + +// UIDs includes all params to make a unique identification of this object. +// Most resources only return one, although some resources can return multiple. +func (obj *TestRes) UIDs() []ResUID { + x := &TestUID{ + BaseUID: BaseUID{Name: obj.GetName(), Kind: obj.GetKind()}, + name: obj.Name, + } + return []ResUID{x} +} + +// GroupCmp returns whether two resources can be grouped together or not. +func (obj *TestRes) GroupCmp(r Res) bool { + _, ok := r.(*TestRes) + if !ok { + return false + } + return obj.AlwaysGroup // grouped together if we were asked to +} + +// Compare two resources and return if they are equivalent. +func (obj *TestRes) Compare(r Res) bool { + // we can only compare TestRes to others of the same resource kind + res, ok := r.(*TestRes) + if !ok { + return false + } + // calling base Compare is probably unneeded for the test res, but do it + if !obj.BaseRes.Compare(res) { // call base Compare + return false + } + if obj.Name != res.Name { + return false + } + + if obj.CompareFail || res.CompareFail { + return false + } + + // TODO: yes, I know the long manual version is absurd, but I couldn't + // get these to work :( + //if !reflect.DeepEqual(obj, res) { // is broken :/ + //if diff := pretty.Compare(obj, res); diff != "" { // causes stack overflow + // return false + //} + + if obj.Bool != res.Bool { + return false + } + if obj.Str != res.Str { + return false + } + + if obj.Int != res.Int { + return false + } + if obj.Int8 != res.Int8 { + return false + } + if obj.Int16 != res.Int16 { + return false + } + if obj.Int32 != res.Int32 { + return false + } + if obj.Int64 != res.Int64 { + return false + } + + if obj.Uint != res.Uint { + return false + } + if obj.Uint8 != res.Uint8 { + return false + } + if obj.Uint16 != res.Uint16 { + return false + } + if obj.Uint32 != res.Uint32 { + return false + } + if obj.Uint64 != res.Uint64 { + return false + } + + //if obj.Uintptr + if obj.Byte != res.Byte { + return false + } + if obj.Rune != res.Rune { + return false + } + + if obj.Float32 != res.Float32 { + return false + } + if obj.Float64 != res.Float64 { + return false + } + if obj.Complex64 != res.Complex64 { + return false + } + if obj.Complex128 != res.Complex128 { + return false + } + + if (obj.BoolPtr == nil) != (res.BoolPtr == nil) { // xor + return false + } + if obj.BoolPtr != nil && res.BoolPtr != nil { + if *obj.BoolPtr != *res.BoolPtr { // compare + return false + } + } + if (obj.StringPtr == nil) != (res.StringPtr == nil) { // xor + return false + } + if obj.StringPtr != nil && res.StringPtr != nil { + if *obj.StringPtr != *res.StringPtr { // compare + return false + } + } + if (obj.Int64Ptr == nil) != (res.Int64Ptr == nil) { // xor + return false + } + if obj.Int64Ptr != nil && res.Int64Ptr != nil { + if *obj.Int64Ptr != *res.Int64Ptr { // compare + return false + } + } + if (obj.Int8Ptr == nil) != (res.Int8Ptr == nil) { // xor + return false + } + if obj.Int8Ptr != nil && res.Int8Ptr != nil { + if *obj.Int8Ptr != *res.Int8Ptr { // compare + return false + } + } + if (obj.Uint8Ptr == nil) != (res.Uint8Ptr == nil) { // xor + return false + } + if obj.Uint8Ptr != nil && res.Uint8Ptr != nil { + if *obj.Uint8Ptr != *res.Uint8Ptr { // compare + return false + } + } + + if !reflect.DeepEqual(obj.Int8PtrPtrPtr, res.Int8PtrPtrPtr) { + return false + } + + if !reflect.DeepEqual(obj.SliceString, res.SliceString) { + return false + } + if !reflect.DeepEqual(obj.MapIntFloat, res.MapIntFloat) { + return false + } + if !reflect.DeepEqual(obj.MixedStruct, res.MixedStruct) { + return false + } + if !reflect.DeepEqual(obj.Interface, res.Interface) { + return false + } + + if obj.AnotherStr != res.AnotherStr { + return false + } + + if obj.ValidateBool != res.ValidateBool { + return false + } + if obj.ValidateError != res.ValidateError { + return false + } + if obj.AlwaysGroup != res.AlwaysGroup { + return false + } + + if obj.Comment != res.Comment { + return false + } + + return true +} + +// UnmarshalYAML is the custom unmarshal handler for this struct. +// It is primarily useful for setting the defaults. +func (obj *TestRes) UnmarshalYAML(unmarshal func(interface{}) error) error { + type rawRes TestRes // indirection to avoid infinite recursion + + def := obj.Default() // get the default + res, ok := def.(*TestRes) // put in the right format + if !ok { + return fmt.Errorf("could not convert to TestRes") + } + raw := rawRes(*res) // convert; the defaults go here + + if err := unmarshal(&raw); err != nil { + return err + } + + *obj = TestRes(raw) // restore from indirection with type conversion! + return nil +} diff --git a/resources/timer.go b/resources/timer.go index b0632d13..159fba6e 100644 --- a/resources/timer.go +++ b/resources/timer.go @@ -27,10 +27,11 @@ func init() { RegisterResource("timer", func() Res { return &TimerRes{} }) } -// TimerRes is a timer resource for time based events. +// TimerRes is a timer resource for time based events. It outputs an event every +// interval seconds. type TimerRes struct { BaseRes `yaml:",inline"` - Interval uint32 `yaml:"interval"` // Interval : Interval between runs + Interval uint32 `yaml:"interval"` // interval between runs in seconds ticker *time.Ticker } diff --git a/resources/util.go b/resources/util.go index 79e88dac..07b4ae45 100644 --- a/resources/util.go +++ b/resources/util.go @@ -28,6 +28,8 @@ import ( "strconv" "strings" + "github.com/purpleidea/mgmt/lang/types" + errwrap "github.com/pkg/errors" ) @@ -75,14 +77,12 @@ func B64ToRes(str string) (Res, error) { } b := bytes.NewBuffer(bb) d := gob.NewDecoder(b) - err = d.Decode(&output) // pass with & - if err != nil { + if err := d.Decode(&output); err != nil { // pass with & return nil, errwrap.Wrapf(err, "gob failed to decode") } res, ok := output.(Res) if !ok { return nil, fmt.Errorf("output `%v` is not a Res", output) - } return res, nil } @@ -134,6 +134,109 @@ func LowerStructFieldNameToFieldName(res Res) (map[string]string, error) { return result, nil } +// LangFieldNameToStructFieldName returns the mapping from lang (AST) field +// names to field name as used in the struct. The logic here is a bit strange; +// if the resource has struct tags, then it uses those, otherwise it falls back +// to using the lower case versions of things. It might be clever to combine the +// two so that tagged fields are used as such, and others are used in lowercase, +// but this is currently not implemented. +// TODO: should this behaviour be changed? +func LangFieldNameToStructFieldName(kind string) (map[string]string, error) { + res, err := NewResource(kind) + if err != nil { + return nil, err + } + mapping, err := StructTagToFieldName(res) + if err != nil { + return nil, errwrap.Wrapf(err, "resource kind `%s` has bad field mapping", kind) + } + if len(mapping) == 0 { // if no `lang` tags exist, get them automatically + mapping, err = LowerStructFieldNameToFieldName(res) + if err != nil { + return nil, errwrap.Wrapf(err, "resource kind `%s` has bad automatic field mapping", kind) + } + } + + return mapping, nil // lang field name -> field name +} + +// StructKindToFieldNameTypeMap returns a map from field name to expected type +// in the lang type system. +func StructKindToFieldNameTypeMap(kind string) (map[string]*types.Type, error) { + res, err := NewResource(kind) + if err != nil { + return nil, err + } + + sv := reflect.ValueOf(res).Elem() // pointer to struct, then struct + if k := sv.Kind(); k != reflect.Struct { + return nil, fmt.Errorf("expected struct, got: %s", k) + } + + result := make(map[string]*types.Type) + + st := reflect.TypeOf(res).Elem() // pointer to struct, then struct + for i := 0; i < st.NumField(); i++ { + field := st.Field(i) + name := field.Name + // TODO: in future, skip over fields that don't have a `lang` tag + //if name == "BaseRes" { // TODO: hack!!! + // continue + //} + + typ, err := types.TypeOf(field.Type) + // some types (eg complex64) aren't convertible, so skip for now... + if err != nil { + continue + //return nil, errwrap.Wrapf(err, "could not identify type of field `%s`", name) + } + result[name] = typ + } + + return result, nil +} + +// LangFieldNameToStructType returns the mapping from lang (AST) field names, +// and the expected type in our type system for each. +func LangFieldNameToStructType(kind string) (map[string]*types.Type, error) { + // returns a mapping between fieldName and expected *types.Type + fieldNameTypMap, err := StructKindToFieldNameTypeMap(kind) + if err != nil { + return nil, errwrap.Wrapf(err, "could not determine types for `%s` resource", kind) + } + + mapping, err := LangFieldNameToStructFieldName(kind) + if err != nil { + return nil, err + } + + // transform from field name to tag name + typMap := make(map[string]*types.Type) + for name, typ := range fieldNameTypMap { + if strings.Title(name) != name { + continue // skip private fields + } + + found := false + for k, v := range mapping { + if v != name { + continue + } + // found + if found { // previously found! + return nil, fmt.Errorf("duplicate mapping for: %s", name) + } + typMap[k] = typ + found = true // :) + } + if !found { + return nil, fmt.Errorf("could not find mapping for: %s", name) + } + } + + return typMap, nil +} + // GetUID returns the UID of an user. It supports an UID or an username. Caller // should first check user is not empty. It will return an error if it can't // lookup the UID or username. diff --git a/resources/util_test.go b/resources/util_test.go index 0a59d6a5..936f26b6 100644 --- a/resources/util_test.go +++ b/resources/util_test.go @@ -155,8 +155,7 @@ func TestMiscEncodeDecode2(t *testing.T) { func TestStructTagToFieldName0(t *testing.T) { type TestStruct struct { - // TODO: switch this to TestRes when it is in git master - NoopRes // so that this struct implements `Res` + TestRes // so that this struct implements `Res` Alpha bool `lang:"alpha" yaml:"nope"` Beta string `yaml:"beta"` Gamma string @@ -182,8 +181,7 @@ func TestStructTagToFieldName0(t *testing.T) { func TestLowerStructFieldNameToFieldName0(t *testing.T) { type TestStruct struct { - // TODO: switch this to TestRes when it is in git master - NoopRes // so that this struct implements `Res` + TestRes // so that this struct implements `Res` Alpha bool skipMe bool Beta string @@ -200,7 +198,7 @@ func TestLowerStructFieldNameToFieldName0(t *testing.T) { } expected := map[string]string{ - "noopres": "NoopRes", // TODO: switch this to TestRes when it is in git master + "testres": "TestRes", // hide by specifying `lang:""` on it "alpha": "Alpha", //"skipme": "skipMe", "beta": "Beta", @@ -218,8 +216,7 @@ func TestLowerStructFieldNameToFieldName0(t *testing.T) { func TestLowerStructFieldNameToFieldName1(t *testing.T) { type TestStruct struct { - // TODO: switch this to TestRes when it is in git master - NoopRes // so that this struct implements `Res` + TestRes // so that this struct implements `Res` Alpha bool skipMe bool Beta string @@ -239,6 +236,68 @@ func TestLowerStructFieldNameToFieldName1(t *testing.T) { } } +func TestLowerStructFieldNameToFieldName2(t *testing.T) { + mapping, err := LowerStructFieldNameToFieldName(&TestRes{}) + if err != nil { + t.Errorf("failed: %+v", err) + return + } + + expected := map[string]string{ + "baseres": "BaseRes", + + "bool": "Bool", + "str": "Str", + + "int": "Int", + "int8": "Int8", + "int16": "Int16", + "int32": "Int32", + "int64": "Int64", + + "uint": "Uint", + "uint8": "Uint8", + "uint16": "Uint16", + "uint32": "Uint32", + "uint64": "Uint64", + + "byte": "Byte", + "rune": "Rune", + + "float32": "Float32", + "float64": "Float64", + "complex64": "Complex64", + "complex128": "Complex128", + + "boolptr": "BoolPtr", + "stringptr": "StringPtr", + "int64ptr": "Int64Ptr", + "int8ptr": "Int8Ptr", + "uint8ptr": "Uint8Ptr", + + "int8ptrptrptr": "Int8PtrPtrPtr", + + "slicestring": "SliceString", + "mapintfloat": "MapIntFloat", + "mixedstruct": "MixedStruct", + "interface": "Interface", + + "anotherstr": "AnotherStr", + + "validatebool": "ValidateBool", + "validateerror": "ValidateError", + "alwaysgroup": "AlwaysGroup", + "comparefail": "CompareFail", + + "comment": "Comment", + } + + if !reflect.DeepEqual(mapping, expected) { + t.Errorf("expected: %+v", expected) + t.Errorf("received: %+v", mapping) + } +} + func TestUnknownGroup(t *testing.T) { gid, err := GetGID("unknowngroup") if err == nil { diff --git a/resources/virt.go b/resources/virt.go index 170b7d26..5680abb6 100644 --- a/resources/virt.go +++ b/resources/virt.go @@ -31,6 +31,8 @@ import ( "sync" "time" + "github.com/purpleidea/mgmt/util" + multierr "github.com/hashicorp/go-multierror" "github.com/libvirt/libvirt-go" libvirtxml "github.com/libvirt/libvirt-go-xml" @@ -999,7 +1001,7 @@ func (d *diskDevice) GetXML(idx int) string { b += "" b += fmt.Sprintf("", d.Type) b += fmt.Sprintf("", source) - b += fmt.Sprintf("", numToAlpha(idx)) + b += fmt.Sprintf("", util.NumToAlpha(idx)) b += "" return b } @@ -1010,7 +1012,7 @@ func (d *cdRomDevice) GetXML(idx int) string { b += "" b += fmt.Sprintf("", d.Type) b += fmt.Sprintf("", source) - b += fmt.Sprintf("", numToAlpha(idx)) + b += fmt.Sprintf("", util.NumToAlpha(idx)) b += "" b += "" return b @@ -1196,12 +1198,3 @@ func expandHome(p string) (string, error) { return p, nil } - -func numToAlpha(idx int) string { - var mod = idx % 26 - var div = idx / 26 - if div > 0 { - return numToAlpha(div-1) + string(rune(mod+int('a'))) - } - return string(rune(mod + int('a'))) -} diff --git a/resources/virt_test.go b/resources/virt_test.go index bc27934b..9aab7e5d 100644 --- a/resources/virt_test.go +++ b/resources/virt_test.go @@ -46,25 +46,3 @@ func TestExpandHome(t *testing.T) { } } } - -func TestNumToAlpha(t *testing.T) { - var numToAlphaTests = []struct { - number int - result string - }{ - {0, "a"}, - {25, "z"}, - {26, "aa"}, - {27, "ab"}, - {702, "aaa"}, - {703, "aab"}, - {63269, "cool"}, - } - - for _, test := range numToAlphaTests { - actual := numToAlpha(test.number) - if actual != test.result { - t.Errorf("numToAlpha(%d): expected %s, actual %s", test.number, test.result, actual) - } - } -} diff --git a/test.sh b/test.sh index 0750ef3b..d0e5814c 100755 --- a/test.sh +++ b/test.sh @@ -29,7 +29,7 @@ run-test ./test/test-gotest.sh # do these longer tests only when running on ci if env | grep -q -e '^TRAVIS=true$' -e '^JENKINS_URL=' -e '^BUILD_TAG=jenkins'; then run-test ./test/test-shell.sh - run-test ./test/test-gotest.sh --race + #run-test ./test/test-gotest.sh --race # XXX: temporarily disabled... fi run-test ./test/test-gometalinter.sh diff --git a/test/shell/graph-exit1.sh b/test/shell/graph-exit1.sh index cb2d43cf..37845b98 100755 --- a/test/shell/graph-exit1.sh +++ b/test/shell/graph-exit1.sh @@ -2,7 +2,7 @@ # should take 15 seconds for longest resources plus startup time to shutdown # we don't want the ^C to allow the rest of the graph to continue executing! -$timeout --kill-after=35s 25s ./mgmt run --yaml graph-exit.yaml --no-watch --no-pgp --tmp-prefix & +$timeout --kill-after=65s 55s ./mgmt run --yaml graph-exit1.yaml --no-watch --no-pgp --tmp-prefix & pid=$! sleep 5s # let the initial resources start to run... killall -SIGINT mgmt # send ^C to exit mgmt diff --git a/test/shell/graph-exit2.sh b/test/shell/graph-exit2.sh index a04f7998..56b8f8c6 100755 --- a/test/shell/graph-exit2.sh +++ b/test/shell/graph-exit2.sh @@ -2,7 +2,7 @@ # should take 15 seconds for longest resources plus startup time to shutdown # we don't want the ^C to allow the rest of the graph to continue executing! -$timeout --kill-after=45s 35s ./mgmt run --yaml graph-exit.yaml --no-watch --no-pgp --tmp-prefix & +$timeout --kill-after=65s 55s ./mgmt run --yaml graph-exit2.yaml --no-watch --no-pgp --tmp-prefix & pid=$! sleep 10s # let the initial resources start to run... killall -SIGINT mgmt # send ^C to exit mgmt diff --git a/test/shell/libmgmt-change1.sh b/test/shell/libmgmt-change1.sh index 3318617b..a5fdd90a 100755 --- a/test/shell/libmgmt-change1.sh +++ b/test/shell/libmgmt-change1.sh @@ -1,5 +1,8 @@ #!/bin/bash -e +# XXX: this has not been updated to latest GAPI/Deploy API. Patches welcome! +exit 0 + go build -i -o libmgmt libmgmt-change1.go # this example should change graphs frequently, and then shutdown... $timeout --kill-after=30s 20s ./libmgmt & diff --git a/test/shell/libmgmt-change2.sh b/test/shell/libmgmt-change2.sh index b91da796..58d9f5b2 100755 --- a/test/shell/libmgmt-change2.sh +++ b/test/shell/libmgmt-change2.sh @@ -1,5 +1,8 @@ #!/bin/bash -e +# XXX: this has not been updated to latest GAPI/Deploy API. Patches welcome! +exit 0 + go build -i -o libmgmt libmgmt-change2.go # this example should change graphs frequently, and then shutdown... $timeout --kill-after=30s 20s ./libmgmt & diff --git a/test/shell/yaml-change1.sh b/test/shell/yaml-change1.sh index 6aa7e6e3..f84c67cf 100755 --- a/test/shell/yaml-change1.sh +++ b/test/shell/yaml-change1.sh @@ -1,5 +1,7 @@ #!/bin/bash -e +exit 0 # TODO: this test needs to be update to use deploys instead + #if env | grep -q -e '^TRAVIS=true$'; then # # inotify doesn't seem to work properly on travis # echo "Travis and Jenkins give wonky results here, skipping test!" diff --git a/test/test-bashfmt.sh b/test/test-bashfmt.sh index 39944f7b..c45b95a8 100755 --- a/test/test-bashfmt.sh +++ b/test/test-bashfmt.sh @@ -2,16 +2,15 @@ # check for any bash files that aren't properly formatted # TODO: this is hardly exhaustive -. test/util.sh - echo running test-bashfmt.sh set -o errexit set -o nounset set -o pipefail +#ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd )" # dir! ROOT=$(dirname "${BASH_SOURCE}")/.. - cd "${ROOT}" +. test/util.sh find_files() { git ls-files | grep -e '\.sh$' -e '\.bash$' | grep -v 'misc/delta-cpu.sh' diff --git a/test/test-examples.sh b/test/test-examples.sh index 299d9cda..ad05b56a 100755 --- a/test/test-examples.sh +++ b/test/test-examples.sh @@ -1,18 +1,20 @@ #!/bin/bash # check that our examples still build, even if we don't run them here -. test/util.sh - echo running test-examples.sh +#ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd )" # dir! +ROOT=$(dirname "${BASH_SOURCE}")/.. +cd "${ROOT}" +. test/util.sh + failures='' function run-test() { $@ || failures=$( [ -n "$failures" ] && echo "$failures\\n$@" || echo "$@" ) } -ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd )" # dir! -cd "${ROOT}" +make build buildout='test-examples.out' # make symlink to outside of package @@ -34,6 +36,7 @@ rm `basename "$linkto"` cd .. rmdir "$tmpdir" # cleanup +make clean if [[ -n "$failures" ]]; then echo 'FAIL' echo "The following tests (in: ${linkto}) have failed:" diff --git a/test/test-gofmt.sh b/test/test-gofmt.sh index ed3f323a..e2d4030d 100755 --- a/test/test-gofmt.sh +++ b/test/test-gofmt.sh @@ -1,14 +1,15 @@ #!/bin/bash # original version of this script from kubernetes project, under ALv2 license -. test/util.sh - echo running test-gofmt.sh set -o errexit set -o nounset set -o pipefail +#ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd )" # dir! ROOT=$(dirname "${BASH_SOURCE}")/.. +cd "${ROOT}" +. test/util.sh #GO_VERSION=($(go version)) # @@ -16,8 +17,6 @@ ROOT=$(dirname "${BASH_SOURCE}")/.. # fail_test "Unknown go version '${GO_VERSION[2]}', failing gofmt." #fi -cd "${ROOT}" - find_files() { git ls-files | grep '\.go$' } diff --git a/test/test-golint.sh b/test/test-golint.sh index 41f62d2c..36db5dca 100755 --- a/test/test-golint.sh +++ b/test/test-golint.sh @@ -1,18 +1,20 @@ #!/bin/bash # check that go lint passes or doesn't get worse by some threshold +echo running test-golint.sh + +ORIGPWD=`pwd` +#ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd )" # dir! +ROOT=$(dirname "${BASH_SOURCE}")/.. +cd "${ROOT}" . test/util.sh -echo running test-golint.sh # TODO: replace with gometalinter instead of plain golint # TODO: output a diff of what has changed in the golint output # FIXME: test a range of commits, since only the last patch is checked here PREVIOUS='HEAD^' CURRENT='HEAD' THRESHOLD=1 # percent problems per new LOC -XPWD=`pwd` -ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd )" # dir! -cd "${ROOT}" >/dev/null # if this branch has more than one commit as compared to master, diff to that # note: this is a cheap way to avoid doing a fancy succession of golint's... @@ -55,7 +57,7 @@ LINT1=`find . -maxdepth 3 -iname '*.go' -not -path './old/*' -not -path './tmp/* COUNT1=`echo -e "$LINT1" | wc -l` # number of golint problems in older branch # clean up -cd "$XPWD" >/dev/null +cd "$ORIGPWD" >/dev/null rm -rf "$T" DELTA=$(printf "%.0f\n" `echo - | awk "{ print (($COUNT - $COUNT1) / $DIFF1) * 100 }"`) diff --git a/test/test-gometalinter.sh b/test/test-gometalinter.sh index adac0b34..1d8c7464 100755 --- a/test/test-gometalinter.sh +++ b/test/test-gometalinter.sh @@ -2,19 +2,19 @@ # check a bunch of linters with the gometalinter # TODO: run this from the test-golint.sh file instead to check for deltas -. test/util.sh - echo running test-gometalinter.sh +#ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd )" # dir! +ROOT=$(dirname "${BASH_SOURCE}")/.. +cd "${ROOT}" +. test/util.sh + failures='' function run-test() { $@ || failures=$( [ -n "$failures" ] && echo "$failures\\n$@" || echo "$@" ) } -ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd )" # dir! -cd "${ROOT}" - # TODO: run more linters here if we're brave... gml='gometalinter --disable-all' #gml="$gml --enable=aligncheck" diff --git a/test/test-gotest.sh b/test/test-gotest.sh index d395efd5..f4595d6a 100755 --- a/test/test-gotest.sh +++ b/test/test-gotest.sh @@ -1,17 +1,19 @@ #!/bin/bash -. test/util.sh - echo "running test-gotest.sh $1" +#ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd )" # dir! +ROOT=$(dirname "${BASH_SOURCE}")/.. +cd "${ROOT}" +. test/util.sh + failures='' function run-test() { $@ || failures=$( [ -n "$failures" ] && echo "$failures\\n$@" || echo "$@" ) } -ROOT=$(dirname "${BASH_SOURCE}")/.. -cd "${ROOT}" +make build base=$(go list .) for pkg in `go list ./... | grep -v "^${base}/vendor/" | grep -v "^${base}/examples/" | grep -v "^${base}/test/" | grep -v "^${base}/old/" | grep -v "^${base}/tmp/"`; do @@ -23,6 +25,7 @@ for pkg in `go list ./... | grep -v "^${base}/vendor/" | grep -v "^${base}/examp fi done +make clean if [[ -n "$failures" ]]; then echo 'FAIL' echo 'The following `go test` runs have failed:' diff --git a/test/test-govet.sh b/test/test-govet.sh index ebeec4d3..ae31da2c 100755 --- a/test/test-govet.sh +++ b/test/test-govet.sh @@ -1,19 +1,19 @@ #!/bin/bash # check that go vet passes -. test/util.sh - echo running test-govet.sh +#ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd )" # dir! +ROOT=$(dirname "${BASH_SOURCE}")/.. +cd "${ROOT}" +. test/util.sh + failures='' function run-test() { $@ || failures=$( [ -n "$failures" ] && echo "$failures\\n$@" || echo "$@" ) } -ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd )" # dir! -cd "${ROOT}" - GO_VERSION=($(go version)) function simplify-gocase() { diff --git a/test/test-headerfmt.sh b/test/test-headerfmt.sh index 23c54b1e..a471a107 100755 --- a/test/test-headerfmt.sh +++ b/test/test-headerfmt.sh @@ -1,20 +1,22 @@ #!/bin/bash # check that headers are properly formatted +echo running test-headerfmt.sh + +#ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd )" # dir! +ROOT=$(dirname "${BASH_SOURCE}")/.. +cd "${ROOT}" . test/util.sh -echo running test-headerfmt.sh -ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd )" # dir! -FILE="${ROOT}/main.go" # file headers should match main.go +FILE="main.go" # file headers should match main.go COUNT=0 while IFS='' read -r line; do # find what header should look like echo "$line" | grep -q '^//' || break COUNT=`expr $COUNT + 1` done < "$FILE" -cd "${ROOT}" find_files() { - git ls-files | grep '\.go$' | grep -v '^bindata/' | grep -v '^examples/' | grep -v '^test/' + git ls-files | grep '\.go$' | grep -v '^examples/' | grep -v '^test/' } bad_files=$( diff --git a/test/test-shell.sh b/test/test-shell.sh index 71cba92d..2d5aae3a 100755 --- a/test/test-shell.sh +++ b/test/test-shell.sh @@ -2,7 +2,15 @@ # simple test harness for testing mgmt # NOTE: this will rm -rf /tmp/mgmt/ +echo running test-shell.sh +set -o errexit +set -o pipefail + +#ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd )" # dir! +ROOT=$(dirname "${BASH_SOURCE}")/.. +cd "${ROOT}" . test/util.sh +cd - >/dev/null if [ "$1" == "--help" ] || [ "$1" == "-h" ]; then echo -e "usage: ./"`basename $0`" [[--help] | ]" @@ -10,10 +18,6 @@ if [ "$1" == "--help" ] || [ "$1" == "-h" ]; then exit 1 fi -echo running test-shell.sh -set -o errexit -set -o pipefail - LINE=$(printf '=%.0s' `seq -s ' ' $(tput cols)`) # a terminal width string DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd )" # dir! cd "$DIR" >/dev/null # work from main mgmt directory diff --git a/test/test-yamlfmt.sh b/test/test-yamlfmt.sh index b23b7b70..af838a4a 100755 --- a/test/test-yamlfmt.sh +++ b/test/test-yamlfmt.sh @@ -3,20 +3,21 @@ exit 0 # i give up, we're skipping this entirely, help wanted to fix this -. test/util.sh - echo running test-yamlfmt.sh set -o errexit set -o nounset set -o pipefail +#ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd )" # dir! +ROOT=$(dirname "${BASH_SOURCE}")/.. +cd "${ROOT}" +. test/util.sh + #if env | grep -q -e '^TRAVIS=true$' -e '^JENKINS_URL=' -e '^BUILD_TAG=jenkins'; then # echo "Travis and Jenkins give wonky results here, skipping test!" # exit 0 #fi -ROOT=$(dirname "${BASH_SOURCE}")/.. - RUBY=`command -v ruby 2>/dev/null` if [ -z $RUBY ]; then fail_test "The 'ruby' utility can't be found." @@ -46,8 +47,6 @@ if [ "$major" -eq 2 ] && [ "$minor" -lt 1 ] ; then exit 0 fi -cd "${ROOT}" - find_files() { git ls-files | grep '\.yaml$' } diff --git a/util/afero.go b/util/afero.go new file mode 100644 index 00000000..b50445b0 --- /dev/null +++ b/util/afero.go @@ -0,0 +1,136 @@ +// Mgmt +// Copyright (C) 2013-2018+ 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 util + +import ( + "fmt" + "io" + "os" + "path" + + "github.com/spf13/afero" +) + +// FsTree returns a string representation of the file system tree similar to the +// well-known `tree` command. +func FsTree(fs afero.Fs, name string) (string, error) { + str := ".\n" // top level dir + s, err := stringify(fs, path.Clean(name), []bool{}) + if err != nil { + return "", err + } + str += s + return str, nil +} + +func stringify(fs afero.Fs, name string, indent []bool) (string, error) { + str := "" + dir, err := fs.Open(name) + if err != nil { + return "", err + } + + fileinfo, err := dir.Readdir(-1) + if err != nil && err != io.EOF { + return "", err + } + for i, fi := range fileinfo { + for _, last := range indent { + if last { + str += " " + } else { + str += "│ " + } + } + + header := "├── " + var last bool + if i == len(fileinfo)-1 { // if last + header = "└── " + last = true + } + + p := fi.Name() + if fi.IsDir() { + p += "/" // identify as a dir + } + str += fmt.Sprintf("%s%s\n", header, p) + if fi.IsDir() { + indented := append(indent, last) + s, err := stringify(fs, path.Join(name, p), indented) + if err != nil { + return "", err // TODO: return partial tree? + } + str += s + } + } + return str, nil +} + +// CopyFs copies a dir from the srcFs to a dir on the dstFs. It expects that the +// dst will be either empty, or that the force flag will be set to true. If the +// dst has a different set of contents in the same location, the behaviour is +// currently undefined. +// TODO: this should be made more rsync like and robust! +func CopyFs(srcFs, dstFs afero.Fs, src, dst string, force bool) error { + if src == "" { + src = "/" + } + if dst == "" { + dst = "/" + } + walkFn := func(name string, info os.FileInfo, err error) error { + if err != nil { + return err + } + //perm := info.Perm() + perm := info.Mode() // TODO: is this correct? + p := name + if dst != "/" { + p = path.Join(dst, name) // relative dest in dst + } + if info.IsDir() { + err := dstFs.Mkdir(p, perm) + if os.IsExist(err) && (name == "/" || force) { + return nil + } + return err + } + + data, err := afero.ReadFile(srcFs, name) + if err != nil { + return err + } + // create file + return afero.WriteFile(dstFs, p, data, perm) + } + + return afero.Walk(srcFs, src, walkFn) +} + +// CopyFsToDisk performs exactly as CopyFs, except that the dst fs is our local +// disk os fs. +func CopyFsToDisk(srcFs afero.Fs, src, dst string, force bool) error { + return CopyFs(srcFs, afero.NewOsFs(), src, dst, force) +} + +// CopyDiskToFs performs exactly as CopyFs, except that the src fs is our local +// disk os fs. +func CopyDiskToFs(dstFs afero.Fs, src, dst string, force bool) error { + return CopyFs(afero.NewOsFs(), dstFs, src, dst, force) +} diff --git a/util/util.go b/util/util.go index e817a677..cf6a891b 100644 --- a/util/util.go +++ b/util/util.go @@ -27,6 +27,18 @@ import ( "github.com/godbus/dbus" ) +// NumToAlpha returns a lower case string of letters representing a number. If +// you specify 0, you'll get `a`, 25 gives you `z`, and 26 gives you `aa` and so +// on... +func NumToAlpha(idx int) string { + var mod = idx % 26 + var div = idx / 26 + if div > 0 { + return NumToAlpha(div-1) + string(rune(mod+int('a'))) + } + return string(rune(mod + int('a'))) +} + // FirstToUpper returns the string with the first character capitalized. func FirstToUpper(str string) string { if str == "" { diff --git a/util/util_test.go b/util/util_test.go index 0443684f..b5e2f4ca 100644 --- a/util/util_test.go +++ b/util/util_test.go @@ -23,6 +23,28 @@ import ( "testing" ) +func TestNumToAlpha(t *testing.T) { + var numToAlphaTests = []struct { + number int + result string + }{ + {0, "a"}, + {25, "z"}, + {26, "aa"}, + {27, "ab"}, + {702, "aaa"}, + {703, "aab"}, + {63269, "cool"}, + } + + for _, test := range numToAlphaTests { + actual := NumToAlpha(test.number) + if actual != test.result { + t.Errorf("NumToAlpha(%d): expected %s, actual %s", test.number, test.result, actual) + } + } +} + func TestUtilT1(t *testing.T) { if Dirname("/foo/bar/baz") != "/foo/bar/" { diff --git a/vendor/github.com/coreos/etcd b/vendor/github.com/coreos/etcd index bb66589f..589a7a19 160000 --- a/vendor/github.com/coreos/etcd +++ b/vendor/github.com/coreos/etcd @@ -1 +1 @@ -Subproject commit bb66589f8cf18960c7f3d56b1b83753caeed9c7a +Subproject commit 589a7a19ac469afa687ab1f7487dd5d4c2a6ee6a diff --git a/yamlgraph/gapi.go b/yamlgraph/gapi.go index 1ea8c779..d40b0450 100644 --- a/yamlgraph/gapi.go +++ b/yamlgraph/gapi.go @@ -25,11 +25,26 @@ import ( "github.com/purpleidea/mgmt/gapi" "github.com/purpleidea/mgmt/pgraph" "github.com/purpleidea/mgmt/recwatch" + "github.com/purpleidea/mgmt/resources" + + errwrap "github.com/pkg/errors" + "github.com/urfave/cli" ) +const ( + // Name is the name of this frontend. + Name = "yaml" + // Start is the entry point filename that we use. It is arbitrary. + Start = "/start.yaml" +) + +func init() { + gapi.Register(Name, func() gapi.GAPI { return &GAPI{} }) // register +} + // GAPI implements the main yamlgraph GAPI interface. type GAPI struct { - File *string // yaml graph definition to use; nil if undefined + InputURI string // input URI of file system containing yaml graph to use data gapi.Data initialized bool @@ -38,12 +53,43 @@ type GAPI struct { configWatcher *recwatch.ConfigWatcher } -// NewGAPI creates a new yamlgraph GAPI struct and calls Init(). -func NewGAPI(data gapi.Data, file *string) (*GAPI, error) { - obj := &GAPI{ - File: file, +// Cli takes a cli.Context, and returns our GAPI if activated. All arguments +// should take the prefix of the registered name. On activation, if there are +// any validation problems, you should return an error. If this was not +// activated, then you should return a nil GAPI and a nil error. +func (obj *GAPI) Cli(c *cli.Context, fs resources.Fs) (*gapi.Deploy, error) { + if s := c.String(Name); c.IsSet(Name) { + if s == "" { + return nil, fmt.Errorf("input yaml is empty") + } + + // single file input only + if err := gapi.CopyFileToFs(fs, s, Start); err != nil { + return nil, errwrap.Wrapf(err, "can't copy yaml from `%s` to `%s`", s, Start) + } + + return &gapi.Deploy{ + Name: Name, + Noop: c.GlobalBool("noop"), + Sema: c.GlobalInt("sema"), + GAPI: &GAPI{ + InputURI: fs.URI(), + // TODO: add properties here... + }, + }, nil + } + return nil, nil // we weren't activated! +} + +// CliFlags returns a list of flags used by this deploy subcommand. +func (obj *GAPI) CliFlags() []cli.Flag { + return []cli.Flag{ + cli.StringFlag{ + Name: Name, + Value: "", + Usage: "yaml graph definition to run", + }, } - return obj, obj.Init(data) } // Init initializes the yamlgraph GAPI struct. @@ -51,8 +97,8 @@ func (obj *GAPI) Init(data gapi.Data) error { if obj.initialized { return fmt.Errorf("already initialized") } - if obj.File == nil { - return fmt.Errorf("the File param must be specified") + if obj.InputURI == "" { + return fmt.Errorf("the InputURI param must be specified") } obj.data = data // store for later obj.closeChan = make(chan struct{}) @@ -64,12 +110,22 @@ func (obj *GAPI) Init(data gapi.Data) error { // Graph returns a current Graph. func (obj *GAPI) Graph() (*pgraph.Graph, error) { if !obj.initialized { - return nil, fmt.Errorf("yamlgraph: GAPI is not initialized") + return nil, fmt.Errorf("%s: GAPI is not initialized", Name) } - config := ParseConfigFromFile(*obj.File) + fs, err := obj.data.World.Fs(obj.InputURI) // open the remote file system + if err != nil { + return nil, errwrap.Wrapf(err, "can't load yaml from file system `%s`", obj.InputURI) + } + + b, err := fs.ReadFile(Start) // read the single file out of it + if err != nil { + return nil, errwrap.Wrapf(err, "can't read yaml from file `%s`", Start) + } + + config := ParseConfigFromFile(b) if config == nil { - return nil, fmt.Errorf("yamlgraph: ParseConfigFromFile returned nil") + return nil, fmt.Errorf("%s: ParseConfigFromFile returned nil", Name) } g, err := config.NewGraphFromConfig(obj.data.Hostname, obj.data.World, obj.data.Noop) @@ -85,7 +141,7 @@ func (obj *GAPI) Next() chan gapi.Next { defer close(ch) // this will run before the obj.wg.Done() if !obj.initialized { next := gapi.Next{ - Err: fmt.Errorf("yamlgraph: GAPI is not initialized"), + Err: fmt.Errorf("%s: GAPI is not initialized", Name), Exit: true, // exit, b/c programming error? } ch <- next @@ -94,12 +150,7 @@ func (obj *GAPI) Next() chan gapi.Next { startChan := make(chan struct{}) // start signal close(startChan) // kick it off! - watchChan, configChan := make(chan error), make(chan error) - if obj.data.NoConfigWatch { - configChan = nil - } else { - configChan = obj.configWatcher.ConfigWatch(*obj.File) // simple - } + watchChan := make(chan error) if obj.data.NoStreamWatch { watchChan = nil } else { @@ -117,15 +168,11 @@ func (obj *GAPI) Next() chan gapi.Next { if !ok { return } - case err, ok = <-configChan: // returns nil events on ok! - if !ok { // the channel closed! - return - } case <-obj.closeChan: return } - log.Printf("yamlgraph: Generating new graph...") + log.Printf("%s: Generating new graph...", Name) next := gapi.Next{ //Exit: true, // TODO: for permanent shutdown! Err: err, @@ -148,7 +195,7 @@ func (obj *GAPI) Next() chan gapi.Next { // Close shuts down the yamlgraph GAPI. func (obj *GAPI) Close() error { if !obj.initialized { - return fmt.Errorf("yamlgraph: GAPI is not initialized") + return fmt.Errorf("%s: GAPI is not initialized", Name) } obj.configWatcher.Close() close(obj.closeChan) diff --git a/yamlgraph/gconfig.go b/yamlgraph/gconfig.go index bc1e77b8..8b9f5f5a 100644 --- a/yamlgraph/gconfig.go +++ b/yamlgraph/gconfig.go @@ -21,7 +21,6 @@ package yamlgraph import ( "errors" "fmt" - "io/ioutil" "log" "reflect" "strings" @@ -35,7 +34,7 @@ import ( type collectorResConfig struct { Kind string `yaml:"kind"` - Pattern string `yaml:"pattern"` // XXX: Not Implemented + Pattern string `yaml:"pattern"` // XXX: not implemented } // Vertex is the data structure of a vertex. @@ -68,7 +67,9 @@ type Resources struct { Nspawn []*resources.NspawnRes `yaml:"nspawn"` Password []*resources.PasswordRes `yaml:"password"` Pkg []*resources.PkgRes `yaml:"pkg"` + Print []*resources.PrintRes `yaml:"print"` Svc []*resources.SvcRes `yaml:"svc"` + Test []*resources.TestRes `yaml:"test"` Timer []*resources.TimerRes `yaml:"timer"` User []*resources.UserRes `yaml:"user"` Virt []*resources.VirtRes `yaml:"virt"` @@ -262,13 +263,7 @@ func (c *GraphConfig) NewGraphFromConfig(hostname string, world resources.World, } // ParseConfigFromFile takes a filename and returns the graph config structure. -func ParseConfigFromFile(filename string) *GraphConfig { - data, err := ioutil.ReadFile(filename) - if err != nil { - log.Printf("Config: Error: ParseConfigFromFile: File: %v", err) - return nil - } - +func ParseConfigFromFile(data []byte) *GraphConfig { var config GraphConfig if err := config.Parse(data); err != nil { log.Printf("Config: Error: ParseConfigFromFile: Parse: %v", err) diff --git a/yamlgraph2/gapi.go b/yamlgraph2/gapi.go index e8f4ac94..3a7cd4a6 100644 --- a/yamlgraph2/gapi.go +++ b/yamlgraph2/gapi.go @@ -25,11 +25,26 @@ import ( "github.com/purpleidea/mgmt/gapi" "github.com/purpleidea/mgmt/pgraph" "github.com/purpleidea/mgmt/recwatch" + "github.com/purpleidea/mgmt/resources" + + errwrap "github.com/pkg/errors" + "github.com/urfave/cli" ) +const ( + // Name is the name of this frontend. + Name = "yaml2" + // Start is the entry point filename that we use. It is arbitrary. + Start = "/start.yaml" +) + +func init() { + gapi.Register(Name, func() gapi.GAPI { return &GAPI{} }) // register +} + // GAPI implements the main yamlgraph GAPI interface. type GAPI struct { - File *string // yaml graph definition to use; nil if undefined + InputURI string // input URI of file system containing yaml graph to use data gapi.Data initialized bool @@ -38,12 +53,43 @@ type GAPI struct { configWatcher *recwatch.ConfigWatcher } -// NewGAPI creates a new yamlgraph GAPI struct and calls Init(). -func NewGAPI(data gapi.Data, file *string) (*GAPI, error) { - obj := &GAPI{ - File: file, +// Cli takes a cli.Context, and returns our GAPI if activated. All arguments +// should take the prefix of the registered name. On activation, if there are +// any validation problems, you should return an error. If this was not +// activated, then you should return a nil GAPI and a nil error. +func (obj *GAPI) Cli(c *cli.Context, fs resources.Fs) (*gapi.Deploy, error) { + if s := c.String(Name); c.IsSet(Name) { + if s == "" { + return nil, fmt.Errorf("input yaml is empty") + } + + // single file input only + if err := gapi.CopyFileToFs(fs, s, Start); err != nil { + return nil, errwrap.Wrapf(err, "can't copy yaml from `%s` to `%s`", s, Start) + } + + return &gapi.Deploy{ + Name: Name, + Noop: c.GlobalBool("noop"), + Sema: c.GlobalInt("sema"), + GAPI: &GAPI{ + InputURI: fs.URI(), + // TODO: add properties here... + }, + }, nil + } + return nil, nil // we weren't activated! +} + +// CliFlags returns a list of flags used by this deploy subcommand. +func (obj *GAPI) CliFlags() []cli.Flag { + return []cli.Flag{ + cli.StringFlag{ + Name: Name, + Value: "", + Usage: "yaml graph definition to run (parser v2)", + }, } - return obj, obj.Init(data) } // Init initializes the yamlgraph GAPI struct. @@ -51,8 +97,8 @@ func (obj *GAPI) Init(data gapi.Data) error { if obj.initialized { return fmt.Errorf("already initialized") } - if obj.File == nil { - return fmt.Errorf("the File param must be specified") + if obj.InputURI == "" { + return fmt.Errorf("the InputURI param must be specified") } obj.data = data // store for later obj.closeChan = make(chan struct{}) @@ -64,12 +110,22 @@ func (obj *GAPI) Init(data gapi.Data) error { // Graph returns a current Graph. func (obj *GAPI) Graph() (*pgraph.Graph, error) { if !obj.initialized { - return nil, fmt.Errorf("yamlgraph: GAPI is not initialized") + return nil, fmt.Errorf("%s: GAPI is not initialized", Name) } - config := ParseConfigFromFile(*obj.File) + fs, err := obj.data.World.Fs(obj.InputURI) // open the remote file system + if err != nil { + return nil, errwrap.Wrapf(err, "can't load yaml from file system `%s`", obj.InputURI) + } + + b, err := fs.ReadFile(Start) // read the single file out of it + if err != nil { + return nil, errwrap.Wrapf(err, "can't read yaml from file `%s`", Start) + } + + config := ParseConfigFromFile(b) if config == nil { - return nil, fmt.Errorf("yamlgraph: ParseConfigFromFile returned nil") + return nil, fmt.Errorf("%s: ParseConfigFromFile returned nil", Name) } g, err := config.NewGraphFromConfig(obj.data.Hostname, obj.data.World, obj.data.Noop) @@ -85,7 +141,7 @@ func (obj *GAPI) Next() chan gapi.Next { defer close(ch) // this will run before the obj.wg.Done() if !obj.initialized { next := gapi.Next{ - Err: fmt.Errorf("yamlgraph: GAPI is not initialized"), + Err: fmt.Errorf("%s: GAPI is not initialized", Name), Exit: true, // exit, b/c programming error? } ch <- next @@ -94,12 +150,7 @@ func (obj *GAPI) Next() chan gapi.Next { startChan := make(chan struct{}) // start signal close(startChan) // kick it off! - watchChan, configChan := make(chan error), make(chan error) - if obj.data.NoConfigWatch { - configChan = nil - } else { - configChan = obj.configWatcher.ConfigWatch(*obj.File) // simple - } + watchChan := make(chan error) if obj.data.NoStreamWatch { watchChan = nil } else { @@ -117,15 +168,11 @@ func (obj *GAPI) Next() chan gapi.Next { if !ok { return } - case err, ok = <-configChan: // returns nil events on ok! - if !ok { // the channel closed! - return - } case <-obj.closeChan: return } - log.Printf("yamlgraph: Generating new graph...") + log.Printf("%s: Generating new graph...", Name) next := gapi.Next{ //Exit: true, // TODO: for permanent shutdown! Err: err, @@ -148,7 +195,7 @@ func (obj *GAPI) Next() chan gapi.Next { // Close shuts down the yamlgraph GAPI. func (obj *GAPI) Close() error { if !obj.initialized { - return fmt.Errorf("yamlgraph: GAPI is not initialized") + return fmt.Errorf("%s: GAPI is not initialized", Name) } obj.configWatcher.Close() close(obj.closeChan) diff --git a/yamlgraph2/gconfig.go b/yamlgraph2/gconfig.go index 7d7092a1..52d1cbca 100644 --- a/yamlgraph2/gconfig.go +++ b/yamlgraph2/gconfig.go @@ -21,7 +21,6 @@ package yamlgraph2 import ( "errors" "fmt" - "io/ioutil" "log" "strings" @@ -34,7 +33,7 @@ import ( type collectorResConfig struct { Kind string `yaml:"kind"` - Pattern string `yaml:"pattern"` // XXX: Not Implemented + Pattern string `yaml:"pattern"` // XXX: not implemented } // Vertex is the data structure of a vertex. @@ -301,13 +300,7 @@ func (c *GraphConfig) NewGraphFromConfig(hostname string, world resources.World, } // ParseConfigFromFile takes a filename and returns the graph config structure. -func ParseConfigFromFile(filename string) *GraphConfig { - data, err := ioutil.ReadFile(filename) - if err != nil { - log.Printf("Config: Error: ParseConfigFromFile: File: %v", err) - return nil - } - +func ParseConfigFromFile(data []byte) *GraphConfig { var config GraphConfig if err := config.Parse(data); err != nil { log.Printf("Config: Error: ParseConfigFromFile: Parse: %v", err)