208 Commits

Author SHA1 Message Date
James Shubin
9c75c55fa4 lib: Update the help text
Give some longer descriptions so they show up nicely for the user.
2021-09-29 02:11:19 -04:00
Joe Groocock
b9741e87bd lang: interpolate: Fix string interpolation of dollar symbols
Dollar symbols were failing to parse when not followed by a non-brace,
non-dollar, non-EOF token and causing expected tests to fail. This
simplifies the rules to allow the remaining tests to succeed.

Fix and reinstate the final few failing tests, and add another.

Allow any escape sequence to be matched so that invalid sequences
produce a meaningful error message instead of a generic "cannot parse":

    ast: interpolate: interpolating: V: \?
    unhandled escape sequence token: \?

Tidy the related Makefile rule for generating the ragel parser.

Signed-off-by: Joe Groocock <me@frebib.net>
2021-09-28 21:40:49 +00:00
James Shubin
c555478b54 engine, lang: Misc fixes for golang lint 2021-08-09 16:55:31 -04:00
Joe Groocock
3718372288 docs: Provide Libera.Chat webchat links over ircs URIs
Only a few days after updating the documentation[1] following the move
to libera.chat, the webchat client was added, mirroring the behaviour of
the documentation prior to the change. Replace the ircs:// links with
clickable URLs to a usable browser chat client, which is more ideal for
beginners. Advanced users will know what to do to connect using their
external client as normal

[1]: 7d7e225823

Signed-off-by: Joe Groocock <me@frebib.net>
2021-07-12 20:57:44 +00:00
James Shubin
390b41bc26 test: Add small test for weird bash spacing 2021-06-21 18:28:05 -04:00
James Shubin
530c5a64fb vendor: Pin version of consul until we're on golang 1.16
The builds broke because the consul dependency now requires golang 1.16,
so let's pin it for now.
2021-06-20 21:53:01 -04:00
Matthew Lesko-Krleza
d285aaedc9 test: Add a test for AddEdge 2021-05-30 21:08:37 -04:00
James Shubin
453fe18d7f lang: Move the Arg type into the common interface package
This lets it get used in multiple places.
2021-05-30 17:59:50 -04:00
James Shubin
5fae5cd308 lang: Fix grammar typos
Woops!
2021-05-30 17:16:49 -04:00
Joe Groocock
7d7e225823 docs: Update IRC links to Libera.Chat
#mgmtconfig has moved to Libera.Chat as the primary channel for IRC
communications. Update the documentation to reflect this.
Libera.Chat doesn't provide a first-party web portal but does recommend
a few in the linked documentation on the website. As there is no
suitable replacement for webchat.freenode.net, link to the "Choosing an
IRC client" page instead.

Signed-off-by: Joe Groocock <me@frebib.net>
2021-05-27 08:50:24 +01:00
viq
19f404799d test: Fix awk usage for printing refs
This should print more fields than just 3rd one if they're present.
2021-05-24 23:49:19 +02:00
James Shubin
3e4652dca3 lang: funcs: Fix structlookup unification bug
We had mapped the field type to a dummy type instead of to T2 the return
type. Fixed now and added some tests.

This broke the unification for the load function lookups.
2021-05-23 22:52:50 -04:00
James Shubin
45b08de874 docs: Update the function guide
Hopefully this makes it easier for new function authors to get going
faster!
2021-05-23 20:21:14 -04:00
James Shubin
310e26dda9 lang: Switch over to the new PolyFunc interface
This isn't perfect yet, but we're trying to do this incrementally, and
merge whatever we can as early as possible.

During this work, I realized that the Simplify method of the exclusive
could probably be improved, and possibly receive a better signature.
This work will have to happen later.
2021-05-23 20:03:10 -04:00
James Shubin
f4eb54b835 lang: funcs: Add more invariants to contains func
This adds even more invariants to contains that I might have missed. It
may be redundant, or it may help. It also adds some tests.
2021-05-23 20:03:10 -04:00
James Shubin
3968c12947 lang: funcs: simplepoly: Support variant's in func definitions
This adds support for variant types in the simple poly definitions. It
is recommended that you avoid using these as much as possible, because
they're a bit harder for the type unification to solve for them. The way
this works is that these functions look at the available input types and
then generate a (recursive) set of invariants which might hold true. It
filters out any impossible ones, which is where this variant matching is
done. It's less likely that you'll get a solution with this mechanism,
but it is possible.
2021-05-23 20:03:10 -04:00
James Shubin
21c97d255f lang: funcs: simple: Check for function signatures
Make sure that we actually get function types here. This is just an
extra safety check.
2021-05-23 20:03:10 -04:00
James Shubin
eb1053607a lang: funcs: simple: Check for variant signatures
This adds a safety check in case someone sneaks in a variant type in the
simple function signature. These might be sneaky to detect, and it's
simpler to catch them right here.

From a design point of view, we might consider actually permitting
these, like we did with the simple poly API, but it's probably better
for them to get implemented in that API instead (if we decide to allow
this long-term) and keep this simple API very simple.
2021-05-23 20:03:10 -04:00
James Shubin
de7198e9dc lang: funcs: Check for functions that haven't been migrated
All polymorphic functions should use the new API, at least until we
either implement a compat wrapper. But it's probably best if we get rid
of the old API as soon as we make all this type unification work
properly.
2021-05-23 20:03:10 -04:00
James Shubin
0f30f47249 lang: funcs: core: world: Add unification to schedule return expr
This adds a sneaky unification between the expression of the function
return value in the unification. I am not entirely sure how often this
will get used, but it could be valuable in the right instance if this
isn't already learned through other sources. I'm fairly confident that
it isn't incorrect, so in the worst case scenario it's redundant
information for the unification solver.

This is being added as a separate commit so that it's obvious how this
type of unification invariant can be applied.
2021-05-23 20:03:10 -04:00
James Shubin
6b2ad8ebc8 lang: funcs: core: world: Add Unify method for schedule function
We should probably add some tests for this function because it once had
type unification ghosts, and while adding this new API method, I somehow
hit some temporary new ghosts that have since been killed.
2021-05-23 20:03:10 -04:00
James Shubin
1f302144ef lang: funcs: core: world: Move schedule func arg names to a const
This is a bit safer and cleaner.
2021-05-23 20:03:10 -04:00
James Shubin
d04c7a6ae4 lang: funcs: Add Unify method for history function
This could use some tests.
2021-05-23 20:03:10 -04:00
James Shubin
9ca2cda8c7 lang: funcs: core: Add more invariants to template func
This adds even more invariants to template that I might have missed. It
may be redundant, or it may help.
2021-05-23 20:03:10 -04:00
James Shubin
1fd06ecbf9 lang: funcs: core: fmt: Add more invariants to printf
This adds even more invariants to print that I might have missed. It may
be redundant, or it may help.
2021-05-23 20:03:10 -04:00
James Shubin
97baad4cb1 lang: funcs: Add Unify method for maplookup function
This also adds a few tests.
2021-05-23 20:03:10 -04:00
James Shubin
fbd93ecf0d lang: funcs: Add Unify method for structlookup function
This also adds a few tests.
2021-05-23 20:03:10 -04:00
James Shubin
e941ccea92 lang: funcs: Add Unify method for the simplepoly API
This is an implementation of the Unify approach for the simplepoly
function API, which wraps the full function API. It is unique in that a
lot of different functions use it, and it is easy to build functions
with it. It needs to use exclusives to represent the different options,
but at least it filters out any that aren't viable.

The Unify implementation here is fairly similar to the patterns in the
template() function.

To improve the filtering, it would be excellent if we could examine the
return type in `solved` somehow (if it is known) and use that to trim
our list of exclusives down even further! The smaller exclusives are,
the faster everything in the solver can run.
2021-05-23 20:03:10 -04:00
James Shubin
d692483bc3 lang: funcs: Add Unify method for operator function
This is an implementation of the Unify approach for the operator
function. It is unique in that it is a wrapper around the simple
operator function API.

To improve the filtering, it would be excellent if we could examine the
return type in `solved` somehow (if it is known) and use that to trim
our list of exclusives down even further! The smaller exclusives are,
the faster everything in the solver can run.
2021-05-23 20:03:10 -04:00
James Shubin
95cfbd0fff lang: funcs: Ensure that Info sig's are invalid if not built yet
In case something in the type unification tries to speculatively call
Info before it's ready to produce a valid sig, make sure we only return
a definitive answer (non-nil, and no variant types) once we've
conclusively finished defining the signature.
2021-05-23 20:03:10 -04:00
James Shubin
b3d1ed9e65 lang: funcs: core: math: Add a fortytwo function
This is mainly meant as a useful test case, but might as well have it be
fun too. As an aside, it taught me a surprising result about the %v verb
in printf, and we'll have to decide if it's an issue we care about.

https://github.com/golang/go/issues/46118

The interesting thing about this method is that it uses the simplepoly
API but has no input args-- only the output types are different. If it
had identical types in the input args, that might also have been
interesting, but it's more rare to have none. Hopefully this exercises
our type unification logic.
2021-05-12 03:30:25 -04:00
Joe Groocock
fe2b8c9fee engine: resources: exec: AutoEdge to User/Group/File
Fixes https://github.com/purpleidea/mgmt/issues/221

Signed-off-by: Joe Groocock <me@frebib.net>
2021-05-11 16:51:02 -04:00
James Shubin
2d7deef4e2 lang: unification: Don't stall the solver over generators
If we have a solution, and all that remains are generators, then feel
free to remove them and win.
2021-05-11 05:23:00 -04:00
James Shubin
b4a70b02e3 lang: funcs: Add Unify method for contains function
This is an implementation of the Unify approach for the contains
function. It is unique in that its generator invariant can recursively
generate a new generator invariant once.
2021-05-11 04:41:32 -04:00
James Shubin
c5c2364ed4 lang: funcs: core: fmt: Add an additional invariant to printf
This adds an invariant for printf that I might have missed. It may be
redundant, or it may help.
2021-05-11 03:23:33 -04:00
James Shubin
efcc4291a3 lang: funcs: core: Add Unify method for template function
This is an implementation of the Unify approach for the template
function.
2021-05-11 03:22:27 -04:00
James Shubin
6ea6ee264d lang: Add new unification rules for functions
This is meant as an incremental step into the new unification. Hopefully
it doesn't break anything and that we can rip out the old polymorphisms
work soon.
2021-05-11 02:52:35 -04:00
James Shubin
2865ba7632 lang: unification: Improve our simple solver
This removed a bug in the InvariantCall stuff, and also hopefully made
it more robust to actually solving when it had a solution.
2021-05-11 02:47:24 -04:00
James Shubin
2bed668d31 lang: interfaces: Small fixups to make unification work for now
This is all hacks until it works. Sorry that I am not a type unification
expert. If you are, please send us some patches =D
2021-05-11 01:31:10 -04:00
James Shubin
9dc24860f3 lang: interfaces: Add a new poly func interface
This new interface is subject to change and will probably be renamed if
we decide to keep it.
2021-05-11 00:45:25 -04:00
James Shubin
f01377b3bc lang: funcs: core: fmt: Add Unify method for printf
This is an implementation of the Unify approach for the printf function.
2021-05-11 00:33:50 -04:00
Joe Groocock
7443dfac4c misc: Run apt update before installing packages
Sometimes the package repo may be out of date and installing required
packages can return 404 because the version in the stale database has
been removed.

Signed-off-by: Joe Groocock <me@frebib.net>
2021-05-09 19:22:40 +01:00
James Shubin
e6408e187c lang: Rename old fail5 and fail6 variables
Last of the numbered error scenario cleanups...
2021-05-08 05:19:36 -04:00
James Shubin
a02d282d3e lang: Rename old fail8 variable to failInterpolate
Eight... Interpolate?

Cleanups...
2021-05-08 04:30:17 -04:00
James Shubin
f778f53744 lang: Rename old fail9 variable to failInit
Cleanups...
2021-05-08 04:25:56 -04:00
James Shubin
95ea93564e lang: Rename old fail4 variable to failGraph
Cleanups...
2021-05-08 04:20:05 -04:00
James Shubin
d51029e86c lang: Rename old fail3 variable to failUnify
Cleanups...
2021-05-08 04:17:21 -04:00
James Shubin
1016699c94 lang: Rename old fail2 variable to failSetScope
Cleanups...
2021-05-08 04:13:33 -04:00
James Shubin
63f63955e7 lang: Replace numbered errors with named ones
This makes the tests easier to read and modify without having out of
order numbers. When writing the tests, you'll remember more easily which
section you're erroring in too!
2021-05-07 23:41:00 -04:00
James Shubin
37be9fda9f lang: Catch duplicate resource fields or meta entries statically
This teaches the compiler to catch entries with duplicate fields, and
duplicate meta entries, because it could be ambiguous to determine which
should take precedence. For example, if you specified `content` to a
file resource twice, this should error. This is known statically, so we
can catch it. If you specified two `Meta:noop` entries, this can also be
caught.

The interesting part happens when you specify one `Meta:noop` entry, and
one `Meta` entry which happens to contain a noop field in the struct.
For this, we actually have to wait until type unification is finished,
and catch the error there. This is because after type unification we
will know the precise type of the struct being passed to `Meta`, and so
we can look at its field names, even if their values aren't yet known
because the graph hasn't run yet.
2021-05-07 23:41:00 -04:00
James Shubin
0756133a7e lang: Add a named error for catching errors in test on Init
This makes it so that we can catch errors that happen in Init. We also
name the errors so that number sequence doesn't matter.
2021-05-07 23:14:49 -04:00
Joe Groocock
83c5ab318b lang: types: Clear map/list types during Into()
Map and list types are now unconditionally initialised during an Into()
call to ensure that the only data within them after the operation is
that added by the Into() function.

Prior to this change, map/list types would likely not be cleared prior
to the data being inserted into them with a few exceptions. Nil
pointers or maps/lists that were not sufficient in capacity would be
reinitialised and used to replace the existing backing data store. In
some cases this wouldn't occur meaning any residual data existing in the
container before the Into() call could persist after the data copy
completes. This behaviour is wildly inconsistent and not ideal in the
vast majority of cases. It should be assumed that the Into() call will
preserve nothing and always produce a consistent and deterministic
output.

Signed-off-by: Joe Groocock <me@frebib.net>
2021-05-05 10:41:48 +01:00
Joe Groocock
0c28957016 lang: funcs: Funcs that never load are fatal
If there is a programming error in any func Stream() implementation then
the node could never output anything, causing the engine to hang
indefinitely waiting for an initial value that will never come,

Nodes keep track of whether they are loaded, so testing for this
occurence is pretty simple. Any nodes that do not return output at least
once before they close their output channel can be considered a fatal
error on which the engine will exit.

Signed-off-by: Joe Groocock <me@frebib.net>
2021-05-04 11:39:34 -04:00
James Shubin
959084040d lang: Don't block the engine for empty values
If the user passed an empty list or map, we should send that and not
block. This also includes a simple test to ensure this keeps working.
2021-05-04 11:27:58 -04:00
James Shubin
8a428c6936 lang: Add a test timeout to catch blocked cases
This should catch any blocked tests and report an error. The timeout is
arbitrary.
2021-05-04 11:26:17 -04:00
Joe Groocock
48da23226c project: Add frebib to AUTHORS
Signed-off-by: Joe Groocock <me@frebib.net>
2021-05-04 14:16:54 +01:00
James Shubin
5f0c6e5102 lang: types: Add extra ValueOf test
Just to double check weird behaviours of the golang reflect lib.
2021-05-04 06:26:33 -04:00
Joe Groocock
29f1c6f50e lang: types: Fix ValueOf() panic with nil pointer values
Some forms of reflect.Value can cause ValueOf() to panic when there is a
nil pointer somewhere within the reflect.Value, whether that be a
container type like a struct, list or map, or just a raw nil pointer.

In these cases, ValueOf() attempted to dereference the pointer without
ever checking if it was nil. mgmt lang doesn't have pointers of any
kind, so these Golang values cannot be represented in mcl types in the
current form so return a helpful error to the user.

Signed-off-by: Joe Groocock <me@frebib.net>
2021-05-04 06:26:23 -04:00
James Shubin
4d187419ac engine: Small typo/cleanups in autogrouping code 2021-05-04 05:30:27 -04:00
James Shubin
58998f9cab engine: Transform the send/recv init functions into helpers
Since we'll want to use them elsewhere, we should make these helper
functions. It also makes the code look a lot neater. Unfortunately, it
adds a bit more indirection, but this isn't a critical flaw here.
2021-05-04 05:30:27 -04:00
James Shubin
cdc5ca8854 util: Add a simple log wrapper for io.Writer
This lets logger interfaces that use io.Writer get met by our logging
interface.
2021-05-04 05:27:07 -04:00
James Shubin
44e1e41266 lang: types: Improve documentation for ValueOf functions
A reminder that nil's in golang don't map to anything in mcl.
2021-05-04 05:27:07 -04:00
James Shubin
33fda8605a lang: types: Add new ValueOf tests
Hopefully this makes all of this a bit more obvious.
2021-05-04 04:27:33 -04:00
Joe Groocock
5f9ed69299 misc: Replace go-bindata with maintained fork
As per [1] go-bindata was removed from GitHub and later replaced by the
community. jteeuwen/go-bindata has since been archived to represent this
state and now most communities use kevinburke/go-bindata instead as it
is more actively maintained.

[1]: https://github.com/jteeuwen/go-bindata/issues/5

Signed-off-by: Joe Groocock <me@frebib.net>
2021-05-04 04:10:05 -04:00
Joe Groocock
7f1baea3b0 engine: resources: docker: Replace deprecated NewClient() with NewClientWithOpts()
docker/client.NewClient() is deprecated in favour of NewClientWithOpts()
which takes a series of client.Opt functions to configure the API
client. As mgmt only passes the API version through, this simplifies the
NewClient() calls.

Signed-off-by: Joe Groocock <me@frebib.net>
2021-05-02 10:39:04 +01:00
James Shubin
f75026e4b2 lang: unification: Teach the solver about new invariants
This extends our simple solver so that it can understand the new
invariants. For the Value holding invariants, it doesn't do much-- those
are expected to be used within the execution of the GeneratorInvariant
so they are passed through untouched. For the GeneratorInvariant it must
actually try running this periodically to see if it produces new
invariants that are helpful for the whole solution. Since this could get
expensive quickly, the logic is to only try running these once we've
entered steady state, but before we've tried to reach for the
ExclusiveInvariant's. The exclusives are the most expensive so we run
these last, and the generators are run late because they won't usually
produce anything helpful unless some of the basic solving has already
happened. If they could produce useful things right away, then there
wouldn't be a need for them!
2021-05-02 00:52:57 -04:00
James Shubin
ce7a1a9c67 lang: Add a CallFuncArgsValueInvariant invariant
This is a new invariant that I realized might be useful. It's not
guaranteed that it will be used or useful, but I wanted to get it out of
my WIP branch, to keep that work cleaner.
2021-05-02 00:52:57 -04:00
James Shubin
a62056fb19 lang: Add a GeneratorInvariant invariant
This is a new invariant that I realized might be useful. It's not
guaranteed that it will be used or useful, but I wanted to get it out of
my WIP branch, to keep that work cleaner.
2021-05-02 00:52:57 -04:00
James Shubin
f3434a8155 lang: Add a ValueInvariant invariant
This is a new invariant that I realized might be useful. It's not
guaranteed that it will be used or useful, but I wanted to get it out of
my WIP branch, to keep that work cleaner.
2021-05-02 00:52:57 -04:00
James Shubin
4e023ef517 lang: Move the ExprAny to the interfaces package
Having this special "placeholder" interface is useful for more than one
package.
2021-05-02 00:52:57 -04:00
James Shubin
97b80cb930 lang: unification: Move the InvariantSolution struct
We are just relocating this in the file for consistency.
2021-05-02 00:52:57 -04:00
James Shubin
525b4e6a53 lang: Move core unification structs into shared interfaces package
We should probably move these into the central interfaces package so
that these can be used from multiple places. They don't have any
dependencies, and it doesn't make sense to have the solver code mixed in
to the same package. Overall the interface being implemented here could
probably be improved, but that's a project for another day.
2021-05-02 00:52:57 -04:00
James Shubin
054eaf65b8 util: safepath: Add a new safe path helper library
This is a new path manipulation library that is designed to be safer
than using simple strings for everything. It is more work to use, but it
can help you keep track of the different path types.

It has been sitting unused in a git branch for too long, and I figured
it should see the light of day in case someone wants to start using it.
2021-05-02 00:52:57 -04:00
James Shubin
48fa796ab1 test: Disable failing test
Hit another intermittent failed test in GH CI.
2021-03-10 03:36:27 -05:00
Jean-Philippe Evrard
1873e022cc test: Add guard when no commit needs testing
Without this patch, github actions fail.

It's a temporary workaround until [1] is done.

[1]: https://github.com/purpleidea/mgmt/issues/643
2021-03-03 10:49:22 +01:00
James Shubin
35a8062b58 test: Disable travis IRC notifications for now
One of our contributors is unusually annoyed by them, and it's important
to keep your contributors happy!
2021-03-03 04:28:53 -05:00
Jean-Philippe Evrard
636248ad67 test: Ensure branches are also testable
test-commit-message runs on PR, but also on push in other branches
which aren't PRs. We need to test those too.

This is fixed by ensuring the same kind of behaviour than travis CI:
When a patch is put on a branch, it's using the branchname for
testing [1].

[1]:
https://docs-staging.travis-ci.com/user/environment-variables/#default-environment-variables
2021-03-03 09:52:49 +01:00
Jean-Philippe Evrard
4511c54fad test: Ensure github CI tests commit messages
Without this patch, the travis var is empty, and we just pass.
This is a problem, as we are using github CI nowadays.

This should fix it.
2021-03-03 02:47:30 -05:00
James Shubin
7f3970541b test: Skip more tests
I think some of these fail due to shared environments and noisy
neighbours in github. We'll have to fix that eventually or test
elsewhere.
2021-03-02 13:41:00 -05:00
James Shubin
4040f4d151 test: Skip yet another intermittent test
We shall not have intermittent tests!
2021-03-02 12:48:44 -05:00
James Shubin
887d374c53 lang: funcs: Catch simple function api usage without types
In case a programmer makes a mistake and passes in a function using the
simple function API without a type or even without the entire value,
we'll now return a sensible error message and panic in init() instead of
requiring a test to catch this alone.
2021-02-28 22:50:51 -05:00
James Shubin
be4b87155d test: Skip another intermittent test
I think this might be related to multiple jobs running at the same time
on the same host. Not sure though.
2021-02-24 04:00:34 -05:00
James Shubin
b987a7da4c examples: tftp: Fix missing error checking in example 2021-02-20 13:31:45 -05:00
James Shubin
7153fe5ad2 test: Skip intermittent tests
It would be great to fix some rare races or debug what's wrong in CI,
but for now let's get rid of these fails so that we can get better data
for when we break something more serious. We'll need to revisit all of
this for sure.
2021-02-19 21:17:57 -05:00
James Shubin
ccd8ba44d9 test: Exclude generated ragel parser from golint 2021-02-17 22:01:34 -05:00
James Shubin
e7ef0f7a6c test: Set default column size if $TERM env var isn't set
Seems Github actions breaks or unsets this, leading to the errors:

tput: No value for $TERM and no -T specified
seq: missing operand
Try 'seq --help' for more information.

Hopefully this makes things a bit more robust.
2021-02-17 04:07:30 -05:00
James Shubin
400b58c0e9 lang: Improve string interpolation
The original string interpolation was based on hil which didn't allow
proper escaping, since they used a different escape pattern. Secondly,
the golang Unquote function didn't deal with the variable substitution,
which meant it had to be performed in a second step.

Most importantly, because we did this partial job in Unquote (the fact
that is strips the leading and trailing quotes tricked me into thinking
I was done with interpolation!) it was impossible to remedy the
remaining parts in a second pass with hil. Both operations needs to be
done in a single step. This is logical when you aren't tunnel visioned.

This patch replaces both of these so that string interpolation works
properly. This removes the ability to allow inline function calls in a
string, however this was an incidental feature, and it's not clear that
having it is a good idea. It also requires you wrap the var name with
curly braces. (They are not optional.)

This comes with a load of tests, but I think I got some of it wrong,
since I'm quite new at ragel. If you find something, please say so =D In
any case, this is much better than the original hil implementation, and
easy for a new contributor to patch to make the necessary fixes.
2021-02-17 03:35:12 -05:00
James Shubin
5257496214 test: Make a few cosmetic changes and enable race testing 2021-02-17 02:45:47 -05:00
Jean-Philippe Evrard
e1bfe4a3ce test: Add GitHub Actions test support
Authored-By: Jean-Philippe Evrard <open-source@a.spamming.party>
Co-Authored-By: James Shubin <james@shubin.ca>
Signed-off-by: Joe Groocock <me@frebib.net>
2021-02-14 21:41:02 -05:00
James Shubin
f31cce8ec2 test: misc: Clarify golang wording 2021-02-14 21:39:01 -05:00
Joe Groocock
169ebfa72c test: make-deps: Add folds around tests and dep blocks
Improves readability of CI test output and hides away the complexity
when in most cases it is not required. Retain fold behaviour for both
Travis and GitHub Actions in case both are used in any capacity.

Signed-off-by: Joe Groocock <me@frebib.net>
2021-02-12 16:44:12 +00:00
Joe Groocock
7cace52ab5 test: prevent LinuxBrew in GitHub Actions CI
Ubuntu-latest in GitHub Actions provides linuxbrew, so the tests install
both the native Debian dependency packages, and also the linuxbrew
variants which is slower and entirely redundant.

Signed-off-by: Joe Groocock <me@frebib.net>
2021-02-12 16:44:12 +00:00
Joe Groocock
95b93c60d9 test: Invert negative bash assertions
In bash `-n` is `non zero length` which is the opposite of `-z` meaning
`zero length`. `-n` is semantically identical to `! -z` but `-n`.

Signed-off-by: Joe Groocock <me@frebib.net>
2021-02-12 16:44:12 +00:00
Joe Groocock
5af1dcb8b1 test: Add in_ci utility test function
in_ci checks for environment variables set by a selection of CI systems
and returns true if the test appears to be running in CI. Additionally
it can test for specific CI systems, and returns true if the CI system
is listed.

Deduplicate existing environment checks for Travis and Jenkins.

Signed-off-by: Joe Groocock <me@frebib.net>
2021-02-12 16:44:11 +00:00
Joe Groocock
6a61774fb7 docker: Bump docker dependencies, add containerd
These dependencies are maintained because the upstream repos bundle
vendor directories into the repos, which cause namespacing issues during
build. Git submodules don't strip the vendor directory whereas most
vendoring tools would.

Signed-off-by: Joe Groocock <me@frebib.net>
2021-02-09 21:14:34 +00:00
James Shubin
ccbaca24f1 gopath: Remove this unused directory
I had this symlink hack a long time ago. I don't think it's being used
anymore.
2021-02-07 21:23:49 -05:00
Joe Groocock
07b6048dc5 etcd: Bump etcd + friends to the latest upstream version
This allows dropping the pinned grpc-prometheus and grpc-gateway
libraries as git master works fine for now.

Signed-off-by: Joe Groocock <me@frebib.net>
2021-02-07 12:55:02 +00:00
Joe Groocock
60dd34d066 make: Drop support for Go 1.9 in make build
docs/development.md says the minimum required Golang version is 1.13 at
the time of writing.

Signed-off-by: Joe Groocock <me@frebib.net>
2021-02-07 00:08:51 -05:00
James Shubin
28451d1e14 lang: funcs: Fixup race in vumeter example
The vumeter example was written quickly and without much care. This
fixes a possible race (panic) and also removes the busy loop that wastes
CPU while we're waiting for the first value to come in.
2021-02-06 23:59:06 -05:00
Joe Groocock
db95b6381f examples: lang: Reinstate mcl as unification bug is fixed
Struct field names now correctly map based on their `lang` tags in Go
structs, so this example now works as originally intended.

Signed-off-by: Joe Groocock <me@frebib.net>
2021-02-06 16:57:01 +00:00
Joe Groocock
6b14c9bea4 lang: Map Go struct fields using lang struct tag
Converting a reflect.Type of KindStruct did not respect the `lang` tag
on struct fields incidating how fields from mcl structs should be mapped
even though resource field names did. This patch should allow structs
with mapped fields to be respected.

Signed-off-by: Joe Groocock <me@frebib.net>
2021-02-06 16:57:01 +00:00
Joe Groocock
742adc00fe lang: Convert StmtRes to engine.Res with types.Into()
Replace existing field-mapping code with calls to types.Into() to
reflect the mcl data into the Go resource struct with finer granularity
and accuracy, and less reliance on the magic reflect.Set() function.

One major advantage over reflect.Value.Set() is Into() allows tailoring
the data that is set, specifically when coercing mcl struct values into
Golang struct values, the fields can be appropriately mapped based on
the lang tag on the struct field. With reflect.Value.Set() this was not
at all possible as there was a contradiction of logic given the
following rules:

- mcl struct fields must have lowercase names
- Golang struct fields with lowercase names are unexported
- Golang reflection does not allow modifying unexported fields

Prior to this change, it was impossible to map an mcl inline struct in a
resource to the matched Golang counterpart, even if the lang tag was
present on the struct field. This can be demonstrated with the following
trivial example:

    test "name" {
        key => struct{
            name => "hello",
        },
    }

and the accompanying Golang resource definition:

    type TestRes struct {
        traits.Base
        traits.Edgeable

        Key struct {
            Name string `lang:"name"`
        } `lang:"key"`
    }

Due to the mismatch in field names in the embedded struct, the type
unifier failed and refused to match mcl 'name' to Go 'Name' due to the
missing mapping logic.

Signed-off-by: Joe Groocock <me@frebib.net>
2021-02-06 16:57:00 +00:00
Joe Groocock
52897cc16c lang: Implement Into() to set types.Value from reflect.Value
Into() mutates a given reflect.Value and sets the data represented by
the types.Value into the storage represented by the reflect.Value.

Into() is the opposite to ValueOf() which converts reflect.Value into
types.Value, and in theory they should be (almost) bijective with some
edge case exceptions where the conversion is lossy.

Simply, it replaces reflect.Value.Set() in the broad case, giving finer
control of how the reflect.Value is modified and how the data is set.
types.Value.Value() is now also a redundant function that achieves the
same outcome as Into(), but with less type specificity.

Signed-off-by: Joe Groocock <me@frebib.net>
2021-02-06 16:57:00 +00:00
Joe Groocock
c950568f1b lang: Move StructTag const into lang/types
This constant value is strongly tied to the language, and little to do
with the engine. Move the definition into the lang/types package to
prevent circular imports between lang/types and engine/util.

Signed-off-by: Joe Groocock <me@frebib.net>
2021-02-06 16:56:57 +00:00
Joe Groocock
845d7ff188 lang: Fix panic in lang/types/ValueOf() for Struct
Replace use of reflect.Value.Len() with NumField() which is intended to
return the number of fields in reflected Struct value.

Len should only be used for Array, Chan, Map, Slice and String types.

Add some trivial sanity check tests for ValueOf() for the simple and
complex container types.

Signed-off-by: Joe Groocock <me@frebib.net>
2021-01-31 23:10:16 +00:00
James Shubin
3bd8658da6 art: Use a transparent logo for dark themes
As helpful user frebib pointed out, our logo doesn't render nicely when
github users use the dark theme. This has already been complained about
by others in:

https://github.community/t/support-theme-context-for-images-in-light-vs-dark-mode/147981/2

For now, we'll switch to a transparent background.
2021-01-31 18:00:47 -05:00
James Shubin
336a38081a legal: Happy 2021 everyone...
Done with:

ack '2020+' -l | xargs sed -i -e 's/2020+/2021+/g'

Checked manually with:

git add -p

Hello to future James from 2022, and Happy Hacking!
2021-01-31 16:52:46 -05:00
Jean-Philippe Evrard
01c2131436 misc: Make-deps should assume go vet present
While the code comment says to check if go vet command is present,
it actually tests if go vet command returns properly.

This is a problem if go vet is _not_ returning 0 due to a
failure while go vet is present: it will try to install the
legacy go vet.

This fixes it by removing this block of code completely,
as we assume a golang version which contains it anyway.
2020-12-09 19:25:19 -05:00
Joe Groocock
c274231544 test: Fix implicit test fail in test-markdownlint
Fix the silent test failure by catching the uncaught error from
`command`, handling the failure gracefully.

    $ bash -x test/test-markdownlint.sh
    ...
    ++ command -v mdl
    + MDL=
    $ echo $?
    1

    $ bash -x test/test-markdownlint.sh
    ...
    ++ command -v mdl
    + MDL=
    + true
    + '[' -z '' ']'
    + fail_test 'The '\''mdl'\'' utility can'\''t be found.'
    + echo -e 'FAIL: The '\''mdl'\'' utility can'\''t be found.'
    FAIL: The 'mdl' utility can't be found.

Fix a couple of glaring shellcheck warnings and errors mostly
surrounding variable quoting.

Signed-off-by: Joe Groocock <me@frebib.net>
2020-12-08 15:51:52 +00:00
Joe Groocock
4a2864701c lang: Assert that 'metadata.yaml' is not parsed as raw mcl
Contradictory to expectations, `mgmt run lang metadata.yaml` would
produce an error similar to the following, which is likely unexpected
from the user perspective:

    2020-11-24 12:24:08.330968 I | cli: lang: lexing/parsing...
    2020-11-24 12:24:08.331153 I | run: error: cli parse error: could not generate AST: parser: `syntax error: unexpected DOT` @1:1

Produce a user-friendly warning instead, hinting with the supported
usage:

    2020-11-24 13:15:01.686659 I | run: error: cli parse error: could not activate an input parser: unexpected raw code 'metadata.yaml'. Did you mean './metadata.yaml'?

Signed-off-by: Joe Groocock <me@frebib.net>
2020-11-24 13:56:08 +00:00
James Shubin
76ede10e0a misc: Update path to go-fuzz
They moved it :/
2020-09-23 13:42:51 -04:00
Ahmed Al-Hulaibi
274e01bb75 misc, docs: Update minimum required golang version to 1.13 2020-09-23 12:34:54 -04:00
James Shubin
d75f763c99 misc, docs: Move to golang 1.12 2020-09-23 12:34:54 -04:00
Donald Bakong
5bc985663c docs: Add underscore issue to FAQ 2020-09-23 11:25:46 -04:00
James Shubin
df9e2e853f docs: Update state of remote execution and resource 2020-09-23 11:21:03 -04:00
Ivan Pejić
b4828a6f0a docs: Update FAQ to mention temp absence of remote 2020-09-23 11:15:44 -04:00
Donald Bakong
e99dd749a0 engine: resources: Fixing return bug 2020-09-23 11:10:38 -04:00
David Randall
10ce7178c0 misc: Incorrect dependency path for dvyukov/go-fuzz
Fix the dependency path for dvyukov/go-fuzz to github.com from golang.org.

Fixes #601
2020-06-07 15:05:46 -04:00
Donald Bakong
5c6a66eaf5 lang: funcs: Add cidr_to_ip function 2020-06-03 22:01:01 -04:00
Francois Rompre-Lanctot
36d30bc985 lang: funcs: Add macfmt function 2020-06-03 17:46:39 -04:00
Adam Sigal
a5152b82e9 engine: resources: exec: Add Env
Add functionality to specify environment variables in exec.
2020-04-19 20:30:43 -06:00
James Shubin
e9af8a2595 engine: resources: exec: Clean up error handling
Some quick fixes, this whole resource should be looked at for
discrepancies, since it was written very early.
2020-04-14 22:59:33 -04:00
Adam Sigal
84b5b60d49 engine: resources: exec: Fix typo
Typo in description of cwd field fixed.
2020-04-14 22:36:25 -04:00
James Shubin
8f60f42be3 engine: resources: Add http:server and http:file resources
This adds a new http server resource, as well as a http file resource
that is used to specify files to serve in that server. This allows you
to have an http server that is entirely server from memory without
needing files on disk.

It does this by using the autogrouping magic that is already available
in the engine.

The http resource is not meant to be a full-featured http server
replacement, and it might still be useful to use the venerable webserver
of your choice, however for integrated, pure-golang bootstrapping
environments, this might prove to be very useful.

It can be combined with the tftp and dhcp resources to build PXE setups
with mgmt!

This resource can be extended further to support an http:flag endpoint,
an http:ui endpoint, automatic edges, and more!
2020-04-11 03:23:04 -04:00
James Shubin
583344138a engine: resources: Add dhcp:server and dhcp:host resources
This adds a new dhcp server resource, as well as a dhcp host resource
used to specify the static mapping between mac address and ip address.
It also adds a simple, pure-golang example dhcp client which might make
testing easier.

The dhcp resource is not meant to be a full-featured dhcp server
replacement, and it might still be useful to use the venerable dhcpd,
however for integrated, pure-golang bootstrapping environments, this
might prove to be very useful.

It can be combined with the tftp resource to build PXE setups with mgmt.

This resource can be extended further to support a dhcp:range directive,
automatic edges, and more!
2020-04-11 02:45:18 -04:00
James Shubin
016d021d5a engine: resources: tftp: Improve validation and error messages
Just some small cleanups for our tftp resource. We also rename the
struct to make it consistent, since golint complains about similar
protocols when it is not all capitalized.
2020-04-11 02:45:18 -04:00
James Shubin
115dc4bfa4 engine: resources: net: Add an IP forward field
This adds an IP forward field (boolean) and improves the documentation.
2020-04-11 02:02:35 -04:00
James Shubin
5b83febb23 etcd: Log an error that wasn't getting seen
We would error when the address could not bind, but it was for
non-standard reasons, and we didn't see the specific reason why.
2020-04-11 02:02:35 -04:00
James Shubin
c9d5c50402 test: Improve our test for long lines
This now allows long URL's to start part way through a sentence instead
of requiring them to start on the beginning of a new line.
2020-04-03 01:15:57 -04:00
James Shubin
fc839d2983 vendor: Fix broken upstream hashicorp module
This module broke compat with old versions of golang. Vendor it until
we're at a minimum of golang 1.13.x everywhere.
2020-04-02 22:27:06 -04:00
James Shubin
3bce96bbd5 docs: Add new talk from cfgmgmtcamp 2020
Those first ten seconds of the video are awesome!
2020-02-29 19:40:26 -05:00
Patrick Meyer
6279be073b lang: Prevent struct types with duplicate field names
The previous fix for #591 in 70eecd5 didn't address all issues
concerning duplicate struct field names. It still crashed for inputs
like `$d []struct{x int; x float}`. Note the different types but
duplicate names.
2020-02-29 19:11:51 -05:00
Patrick Meyer
ea37132ce4 lang: Fix wrong go-fuzz bin name
I missed renaming this after moving the fuzz.go from the lang package
to its own fuzz package.
2020-02-29 05:28:38 +01:00
James Shubin
70eecd5289 lang: Prevent struct types with duplicate fields
Struct types with duplicate fields are invalid types and weren't caught
by the parser. This fixes the issue and adds some associated tests. It
also checks and tests for duplicate struct value field names.

As a technical side-note, this doesn't change the lang/types/ functions
to remove panics-- the signatures are simplified to make their use
simple, and we intentionally panic if they're used incorrectly. In this
case, one was being used without having previously validated the input.

Thanks to Patrick Meyer for finding this issue via fuzzing!
2020-02-27 18:52:02 -05:00
James Shubin
380d03257f misc, lang: Small fixups
Change some minor style issues.
2020-02-27 17:34:03 -05:00
Patrick Meyer
006de6da14 lang: Add fuzz target to lang Makefile 2020-02-26 12:07:00 +01:00
Kenneth Hoste
10aa80e8f5 docs: Add link to recording of James' FOSDEM 2020 talks 2020-02-17 14:23:20 -05:00
Felix Frank
013439af6d test: Make prometheus tests safer and more verbose 2020-02-16 18:38:58 -05:00
Felix Frank
3408961155 test: Fix syntax in the loadavg test 2020-02-16 18:38:43 -05:00
Francois Rompre-Lanctot
f3b4a8d055 engine: resources: Add a test case for resource owner check
This adds FileOwnerExpect as a new Step which allows validating if the
owner was set properly on a resource.
2020-02-10 22:17:42 -05:00
Francois Rompre-Lanctot
104af7e86f engine: resources: Fix typo 2020-02-03 22:57:13 -05:00
James Shubin
be39fbeff6 examples: lang: Update examples 2020-02-01 16:48:23 -05:00
James Shubin
4109045fa4 github: Add new needinfo tag 2020-01-29 11:38:53 -05:00
James Shubin
90fd8023dd lang, engine: Add a facility for resources to export constants
Since we focus on safety, it would be nice to reduce the chance of any
runtime errors if we made a typo for a resource parameter. With this
patch, each resource can export constants into the global namespace so
that typos would cause a compile error.

Of course in the future if we had a more advanced type system, then we
could support precise types for each individual resource param, but in
an attempt to keep things simple, we'll leave that for another day. It
would add complexity too if we ever wanted to store a parameter
externally.

Lastly, we might consider adding "special case" parsing so that directly
specified fields would parse intelligently. For example, we could allow:

	file "/tmp/hello" {
		state => exists,	# magic sugar!
	}

This isn't supported for now, but if it works after all the other parser
changes have been made, it might be something to consider.
2020-01-29 11:16:04 -05:00
James Shubin
f67ad9c061 test: Add a check for too long or badly reflowed docstrings
This ensures that docstring comments are wrapped to 80 chars. ffrank
seemed to be making this mistake far too often, and it's a silly thing
to look for manually. As it turns out, I've made it too, as have many
others. Now we have a test that checks for most cases. There are still a
few stray cases that aren't checked automatically, but this can be
improved upon if someone is motivated to do so.

Before anyone complains about the 80 character limit: this only checks
docstring comments, not source code length or inline source code
comments. There's no excuse for having docstrings that are badly
reflowed or over 80 chars, particularly if you have an automated test.
2020-01-25 04:43:33 -05:00
James Shubin
525e2bafee misc: Let sigtee work on older golang versions 2020-01-25 04:43:33 -05:00
James Shubin
b65a9abf8e misc: Add two scripts to help debug things
This adds two new helper scripts that are good for debugging mgmt. Just
build and add `sigtee` to your ~/bin/ along with filter-golang-stack.py
and mgmt_debug.sh and use the later to call mgmt as you would normally.

For example, I might do:

$ mgmt_debug.sh ./mgmt run --tmp-prefix lang examples/lang/hello0.mcl

And if I kill it with ^\ then I'll get a filtered trace at the end in my
$PAGER (which is assumed to be `less`) and this should make my life
easier.

As a cool bonus, this means we use bash, python, and golang all
together!
2020-01-13 01:07:00 -05:00
James Shubin
fec94aa53a engine, lang: Fix simple test failures
Two bugs sneaked in while pushing old stuff.
2020-01-12 19:35:11 -05:00
James Shubin
3d4b345728 examples: lang: Improve reverse example
It's cool to show just the mode changes.
2020-01-12 17:42:20 -05:00
James Shubin
579975f08d engine: graph: Don't error when state file is missing
For some reason we get errors when we try to remove a non-existent state
file. There's a slight possibility that it could be a bug we're working
around, but it's not clear that this is the case, and I think it's
possible that a state file could have gotten nuked by the user somehow,
although this was occurring "naturally" when running reverse1.mcl so
let's keep that working for now.
2020-01-12 16:41:09 -05:00
James Shubin
3707b39fef engine: graph: Improve comments
Clarify that we're referring to cycles in the graph, since it needs to
be a DAG.
2020-01-12 16:39:32 -05:00
James Shubin
f07387225b engine: resources: Log more info about tftp errors
This helps for debugging this kind of issue:
https://github.com/pin/tftp/issues/41#issuecomment-570744056
2020-01-03 20:42:34 -05:00
James Shubin
2648fb1bb1 legal: Happy 2020 everyone...
Done with:

ack '2019+' -l | xargs sed -i -e 's/2019+/2020+/g'

Checked manually with:

git add -p

Hello to future James from 2021, and Happy Hacking!
2020-01-03 20:08:37 -05:00
James Shubin
d34715b4ba engine: resources: pippet: Cleanup and proper wrapping
Felix, please configure your editor to wrap at 80 chars and/or help us
write a test for this please =D
2020-01-03 01:20:00 -05:00
Felix Frank
63af50bf98 engine: resources: pippet: Initial implementation for new resource type
The pippet resource implements faster integration of Puppet resources
in mgmt at runtime, by piping synchronization commands to a Puppet
process that keeps running alongside mgmt. This avoids huge overhead
through launching a Puppet process for each operation on a resource
that is delegated to Puppet.
2020-01-03 01:19:37 -05:00
James Shubin
456550c1d4 engine: resources: docker: Make a few fixups
Here are a few fixups to the docker resources. All miscellaneous stuff,
nothing major.
2020-01-03 00:53:20 -05:00
Jonathan Gold
8174b88ec3 engine: resources: docker: Add sensible defaults 2020-01-03 00:30:01 -05:00
Jonathan Gold
3233973748 engine: resources: docker: Add AutoEdges between container and image 2020-01-03 00:30:01 -05:00
Jonathan Gold
bdfb1cf33e engine: resources: docker: Ensure image is specified for containers 2020-01-03 00:30:01 -05:00
Jonathan Gold
1c5fcd59e7 engine: resources: docker: Add a docker image resource 2020-01-03 00:30:01 -05:00
James Shubin
5cc960527e lang: funcs: Differentiate between empty and nil values
It would be good to differentiate between receiving an empty value or
not having received a value yet. This is similar to the previous commit.
2020-01-03 00:28:54 -05:00
James Shubin
762c53fb8d lang: funcs: Send empty values when appropriate
I seem to have forgotten to differentiate between the empty string and
no data because the zero value for the stored result was the empty
string. This turns it into a pointer so that we don't block the function
engine if a template or one of the other patched functions sends an
empty string as the first value.
2019-12-30 12:35:08 -05:00
James Shubin
ff20e67d07 lang: funcs: Improve template function
Template function should be able to be called with just one arg (no
input vars) and we should correctly use the known arg name and not the
string "a" or "b".
2019-12-30 11:21:43 -05:00
Francois Rompre-Lanctot
c0cea013d1 pgraph: Add test for SetValue function 2019-12-18 20:00:47 -05:00
James Shubin
5526bbba64 engine: resources: Add a tftp server and tftp file resource
This adds a tftp server and tftp file resource to help you run a small
pure golang tftp server embedded inside the mgmt resource model.
2019-12-17 03:41:45 -05:00
James Shubin
f0aa96ea8c etcd: Remove the capnslog stuff and switch to zap
Unfortunately, this doesn't give us a way to pass in our own logger
function, and afaict by reading the source, it's not possible because
the necessary methods are private. In any case, this is left as a future
exercise.
2019-12-17 03:40:44 -05:00
James Shubin
e73007c398 etcd: Bump to new 3.4.x version
This moves to the newest etcd release, and also updates the imports to
the new go.etcd.io path. I think this is a bit of a pain, but might as
well get it done.
2019-12-17 02:45:38 -05:00
Jonathan Gold
fdc459ec5b vagrant: Update Vagrantfile
This patch updates the Vagrantfile to Fedora 31, and updates the
install process to match the quick start guide.
2019-11-17 20:25:06 -05:00
Felix Frank
bdb523ece1 lang: funcs: funcgen: Suppress informational messages
Send non-error log messages to stdout rather than stderr. Any messages
outside the main function are expected to be purely informational. By
sending to stdout rather than stderr, they can be discarded during the
build.

Fixes #568
2019-11-17 20:21:52 -05:00
James Shubin
164a9479ad test: Add a new test to the commit message checking
Fix the missing "s" bug.
2019-11-17 20:21:52 -05:00
James Shubin
e18adc781f git: Add a simple gitignore helper
Run `touch 1 && chmod ugo-w 1` to prevent silly scripting bugs from
running without being caught.
2019-11-12 17:33:58 -05:00
Felix Frank
33d89c2739 examples: lib: Update code for urfave/cli v2 2019-11-12 21:07:38 +01:00
Felix Frank
7cc9ab9083 lib: Update for urfave/cli v2 2019-11-12 21:07:38 +01:00
Donald Bakong
4b4b7dc169 engine: resources: Adding tests to file mode 2019-11-08 10:02:30 -05:00
Yohan Belval
71ad5c5f05 examples: lang: Added os check for pkg example fix 2019-11-06 10:20:32 -05:00
Yohan Belval
39368bb5cb examples: lang: Fixed pkg example with cowsay 2019-11-06 10:03:20 -05:00
James Shubin
7a587ee8d1 misc: mkosi: Switch to copy-git-more
I added a new feature in mkosi which got merged in:

31801e89e77188e697ed937ca6b8668fde4c4a4d

This allows us to pull in all of the git repository so that we can see
the version number that comes from git.
2019-11-02 07:34:04 -04:00
James Shubin
77346527f3 docs: Update style guide with more review items
Hopefully this helps new contributors understand review changes and
avoid making them too!
2019-11-01 22:01:38 -04:00
James Shubin
1eba5833d5 engine: resources: Consistency changes and cleanup for file mode
This makes a few consistency changes and cleanups to the file mode
feature so that it's more in style with the rest of the code base.
2019-11-01 22:01:38 -04:00
Derek Buckley
83a747794e engine: resources: Adds symbolic mode to file resource
Adds a symbolic parsing function to the util package for parsing in the
file resource.
2019-11-01 21:57:10 -04:00
Julien Pivotto
3e16d1da46 engine: resources: Add new consul resource
This commit adds a new consul:kv resource which allows us to set and
watch keys inside a consul kv datastore.

This was started by roidelapluie, and was finished during pair
programming with purpleidea.

Signed-off-by: Julien Pivotto <roidelapluie@inuits.eu>
Signed-off-by: James Shubin <james@shubin.ca>
2019-11-01 21:38:08 -04:00
Julien Pivotto
ae1860e859 lang: funcs: Add datetime.format function
Signed-off-by: Julien Pivotto <roidelapluie@inuits.eu>
2019-11-01 19:26:17 -04:00
Julien Pivotto
2ebc8fdf2a lang: funcs: Add datetime.hour function
Signed-off-by: Julien Pivotto <roidelapluie@inuits.eu>
2019-11-01 20:45:45 +01:00
Julien Pivotto
be4023be66 docs: Update resource-guide.md 2019-11-01 10:03:36 -04:00
Julien Pivotto
7f4ad76298 lang: funcs: Fix autogenerated comments
Signed-off-by: Julien Pivotto <roidelapluie@inuits.eu>
2019-11-01 11:18:48 +01:00
Julien Pivotto
0cbfaf98f3 lang: funcs: Support for []byte
Signed-off-by: Julien Pivotto <roidelapluie@inuits.eu>
2019-10-30 22:14:08 +01:00
James Shubin
631124e658 lang: funcs: Add nitpicks from funcgen
Discussed nitpicks with roidelapluie to clean up slightly for
consistency.
2019-10-30 08:51:11 -04:00
Julien Pivotto
1685ee1ecb lang: funcs: Autogenerated a lot of new functions
Signed-off-by: Julien Pivotto <roidelapluie@inuits.eu>
2019-10-30 08:42:53 -04:00
James Shubin
9b4d11f220 lang: funcs: Move convert into correct folder
This got merged in the wrong folder by accident.
2019-10-30 06:21:34 -04:00
James Shubin
46a71296a9 engine: resources: Add a Purge option to the file resource
This adds a "purge" parameter to the file resource. To do this, we have
to add the API hooks so the file resource can query other resources in
the graph to know if they are present, and as a result whether they
should be excluded from the purge or not.

This is useful for when we have a managed directory with some managed
contents. If a managed file is removed from the directory, then it will
be removed by the file (directory) resource if it has Purge set.
Alternatively, you can use the Reverse meta param, which is sometimes
preferable for this use case and sometimes not. This will be discussed
elsewhere.

This also adds a bunch of tests for this feature.

This also makes a few somewhat related cleanups in the file code.
2019-10-29 11:54:08 -04:00
James Shubin
1285588b62 engine: resources: Fix file absent helper
We should check this nil case too.
2019-10-29 07:15:43 -04:00
James Shubin
d96392f65e engine: resources: Improve error message in test
Nothing major, just improve testing here.
2019-10-29 07:15:43 -04:00
James Shubin
d1c5a736ae engine: resources: Allow nil helper functions in tests
This reduces some of the boilerplate when writing resource tests.
2019-10-29 07:15:43 -04:00
James Shubin
6b1e038c5c engine: resources: Add file exists helper
This will allow us to test even more things!
2019-10-29 07:15:43 -04:00
James Shubin
eaab1aae28 engine: graph, resources: Add filtered graph function
This lets a resource query the resource graph in a controlled way.
2019-10-29 07:15:43 -04:00
James Shubin
31030343a2 engine: Add graph queryable trait
This is a trait that specifies that your resource can be queried by
others. You can either enable it plainly to add wholesale access by
everyone, or you can implement your own Allow method to limit what is
permitted.
2019-10-29 07:15:43 -04:00
James Shubin
325ca03a13 engine: graph: Pass through the graph struct
We want to use it in the resources.
2019-10-29 07:15:43 -04:00
James Shubin
dea8e63df2 util: Add more tests to HasPathPrefix
We need to ensure this behaviour in a future commit, so might as well
double check that this works as expected!
2019-10-29 07:15:43 -04:00
James Shubin
58421fd31a engine: resources: Add fragments support to file resource
This adds a "fragments" mode to the file resource. In addition to
"content" and "source", you can now alternatively build a file from the
file fragments of other files.
2019-10-29 07:15:43 -04:00
James Shubin
b961c96862 lang: Include automatic edges in our test case
When running this test, we didn't attempt to build any automatic edges.
Since we'd like to test this here as well, let's add it.
2019-10-24 04:30:49 -04:00
Donald Bakong
2d23c1b0f3 lang: funcs: Add to_int and to_float functions 2019-10-22 08:34:29 -04:00
James Shubin
06952c224b engine: resources: nspawn: Use populate variable
We referred to the wrong variable. Not a major bug, but would produce a
useless or confusing error message otherwise.
2019-10-21 07:38:42 -04:00
Donald Bakong
2ea492c965 docs: Fix error on language-guide.md 2019-10-19 18:44:56 -04:00
Donald Bakong
dbf84f6879 docs: Fix typo on language-guide.md 2019-10-19 18:44:11 -04:00
James Shubin
0fa3d6c462 github: Update funding information file
Just got added to the GH sponsors thing and there are more fields
available in this file now. Let's hope this works!
2019-10-09 19:56:19 -04:00
James Shubin
d57f7aa03f misc: Specific mkosi fixes for centos-7
Seems we need golang from epel, to mask out the old git version, and to
workaround mkosi bugs.
2019-10-05 01:49:42 -04:00
Jimmy Tang
d64f9f5401 misc: Add CentOS 7 rpm build 2019-10-04 23:47:12 -04:00
James Shubin
a3029afc41 art: Rename art file to clarify it's about Winnie the Pooh
Pretty cool that we have our first meme =D Have a look in the FAQ to see
the real reason we watch our resources. (TL;DR: It allows us to make
graph changes very fast.)
2019-10-04 08:01:23 -04:00
579 changed files with 17987 additions and 3139 deletions

5
.github/FUNDING.yml vendored
View File

@@ -1,2 +1,5 @@
# You can add one username per supported platform and one custom link
# You can add one username per supported platform and one custom link.
custom: "https://paypal.me/purpleidea"
github: purpleidea
liberapay: purpleidea
patreon: purpleidea

View File

@@ -68,6 +68,8 @@ labels:
color: e11d21
- name: question
color: cc317c
- name: needinfo
color: fbca04
- name: wontfix
color: ffffff
# - name: first-timers-only

70
.github/workflows/test.yaml vendored Normal file
View File

@@ -0,0 +1,70 @@
# Docs: https://help.github.com/en/articles/workflow-syntax-for-github-actions
# If the name is omitted, it uses the filename instead.
#name: Test
on:
# Run on all pull requests.
pull_request:
#branches:
#- master
# Run on all pushes.
push:
# Run daily at 4am.
schedule:
- cron: 0 4 * * *
jobs:
maketest:
name: Test (${{ matrix.test_block }}) on ${{ matrix.os }} with golang ${{ matrix.golang_version }}
runs-on: ${{ matrix.os }}
env:
GOPATH: /home/runner/work/mgmt/mgmt/go
strategy:
matrix:
# TODO: Add tip when it's supported: https://github.com/actions/setup-go/issues/21
os:
- ubuntu-latest
# macos tests are currently failing in CI
#- macos-latest
golang_version:
# TODO: add 1.15.x and tip
# minimum required and latest published go_version
#- 1.13
- 1.15
test_block:
- basic
- shell
- race
#fail-fast: false
steps:
# Do not shallow fetch, will fail when building bindata/
# The path can't be absolute, so we need to move it to the
# expected location later.
- name: Clone mgmt
uses: actions/checkout@v2
with:
submodules: recursive
fetch-depth: 0
path: ./go/src/github.com/purpleidea/mgmt
- name: Install Go ${{ matrix.golang_version }}
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.golang_version }}
# Install & configure ruby, fixes gem permissions error
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: head
- name: Install dependencies
working-directory: ./go/src/github.com/purpleidea/mgmt
run: |
make deps
- name: Run test
working-directory: ./go/src/github.com/purpleidea/mgmt
run: |
TEST_BLOCK="${{ matrix.test_block }}" make test

2
.gitignore vendored
View File

@@ -17,3 +17,5 @@ rpmbuild/
releases/
# vim swap files
.*.sw[op]
# prevent `echo foo 2>1` typo errors by making this file read-only
1

14
.gitmodules vendored
View File

@@ -1,5 +1,5 @@
[submodule "vendor/github.com/coreos/etcd"]
path = vendor/github.com/coreos/etcd
path = vendor/go.etcd.io/etcd
url = https://github.com/coreos/etcd/
[submodule "vendor/google.golang.org/grpc"]
path = vendor/google.golang.org/grpc
@@ -28,6 +28,12 @@
[submodule "vendor/github.com/purpleidea/distribution"]
path = vendor/github.com/docker/distribution
url = https://github.com/purpleidea/distribution
[submodule "vendor/github.com/purpleidea/go-connections"]
path = vendor/github.com/docker/go-connections
url = https://github.com/docker/go-connections
[submodule "vendor/github.com/hashicorp/go-multierror"]
path = vendor/github.com/hashicorp/go-multierror
url = https://github.com/hashicorp/go-multierror
[submodule "vendor/github.com/containerd/containerd"]
path = vendor/github.com/containerd/containerd
url = https://github.com/purpleidea/containerd
[submodule "vendor/github.com/hashicorp/consul"]
path = vendor/github.com/hashicorp/consul
url = https://github.com/hashicorp/consul/

View File

@@ -24,21 +24,21 @@ install: 'make deps'
matrix:
fast_finish: false
allow_failures:
- go: 1.12.x
- go: 1.14.x
- go: tip
- os: osx
# include only one build for osx for a quicker build as the nr. of these runners are sparse
include:
- name: "basic tests"
go: 1.11.x
go: 1.13.x
env: TEST_BLOCK=basic
- name: "shell tests"
go: 1.11.x
go: 1.13.x
env: TEST_BLOCK=shell
- name: "race tests"
go: 1.11.x
go: 1.13.x
env: TEST_BLOCK=race
- go: 1.12.x
- go: 1.14.x
- go: tip
- os: osx
script: 'TEST_BLOCK="$TEST_BLOCK" make test'
@@ -47,8 +47,8 @@ script: 'TEST_BLOCK="$TEST_BLOCK" make test'
# with a value of: irc.freenode.net#mgmtconfig to eliminate noise from forks...
notifications:
irc:
channels:
- secure: htcuWAczm3C1zKC9vUfdRzhIXM1vtF+q0cLlQFXK1IQQlk693/pM30Mmf2L/9V2DVDeps+GyLdip0ARXD1DZEJV0lK+Ca1qbHdFP1r4Xv6l5+jaDb5Y88YU5LI8K758QShiZJojuQ1aO2j8xmmt9V0/5y5QwlpPeHbKYBOFPBX3HvlT9DhvwZNKGhBb4qJOEaPVOwq9IkN3DyQ456MHcJ3q3vF9Lb440uTuLsJNof2AbYZH8ZIHCSG2N8tBj2qhJOpWQboYtQJzE2pRaGkGBL4kYcHZSZMXX8sl4cBM1vx/IRUkvBxJUpLJz2gn/eRI+/gr59juZE2K0+FOLlx9dLnX626Y9xSViopBI6JsIoHJDqNC7aGaF2qaYulGYN65VNKVqmghjgt6JLmmiKeH10hYrJMMvt2rms8l4+5iwmCwXvhH/WU9edzk2p5wqERMnostJFEJib0zI3yzLoF0sdJs+veKtagzfayY2d2l7hlmt951IpqqVWldVgWUcQKVvi8gmRarbwFlK+5D7BEnkUDcLNly/cqf7BgEeX6YfF+FiR4pgfOhYvGCD+2q91NgWQXHBCxbyN0be1TVdkXD94f0Lkn94VyEJJ+PkPlG+rPgFwGcjqN4oEGkJeJmES2If05q2Ms1dJLwYQDL3+Py4lNMSdSWj24TzlFVhtwHepuw=
#channels:
# - secure: htcuWAczm3C1zKC9vUfdRzhIXM1vtF+q0cLlQFXK1IQQlk693/pM30Mmf2L/9V2DVDeps+GyLdip0ARXD1DZEJV0lK+Ca1qbHdFP1r4Xv6l5+jaDb5Y88YU5LI8K758QShiZJojuQ1aO2j8xmmt9V0/5y5QwlpPeHbKYBOFPBX3HvlT9DhvwZNKGhBb4qJOEaPVOwq9IkN3DyQ456MHcJ3q3vF9Lb440uTuLsJNof2AbYZH8ZIHCSG2N8tBj2qhJOpWQboYtQJzE2pRaGkGBL4kYcHZSZMXX8sl4cBM1vx/IRUkvBxJUpLJz2gn/eRI+/gr59juZE2K0+FOLlx9dLnX626Y9xSViopBI6JsIoHJDqNC7aGaF2qaYulGYN65VNKVqmghjgt6JLmmiKeH10hYrJMMvt2rms8l4+5iwmCwXvhH/WU9edzk2p5wqERMnostJFEJib0zI3yzLoF0sdJs+veKtagzfayY2d2l7hlmt951IpqqVWldVgWUcQKVvi8gmRarbwFlK+5D7BEnkUDcLNly/cqf7BgEeX6YfF+FiR4pgfOhYvGCD+2q91NgWQXHBCxbyN0be1TVdkXD94f0Lkn94VyEJJ+PkPlG+rPgFwGcjqN4oEGkJeJmES2If05q2Ms1dJLwYQDL3+Py4lNMSdSWj24TzlFVhtwHepuw=
template:
- "%{repository} (%{commit}: %{author}): %{message}"
- "More info : %{build_url}"

View File

@@ -6,6 +6,7 @@ This list is sorted alphabetically by first name.
Felix Frank
James Shubin
Joe Groocock
Johan Bloemberg
Jonathan Gold
Julien Pivotto

View File

@@ -1,5 +1,5 @@
Mgmt
Copyright (C) 2013-2019+ James Shubin and the project contributors
Copyright (C) 2013-2021+ James Shubin and the project contributors
Written by James Shubin <james@shubin.ca> and the project contributors
This program is free software: you can redistribute it and/or modify

View File

@@ -1,5 +1,5 @@
# Mgmt
# Copyright (C) 2013-2019+ James Shubin and the project contributors
# Copyright (C) 2013-2021+ James Shubin and the project contributors
# Written by James Shubin <james@shubin.ca> and the project contributors
#
# This program is free software: you can redistribute it and/or modify
@@ -18,8 +18,8 @@
SHELL = /usr/bin/env bash
.PHONY: all art cleanart version program lang path deps run race bindata generate build build-debug crossbuild clean test gofmt yamlfmt format docs
.PHONY: rpmbuild mkdirs rpm srpm spec tar upload upload-sources upload-srpms upload-rpms upload-releases copr tag
.PHONY: mkosi mkosi_fedora-30 mkosi_fedora-29 mkosi_debian-10 mkosi_ubuntu-bionic mkosi_archlinux
.PHONY: release releases_path release_fedora-30 release_fedora-29 release_debian-10 release_ubuntu-bionic release_archlinux
.PHONY: mkosi mkosi_fedora-30 mkosi_fedora-29 mkosi_centos-7 mkosi_debian-10 mkosi_ubuntu-bionic mkosi_archlinux
.PHONY: release releases_path release_fedora-30 release_fedora-29 release_centos-7 release_debian-10 release_ubuntu-bionic release_archlinux
.PHONY: funcgen
.SILENT: clean bindata
@@ -55,18 +55,21 @@ GOHOSTARCH = $(shell go env GOHOSTARCH)
TOKEN_FEDORA-30 = fedora-30
TOKEN_FEDORA-29 = fedora-29
TOKEN_CENTOS-7 = centos-7
TOKEN_DEBIAN-10 = debian-10
TOKEN_UBUNTU-BIONIC = ubuntu-bionic
TOKEN_ARCHLINUX = archlinux
FILE_FEDORA-30 = mgmt-$(TOKEN_FEDORA-30)-$(VERSION)-1.x86_64.rpm
FILE_FEDORA-29 = mgmt-$(TOKEN_FEDORA-29)-$(VERSION)-1.x86_64.rpm
FILE_CENTOS-7 = mgmt-$(TOKEN_CENTOS-7)-$(VERSION)-1.x86_64.rpm
FILE_DEBIAN-10 = mgmt_$(TOKEN_DEBIAN-10)_$(VERSION)_amd64.deb
FILE_UBUNTU-BIONIC = mgmt_$(TOKEN_UBUNTU-BIONIC)_$(VERSION)_amd64.deb
FILE_ARCHLINUX = mgmt-$(TOKEN_ARCHLINUX)-$(VERSION)-1-x86_64.pkg.tar.xz
PKG_FEDORA-30 = releases/$(VERSION)/$(TOKEN_FEDORA-30)/$(FILE_FEDORA-30)
PKG_FEDORA-29 = releases/$(VERSION)/$(TOKEN_FEDORA-29)/$(FILE_FEDORA-29)
PKG_CENTOS-7 = releases/$(VERSION)/$(TOKEN_CENTOS-7)/$(FILE_CENTOS-7)
PKG_DEBIAN-10 = releases/$(VERSION)/$(TOKEN_DEBIAN-10)/$(FILE_DEBIAN-10)
PKG_UBUNTU-BIONIC = releases/$(VERSION)/$(TOKEN_UBUNTU-BIONIC)/$(FILE_UBUNTU-BIONIC)
PKG_ARCHLINUX = releases/$(VERSION)/$(TOKEN_ARCHLINUX)/$(FILE_ARCHLINUX)
@@ -167,19 +170,14 @@ GOOS=$(firstword $(subst -, ,$*))
GOARCH=$(lastword $(subst -, ,$*))
build/mgmt-%: $(GO_FILES) $(MCL_FILES) | bindata lang funcgen
@echo "Building: $(PROGRAM), os/arch: $*, version: $(SVERSION)..."
@# reassigning GOOS and GOARCH to make build command copy/pastable
@# go 1.10+ requires specifying the package for ldflags
@if go version | grep -qE 'go1.9'; then \
time env GOOS=${GOOS} GOARCH=${GOARCH} go build -i -ldflags "-X main.program=$(PROGRAM) -X main.version=$(SVERSION) ${LDFLAGS}" -o $@ $(BUILD_FLAGS); \
else \
time env GOOS=${GOOS} GOARCH=${GOARCH} go build -i -ldflags=$(PKGNAME)="-X main.program=$(PROGRAM) -X main.version=$(SVERSION) ${LDFLAGS}" -o $@ $(BUILD_FLAGS); \
fi
@time env GOOS=${GOOS} GOARCH=${GOARCH} go build -i -ldflags=$(PKGNAME)="-X main.program=$(PROGRAM) -X main.version=$(SVERSION) ${LDFLAGS}" -o $@ $(BUILD_FLAGS)
# create a list of binary file names to use as make targets
crossbuild_targets = $(addprefix build/mgmt-,$(subst /,-,${GOOSARCHES}))
crossbuild: ${crossbuild_targets}
clean: ## clean things up
$(MAKE) --quiet -C test clean
$(MAKE) --quiet -C bindata clean
$(MAKE) --quiet -C lang/funcs clean
$(MAKE) --quiet -C lang clean
@@ -193,6 +191,8 @@ clean: ## clean things up
rm -f build/mgmt-*
test: build ## run tests
@# recursively run make in child dir named test
@$(MAKE) --quiet -C test
./test.sh
# create all test targets for make tab completion (eg: make test-gofmt)
@@ -365,7 +365,7 @@ tag: ## tags a new release
#
# mkosi
#
mkosi: mkosi_fedora-30 mkosi_fedora-29 mkosi_debian-10 mkosi_ubuntu-bionic mkosi_archlinux ## builds distro packages via mkosi
mkosi: mkosi_fedora-30 mkosi_fedora-29 mkosi_centos-7 mkosi_debian-10 mkosi_ubuntu-bionic mkosi_archlinux ## builds distro packages via mkosi
mkosi_fedora-30: releases/$(VERSION)/.mkdir
@title='$@' ; echo "Generating: $${title#'mkosi_'} via mkosi..."
@@ -375,6 +375,10 @@ mkosi_fedora-29: releases/$(VERSION)/.mkdir
@title='$@' ; echo "Generating: $${title#'mkosi_'} via mkosi..."
@title='$@' ; distro=$${title#'mkosi_'} ; ./misc/mkosi/make.sh $${distro} `realpath "releases/$(VERSION)/"`
mkosi_centos-7: releases/$(VERSION)/.mkdir
@title='$@' ; echo "Generating: $${title#'mkosi_'} via mkosi..."
@title='$@' ; distro=$${title#'mkosi_'} ; ./misc/mkosi/make.sh $${distro} `realpath "releases/$(VERSION)/"`
mkosi_debian-10: releases/$(VERSION)/.mkdir
@title='$@' ; echo "Generating: $${title#'mkosi_'} via mkosi..."
@title='$@' ; distro=$${title#'mkosi_'} ; ./misc/mkosi/make.sh $${distro} `realpath "releases/$(VERSION)/"`
@@ -398,11 +402,12 @@ releases_path:
release_fedora-30: $(PKG_FEDORA-30)
release_fedora-29: $(PKG_FEDORA-29)
release_centos-7: $(PKG_CENTOS-7)
release_debian-10: $(PKG_DEBIAN-10)
release_ubuntu-bionic: $(PKG_UBUNTU-BIONIC)
release_archlinux: $(PKG_ARCHLINUX)
releases/$(VERSION)/mgmt-release.url: $(PKG_FEDORA-30) $(PKG_FEDORA-29) $(PKG_DEBIAN-10) $(PKG_UBUNTU-BIONIC) $(PKG_ARCHLINUX) $(SHA256SUMS_ASC)
releases/$(VERSION)/mgmt-release.url: $(PKG_FEDORA-30) $(PKG_FEDORA-29) $(PKG_CENTOS-7) $(PKG_DEBIAN-10) $(PKG_UBUNTU-BIONIC) $(PKG_ARCHLINUX) $(SHA256SUMS_ASC)
@echo "Pushing git tag $(VERSION) to origin..."
git push origin $(VERSION)
@echo "Creating github release..."
@@ -410,6 +415,7 @@ releases/$(VERSION)/mgmt-release.url: $(PKG_FEDORA-30) $(PKG_FEDORA-29) $(PKG_DE
-F <( echo -e "$(VERSION)\n";echo "Verify the signatures of all packages before you use them. The signing key can be downloaded from https://purpleidea.com/contact/#pgp-key to verify the release." ) \
-a $(PKG_FEDORA-30) \
-a $(PKG_FEDORA-29) \
-a $(PKG_CENTOS-7) \
-a $(PKG_DEBIAN-10) \
-a $(PKG_UBUNTU-BIONIC) \
-a $(PKG_ARCHLINUX) \
@@ -420,7 +426,7 @@ releases/$(VERSION)/mgmt-release.url: $(PKG_FEDORA-30) $(PKG_FEDORA-29) $(PKG_DE
|| rm -f releases/$(VERSION)/mgmt-release.url
releases/$(VERSION)/.mkdir:
mkdir -p releases/$(VERSION)/{$(TOKEN_FEDORA-30),$(TOKEN_FEDORA-29),$(TOKEN_DEBIAN-10),$(TOKEN_UBUNTU-BIONIC),$(TOKEN_ARCHLINUX)}/ && touch releases/$(VERSION)/.mkdir
mkdir -p releases/$(VERSION)/{$(TOKEN_FEDORA-30),$(TOKEN_FEDORA-29),$(TOKEN_CENTOS-7),$(TOKEN_DEBIAN-10),$(TOKEN_UBUNTU-BIONIC),$(TOKEN_ARCHLINUX)}/ && touch releases/$(VERSION)/.mkdir
releases/$(VERSION)/$(TOKEN_FEDORA-30)/changelog: $(PROGRAM) releases/$(VERSION)/.mkdir
@title='$(@D)' ; distro=$${title#'releases/$(VERSION)/'} ; echo "Generating: $${distro} changelog..."
@@ -438,6 +444,14 @@ $(PKG_FEDORA-29): releases/$(VERSION)/$(TOKEN_FEDORA-29)/changelog
@title='$(@D)' ; distro=$${title#'releases/$(VERSION)/'} ; echo "Building: $${distro} package..."
@title='$(@D)' ; distro=$${title#'releases/$(VERSION)/'} ; ./misc/fpm-pack.sh $${distro} $(VERSION) "$(FILE_FEDORA-29)" libvirt-devel augeas-devel
releases/$(VERSION)/$(TOKEN_CENTOS-7)/changelog: $(PROGRAM) releases/$(VERSION)/.mkdir
@title='$(@D)' ; distro=$${title#'releases/$(VERSION)/'} ; echo "Generating: $${distro} changelog..."
@title='$(@D)' ; distro=$${title#'releases/$(VERSION)/'} ; ./misc/make-rpm-changelog.sh "$${distro}" $(VERSION)
$(PKG_CENTOS-7): releases/$(VERSION)/$(TOKEN_CENTOS-7)/changelog
@title='$(@D)' ; distro=$${title#'releases/$(VERSION)/'} ; echo "Building: $${distro} package..."
@title='$(@D)' ; distro=$${title#'releases/$(VERSION)/'} ; ./misc/fpm-pack.sh $${distro} $(VERSION) "$(FILE_CENTOS-7)" libvirt-devel augeas-devel
releases/$(VERSION)/$(TOKEN_DEBIAN-10)/changelog: $(PROGRAM) releases/$(VERSION)/.mkdir
@title='$(@D)' ; distro=$${title#'releases/$(VERSION)/'} ; echo "Generating: $${distro} changelog..."
@title='$(@D)' ; distro=$${title#'releases/$(VERSION)/'} ; ./misc/make-deb-changelog.sh "$${distro}" $(VERSION)
@@ -458,10 +472,10 @@ $(PKG_ARCHLINUX): $(PROGRAM) releases/$(VERSION)/.mkdir
@title='$(@D)' ; distro=$${title#'releases/$(VERSION)/'} ; echo "Building: $${distro} package..."
@title='$(@D)' ; distro=$${title#'releases/$(VERSION)/'} ; ./misc/fpm-pack.sh $${distro} $(VERSION) "$(FILE_ARCHLINUX)" libvirt augeas
$(SHA256SUMS): $(PKG_FEDORA-30) $(PKG_FEDORA-29) $(PKG_DEBIAN-10) $(PKG_UBUNTU-BIONIC) $(PKG_ARCHLINUX)
$(SHA256SUMS): $(PKG_FEDORA-30) $(PKG_FEDORA-29) $(PKG_CENTOS-7) $(PKG_DEBIAN-10) $(PKG_UBUNTU-BIONIC) $(PKG_ARCHLINUX)
@# remove the directory separator in the SHA256SUMS file
@echo "Generating: sha256 sum..."
sha256sum $(PKG_FEDORA-30) $(PKG_FEDORA-29) $(PKG_DEBIAN-10) $(PKG_UBUNTU-BIONIC) $(PKG_ARCHLINUX) | awk -F '/| ' '{print $$1" "$$6}' > $(SHA256SUMS)
sha256sum $(PKG_FEDORA-30) $(PKG_FEDORA-29) $(PKG_CENTOS-7) $(PKG_DEBIAN-10) $(PKG_UBUNTU-BIONIC) $(PKG_ARCHLINUX) | awk -F '/| ' '{print $$1" "$$6}' > $(SHA256SUMS)
$(SHA256SUMS_ASC): $(SHA256SUMS)
@echo "Signing sha256 sum..."
@@ -487,14 +501,10 @@ help: ## show this help screen
awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
@echo ''
funcgen: lang/funcs/core/generated_funcs_test.go lang/funcs/core/generated_funcs.go
lang/funcs/core/generated_funcs_test.go: lang/funcs/funcgen/*.go lang/funcs/core/funcgen.yaml lang/funcs/funcgen/templates/generated_funcs_test.go.tpl
@echo "Generating: funcs test..."
@go run lang/funcs/funcgen/*.go -templates lang/funcs/funcgen/templates/generated_funcs_test.go.tpl 2>/dev/null
funcgen: lang/funcs/core/generated_funcs.go
lang/funcs/core/generated_funcs.go: lang/funcs/funcgen/*.go lang/funcs/core/funcgen.yaml lang/funcs/funcgen/templates/generated_funcs.go.tpl
@echo "Generating: funcs..."
@go run lang/funcs/funcgen/*.go -templates lang/funcs/funcgen/templates/generated_funcs.go.tpl 2>/dev/null
@go run `find lang/funcs/funcgen/ -maxdepth 1 -type f -name '*.go' -not -name '*_test.go'` -templates=lang/funcs/funcgen/templates/generated_funcs.go.tpl >/dev/null
# vim: ts=8

View File

@@ -4,8 +4,9 @@
[![Go Report Card](https://goreportcard.com/badge/github.com/purpleidea/mgmt?style=flat-square)](https://goreportcard.com/report/github.com/purpleidea/mgmt)
[![Build Status](https://img.shields.io/travis/purpleidea/mgmt/master.svg?style=flat-square)](http://travis-ci.org/purpleidea/mgmt)
[![Build Status](https://github.com/purpleidea/mgmt/workflows/.github/workflows/test.yaml/badge.svg)](https://github.com/purpleidea/mgmt/actions/)
[![GoDoc](https://img.shields.io/badge/godoc-reference-5272B4.svg?style=flat-square)](https://godoc.org/github.com/purpleidea/mgmt)
[![IRC](https://img.shields.io/badge/irc-%23mgmtconfig-orange.svg?style=flat-square)](https://webchat.freenode.net/?channels=#mgmtconfig)
[![IRC](https://img.shields.io/badge/irc-%23mgmtconfig-orange.svg?style=flat-square)](https://web.libera.chat/?channels=#mgmtconfig)
[![Patreon](https://img.shields.io/badge/patreon-donate-yellow.svg?style=flat-square)](https://www.patreon.com/purpleidea)
[![Liberapay](https://img.shields.io/badge/liberapay-donate-yellow.svg?style=flat-square)](https://liberapay.com/purpleidea/donate)
@@ -21,7 +22,7 @@ ensure that your file server is set to read-only when it's friday.
import "datetime"
$is_friday = datetime.weekday(datetime.now()) == "friday"
file "/srv/files/" {
state => "exists",
state => $const.res.file.state.exists,
mode => if $is_friday { # this updates the mode, the instant it changes!
"0550"
} else {
@@ -65,7 +66,7 @@ Come join us in the `mgmt` community!
| Medium | Link |
|---|---|
| IRC | [#mgmtconfig](https://webchat.freenode.net/?channels=#mgmtconfig) on Freenode |
| IRC | [#mgmtconfig](https://web.libera.chat/?channels=#mgmtconfig) on Libera.Chat |
| Twitter | [@mgmtconfig](https://twitter.com/mgmtconfig) & [#mgmtconfig](https://twitter.com/hashtag/mgmtconfig) |
| Mailing list | [mgmtconfig-list@redhat.com](https://www.redhat.com/mailman/listinfo/mgmtconfig-list) |
| Patreon | [purpleidea](https://www.patreon.com/purpleidea) on Patreon |

11
Vagrantfile vendored
View File

@@ -6,7 +6,7 @@ Vagrant.configure(2) do |config|
config.vm.synced_folder ".", "/vagrant", disabled: true
config.vm.define "mgmt-dev" do |instance|
instance.vm.box = "fedora/28-cloud-base"
instance.vm.box = "bento/fedora-31"
end
config.vm.provider "virtualbox" do |v|
@@ -23,8 +23,7 @@ Vagrant.configure(2) do |config|
config.vm.provision "file", source: "vagrant/mgmt.bashrc", destination: ".mgmt.bashrc"
config.vm.provision "file", source: "~/.gitconfig", destination: ".gitconfig"
# copied from make-deps.sh (with added git)
config.vm.provision "shell", inline: "dnf install -y libvirt-devel golang golang-googlecode-tools-stringer hg git make gem"
config.vm.provision "shell", inline: "dnf install -y golang git make"
# set up packagekit
config.vm.provision "shell" do |shell|
@@ -39,8 +38,10 @@ Vagrant.configure(2) do |config|
script = <<-SCRIPT
grep -q 'mgmt\.bashrc' ~/.bashrc || echo '. ~/.mgmt.bashrc' >>~/.bashrc
. ~/.mgmt.bashrc
go get -u github.com/purpleidea/mgmt
cd ~/gopath/src/github.com/purpleidea/mgmt
mkdir -p ~/gopath/src/github.com/purpleidea
cd ~/gopath/src/github.com/purpleidea
git clone https://github.com/purpleidea/mgmt --recursive
cd mgmt
make deps
SCRIPT
config.vm.provision "shell" do |shell|

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 683 KiB

View File

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 102 KiB

View File

@@ -1,5 +1,5 @@
# Mgmt
# Copyright (C) 2013-2019+ James Shubin and the project contributors
# Copyright (C) 2013-2021+ James Shubin and the project contributors
# Written by James Shubin <james@shubin.ca> and the project contributors
#
# This program is free software: you can redistribute it and/or modify

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2021+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2021+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify

2
debian/copyright vendored
View File

@@ -3,7 +3,7 @@ Upstream-Name: mgmt
Source: <https://github.com/purpleidea/mgmt>
Files: *
Copyright: Copyright (C) 2013-2019+ James Shubin and the project contributors
Copyright: Copyright (C) 2013-2021+ James Shubin and the project contributors
License: GPL-3.0
License: GPL-3.0

2
doc.go
View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2021+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify

View File

@@ -1,10 +1,10 @@
FROM golang:1.9
FROM golang:1.13
MAINTAINER Michał Czeraszkiewicz <contact@czerasz.com>
# Set the reset cache variable
# Read more here: http://czerasz.com/2014/11/13/docker-tip-and-tricks/#use-refreshedat-variable-for-better-cache-control
ENV REFRESHED_AT 2017-11-16
ENV REFRESHED_AT 2020-09-23
# Update the package list to be able to use required packages
RUN apt-get update

View File

@@ -1,4 +1,4 @@
FROM golang:1.11
FROM golang:1.13
MAINTAINER Michał Czeraszkiewicz <contact@czerasz.com>

View File

@@ -51,7 +51,7 @@ master_doc = 'index'
# General information about the project.
project = u'mgmt'
copyright = u'2013-2019+ James Shubin and the project contributors'
copyright = u'2013-2021+ James Shubin and the project contributors'
author = u'James Shubin'
# The version info for the project you're documenting, acts as replacement for

View File

@@ -28,7 +28,7 @@ required for running the _test_ suite.
### Build
* `golang` 1.11 or higher (required, available in some distros and distributed
* `golang` 1.13 or higher (required, available in some distros and distributed
as a binary officially by [golang.org](https://golang.org/dl/))
### Runtime

View File

@@ -122,6 +122,10 @@ entire set of running mgmt agents will need to all simultaneously converge for
the group to exit. This is particularly useful for bootstrapping new clusters
which need to exchange information that is only available at run time.
This existed in earlier versions of mgmt as a `--remote` option, but it has been
removed and is being ported to a more powerful variant where you can remote
execute via a `remote` resource.
#### Blog post
You can read the introductory blog post about this topic here:
@@ -360,12 +364,6 @@ collision with this globally defined semaphore. The size value must be greater
than zero at this time. The traditional non-parallel execution found in config
management tools such as `Puppet` can be obtained with `--sema 1`.
#### `--remote <graph.yaml>`
Point to a graph file to run on the remote host specified within. This parameter
can be used multiple times if you'd like to remotely run on multiple hosts in
parallel.
#### `--allow-interactive`
Allow interactive prompting for SSH passwords if there is no authentication
@@ -404,8 +402,8 @@ default prefix. This can't be combined with the `--prefix` option.
If this option is specified, we will attempt to fall back to a temporary prefix
if the primary prefix couldn't be created. This is useful for avoiding failures
in environments where the primary prefix may or may not be available, but you'd
like to try. The canonical example is when running `mgmt` with `--remote` there
might be a cached copy of the binary in the primary prefix, but in case there's
like to try. The canonical example is when running `mgmt` with remote execution
there might be a cached copy of the binary in the primary prefix, but if there's
no binary available continue working in a temporary directory to avoid failure.
### Compilation options
@@ -488,7 +486,7 @@ To report any bugs, please file a ticket at: [https://github.com/purpleidea/mgmt
## Authors
Copyright (C) 2013-2019+ James Shubin and the project contributors
Copyright (C) 2013-2021+ James Shubin and the project contributors
Please see the
[AUTHORS](https://github.com/purpleidea/mgmt/tree/master/AUTHORS) file

View File

@@ -53,10 +53,11 @@ find a number of tutorials online.
3. Spend between four to six hours with the [golang tour](https://tour.golang.org/).
Skip over the longer problems, but try and get a solid overview of everything.
If you forget something, you can always go back and repeat those parts.
4. Connect to our [#mgmtconfig](https://webchat.freenode.net/?channels=#mgmtconfig)
IRC channel on the [Freenode](https://freenode.net/) network. You can use any
IRC client that you'd like, but the [hosted web portal](https://webchat.freenode.net/?channels=#mgmtconfig)
will suffice if you don't know what else to use.
4. Connect to our [#mgmtconfig](https://web.libera.chat/?channels=#mgmtconfig)
IRC channel on the [Libera.Chat](https://libera.chat/) network. You can use any
IRC client that you'd like, but the [hosted web portal](https://web.libera.chat/?channels=#mgmtconfig)
will suffice if you don't know what else to use. [Here are a few suggestions for
alternative clients.](https://libera.chat/guides/clients)
5. Now it's time to try and starting writing a patch! We have tagged a bunch of
[open issues as #mgmtlove](https://github.com/purpleidea/mgmt/issues?q=is%3Aissue+is%3Aopen+label%3Amgmtlove)
for new users to have somewhere to get involved. Look through them to see if
@@ -242,7 +243,7 @@ gets created in case it is not present, then you must also specify the state:
```
file "/tmp/foo" {
state => "exists",
state => $const.res.file.state.exists,
content => "hello world\n",
}
```
@@ -254,6 +255,13 @@ prevent masking an error for a situation when you expected a file to already be
at that location. It also turns out to simplify the internals significantly, and
remove an ambiguous scenario with the reversable file resource.
### Why do function names inside of templates include underscores?
The golang template library which we use to implement the template() function
doesn't support the dot notation, so we import all our normal functions, and
just replace dots with underscores. As an example, the standard `datetime.print`
function is shown within mcl scripts as datetime_print after being imported.
### On startup `mgmt` hangs after: `etcd: server: starting...`.
If you get an error message similar to:
@@ -287,6 +295,13 @@ an instance of mgmt running, or if a related file locking issue occurred. To
solve this, shutdown and running mgmt process, run `rm mgmt` to remove the file,
and then get a new one by running `make` again.
### The docs speaks of `--remote` but the CLI errors out?
The `--remote` flag existed in an earlier version of mgmt. It was removed and
will be replaced with a more powerful version, which is a "remote" resource. The
code is mostly ready but it's not finished. If you'd like to help finish it or
sponsor the work, please let me know.
### Does this support Windows? OSX? GNU Hurd?
Mgmt probably works best on Linux, because that's what most developers use for
@@ -362,7 +377,7 @@ 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)
It's best to ask on [IRC](https://web.libera.chat/?channels=#mgmtconfig)
to see if someone can help you. If you don't get a response from IRC, you can
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

View File

@@ -37,8 +37,10 @@ available types and values in the mgmt language. It is very easy to use, and
should be fairly intuitive. Most of what you'll need to know can be inferred
from looking at example code.
To implement a function, you'll need to create a file in
[`lang/funcs/simple/`](https://github.com/purpleidea/mgmt/tree/master/lang/funcs/simple/).
To implement a function, you'll need to create a file that imports the
[`lang/funcs/simple/`](https://github.com/purpleidea/mgmt/tree/master/lang/funcs/simple/)
module. It should probably get created in the correct directory inside of:
[`lang/funcs/core/`](https://github.com/purpleidea/mgmt/tree/master/lang/funcs/core/).
The function should be implemented as a `FuncValue` in our type system. It is
then registered with the engine during `init()`. An example explains it best:
@@ -50,14 +52,15 @@ package simple
import (
"fmt"
"github.com/purpleidea/mgmt/lang/funcs/simple"
"github.com/purpleidea/mgmt/lang/types"
)
// you must register your functions in init when the program starts up
func init() {
// Example function that squares an int and prints out answer as an str.
Register("talkingsquare", &types.FuncValue{
T: types.NewType("func(a int) str"), // declare the signature
simple.ModuleRegister(ModuleName, "talkingsquare", &types.FuncValue{
T: types.NewType("func(int) str"), // declare the signature
V: func(input []types.Value) (types.Value, error) {
i := input[0].Int() // get first arg as an int64
// must return the above specified value
@@ -109,15 +112,20 @@ As with the simple, non-polymorphic API, you can only implement [pure](https://e
functions, without writing too much boilerplate code. They will be automatically
re-evaluated as needed when their input values change.
To implement a function, you'll need to create a file in
[`lang/funcs/simplepoly/`](https://github.com/purpleidea/mgmt/tree/master/lang/funcs/simplepoly/).
To implement a function, you'll need to create a file that imports the
[`lang/funcs/simplepoly/`](https://github.com/purpleidea/mgmt/tree/master/lang/funcs/simplepoly/)
module. It should probably get created in the correct directory inside of:
[`lang/funcs/core/`](https://github.com/purpleidea/mgmt/tree/master/lang/funcs/core/).
The function should be implemented as a list of `FuncValue`'s in our type
system. It is then registered with the engine during `init()`. You may also use
the `variant` type in your type definitions. This special type will never be
seen inside a running program, and will get converted to a concrete type if a
suitable match to this signature can be found. Be warned that signatures which
contain too many variants, or which are very general, might be hard for the
compiler to match, and ambiguous type graphs make for user compiler errors.
compiler to match, and ambiguous type graphs make for user compiler errors. The
top-level type must still be a function type, it may only contain variants as
part of its signature. It is probably more difficult to unify a function if its
return type is a variant, as opposed to if one of its args was.
An example explains it best:
@@ -127,11 +135,13 @@ An example explains it best:
import (
"fmt"
"github.com/purpleidea/mgmt/lang/types"
"github.com/purpleidea/mgmt/lang/funcs/simplepoly"
"github.com/purpleidea/mgmt/lang/types"
)
func init() {
// You may use the simplepoly.ModuleRegister method to register your
// function if it's in a module, as seen in the simple function example.
simplepoly.Register("len", []*types.FuncValue{
{
T: types.NewType("func([]variant) int"),
@@ -190,7 +200,7 @@ if it meets your needs. Most functions will be able to use that API. If you
really need something more powerful, then you can use the regular function API.
What follows are each of the method signatures and a description of each.
### Default
### Info
```golang
Info() *interfaces.Info
@@ -435,6 +445,11 @@ generator to build your `FuncValue` implementations, and pass in the unique
signature to each one as you are building them. Using a generator is a common
technique which was mentioned previously.
One obvious situation where this might occur is if your function doesn't take
any inputs! An example `math.fortytwo()` function was implemented that
demonstrates the use of function generators to pass the type signatures into the
implementations.
### 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).

View File

@@ -54,7 +54,7 @@ can be impossible to infer the item's type.
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,}`.
`{"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.
@@ -206,7 +206,7 @@ value to use if that boolean is true. You can do this with the resource-specific
$b = true # change me to false and then try editing the file manually
file "/tmp/mgmt-elvis" {
content => $b ?: "hello world\n",
state => "exists",
state => $const.res.file.state.exists,
}
```
@@ -293,7 +293,7 @@ send notifications. You may have multiples of these per resource, including
multiple `Depend` lines if necessary. Each of these properties also supports the
conditional inclusion `elvis` operator as well.
For example, you may write is:
For example, you may write:
```mcl
$b = true # for example purposes

View File

@@ -52,3 +52,7 @@ if we missed something that you think is relevant!
| James Shubin | video | [Recording from FOSDEM Containers Devroom 2019](https://video.fosdem.org/2019/UA2.114/containers_mgmt.webm) |
| James Shubin | video | [Recording from FOSDEM Monitoring Devroom 2019](https://video.fosdem.org/2019/UB2.252A/real_time_merging_of_config_management_and_monitoring.webm) |
| James Shubin | blog | [Mgmt Configuration Language: Class and Include](https://purpleidea.com/blog/2019/07/26/class-and-include-in-mgmt/) |
| James Shubin | video | [Recording from FOSDEM 2020, Main Track (History)](https://video.fosdem.org/2020/Janson/automation.webm) |
| James Shubin | video | [Recording from FOSDEM 2020, Infra Management Devroom](https://video.fosdem.org/2020/UA2.120/mgmt.webm) |
| James Shubin | video | [Recording from FOSDEM 2020, Minimalistic Languages Devroom](https://video.fosdem.org/2020/AW1.125/mgmtconfigmore.webm) |
| James Shubin | video | [Recording from CfgMgmtCamp.eu 2020](https://www.youtube.com/watch?v=Kd7FAORFtsc) |

View File

@@ -37,7 +37,7 @@ You'll need some dependencies, including `golang`, and some associated tools.
#### Installing golang
* You need golang version 1.11 or greater installed.
* You need golang version 1.13 or greater installed.
* To install on rpm style systems: `sudo dnf install golang`
* To install on apt style systems: `sudo apt install golang`
* To install on macOS systems install [Homebrew](https://brew.sh)

View File

@@ -105,7 +105,7 @@ when parameters take a zero value, whenever this is possible.)
```golang
// Default returns some sensible defaults for this resource.
func (obj *FooRes) Default() Res {
func (obj *FooRes) Default() engine.Res {
return &FooRes{
Answer: 42, // sometimes, defaults shouldn't be the zero value
}
@@ -642,8 +642,8 @@ The signature intentionally matches what is required to satisfy the `go-yaml`
#### Example
```golang
// UnmarshalYAML is the custom unmarshal handler for this struct.
// It is primarily useful for setting the defaults.
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
// primarily useful for setting the defaults.
func (obj *FooRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
type rawRes FooRes // indirection to avoid infinite recursion

View File

@@ -17,6 +17,7 @@ You might want to look at the [generated documentation](https://godoc.org/github
for more up-to-date information about these resources.
* [Augeas](#Augeas): Manipulate files using augeas.
* [Consul:KV](#ConsulKV): Set keys in a Consul datastore.
* [Docker](#Docker):[Container](#Container) Manage docker containers.
* [Exec](#Exec): Execute shell commands on the system.
* [File](#File): Manage files and directories.
@@ -32,6 +33,8 @@ for more up-to-date information about these resources.
* [Print](#Print): Print messages to the console.
* [Svc](#Svc): Manage system systemd services.
* [Test](#Test): A mostly harmless resource that is used for internal testing.
* [Tftp:File](#TftpFile): Add files to the small embedded embedded tftp server.
* [Tftp:Server](#TftpServer): Run a small embedded tftp server.
* [Timer](#Timer): Manage system systemd services.
* [User](#User): Manage system users.
* [Virt](#Virt): Manage virtual machines with libvirt.
@@ -71,7 +74,7 @@ It has the following properties:
* `path`: absolute file path (directories have a trailing slash here)
* `state`: either `exists`, `absent`, or undefined
* `content`: raw file content
* `mode`: octal unix file permissions
* `mode`: octal unix file permissions or symbolic string
* `owner`: username or uid for the file owner
* `group`: group name or gid for the file group
@@ -98,6 +101,13 @@ The content property is a string that specifies the desired file contents.
The source property points to a source file or directory path that we wish to
copy over and use as the desired contents for our resource.
### Fragments
The fragments property lets you specify a list of files to concatenate together
to make up the contents of this file. They will be combined in the order that
they are listed in. If one of the files specified is a directory, then the
files in that top-level directory will be themselves combined together and used.
### Recurse
The recurse property limits whether file resource operations should recurse into
@@ -109,6 +119,12 @@ The force property is required if we want the file resource to be able to change
a file into a directory or vice-versa. If such a change is needed, but the force
property is not set to `true`, then this file resource will error.
### Purge
The purge property is used when this file represents a directory, and we'd like
to remove any unmanaged files from within it. Please note that any unmanaged
files in a directory with this flag set will be irreversibly deleted.
## Group
The group resource manages the system groups from `/etc/group`.
@@ -211,6 +227,16 @@ The service resource is still very WIP. Please help us by improving it!
The test resource is mostly harmless and is used for internal tests.
## Tftp:File
This adds files to the running tftp server. It's useful because it allows you to
add individual files without needing to create them on disk.
## Tftp:Server
Run a small embedded tftp server. This doesn't apply any state, but instead runs
a pure golang tftp server in the Watch loop.
## Timer
This resource needs better documentation. Please help us by improving it!

View File

@@ -61,6 +61,12 @@ 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.
### Default values
Whenever a constant or function parameter is defined, try and have the safer or
default value be the `zero` value. For example, instead of `const NoDanger`, use
`const AllowDanger` so that the `false` value is the safe scenario.
### Method receiver naming
[Contrary](https://github.com/golang/go/wiki/CodeReviewComments#receiver-names)
@@ -84,6 +90,57 @@ func (obj *Foo) Bar(baz string) int {
}
```
### Variable naming
We prefer shorter, scoped variables rather than `unnecessarilyLongIdentifiers`.
Remember the scoping rules and feel free to use new variables where appropriate.
For example, in a short string snippet you can use `s` instead of `myString`, as
well as other common choices. `i` is a common `int` counter, `f` for files, `fn`
for functions, `x` for something else and so on.
### Variable re-use
Feel free to create and use new variables instead of attempting to re-use the
same string. For example, if a function input arg is named `s`, you can use a
new variable to receive the first computation result on `s` instead of storing
it back into the original `s`. This avoids confusion if a different part of the
code wants to read the original input, and it avoids any chance of edit by
reference of the original callers copy of the variable.
#### Example
```golang
MyNotIdealFunc(s string, b bool) string {
if !b {
return s + "hey"
}
s = strings.Replace(s, "blah", "", -1) // not ideal (re-use of `s` var)
return s
}
MyOkayFunc(s string, b bool) string {
if !b {
return s + "hey"
}
s2 := strings.Replace(s, "blah", "", -1) // doesn't re-use `s` variable
return s2
}
MyGreatFunc(s string, b bool) string {
if !b {
return s + "hey"
}
return strings.Replace(s, "blah", "", -1) // even cleaner
}
```
### Constants in code
If a function takes a specifier (often a bool) it's sometimes better to name
that variable (often with a `const`) rather than leaving a naked `bool` in the
code. For example, `x := MyFoo("blah", false)` is less clear than
`const useMagic = false; x := MyFoo("blah", useMagic)`.
### Consistent ordering
In general we try to preserve a logical ordering in source files which usually
@@ -96,6 +153,23 @@ 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`.
For other aspects where you have a set of items, try to be internally consistent
as well. For example, if you have two switch statements with `A`, `B`, and `C`,
please use the same ordering for these elements elsewhere that they appear in
the code and in the commentary if it is not illogical to do so.
### Product identifiers
Try to avoid references in the code to `mgmt` or a specific program name string
if possible. This makes it easier to rename code if we ever pick a better name
or support `libmgmt` better if we embed it. You can use the `Program` variable
which is available in numerous places if you want a string to put in the logs.
It is also recommended to avoid the `go` (programming language name) string if
possible. Try to use `golang` if required, since the word `go` is already
overloaded, and in particular it was even already used by the
[`go!`](https://en.wikipedia.org/wiki/Go!_(programming_language)).
## Overview for mcl code
The `mcl` language is quite new, so this guide will probably change over time as

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2021+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
@@ -68,7 +68,8 @@ type AutoEdge interface {
Test([]bool) bool // call until false
}
// ResUID is a unique identifier for a resource, namely it's name, and the kind ("type").
// ResUID is a unique identifier for a resource, namely it's name, and the kind
// ("type").
type ResUID interface {
fmt.Stringer // String() string
@@ -104,9 +105,9 @@ func (obj *BaseUID) String() string {
}
// IFF looks at two UID's and if and only if they are equivalent, returns true.
// If they are not equivalent, it returns false.
// Most resources will want to override this method, since it does the important
// work of actually discerning if two resources are identical in function.
// If they are not equivalent, it returns false. Most resources will want to
// override this method, since it does the important work of actually discerning
// if two resources are identical in function.
func (obj *BaseUID) IFF(uid ResUID) bool {
res, ok := uid.(*BaseUID)
if !ok {

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2021+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2021+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
@@ -39,7 +39,7 @@ type GroupableRes interface {
SetAutoGroupMeta(*AutoGroupMeta)
// GroupCmp compares two resources and decides if they're suitable for
//grouping. This usually needs to be unique to your resource.
// grouping. This usually needs to be unique to your resource.
GroupCmp(res GroupableRes) error
// GroupRes groups resource argument (res) into self.

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2021+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2021+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2021+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2021+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2021+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2021+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2021+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2021+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
@@ -90,8 +90,8 @@ func AutoEdge(graph *pgraph.Graph, debug bool, logf func(format string, v ...int
}
}
// It would be great to ensure we didn't add any loops here, but instead
// of checking now, we'll move the check into the main loop.
// It would be great to ensure we didn't add any graph cycles here, but
// instead of checking now, we'll move the check into the main loop.
return nil
}

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2021+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
@@ -74,10 +74,10 @@ func (obj *wrappedGrouper) VertexCmp(v1, v2 pgraph.Vertex) error {
return fmt.Errorf("v2 is not a GroupableRes")
}
if r1.Kind() != r2.Kind() { // we must group similar kinds
// TODO: maybe future resources won't need this limitation?
return fmt.Errorf("the two resources aren't the same kind")
}
// Some resources of different kinds can now group together!
//if r1.Kind() != r2.Kind() { // we must group similar kinds
// return fmt.Errorf("the two resources aren't the same kind")
//}
// someone doesn't want to group!
if r1.AutoGroupMeta().Disabled || r2.AutoGroupMeta().Disabled {
return fmt.Errorf("one of the autogroup flags is false")

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2021+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
@@ -54,7 +54,7 @@ func AutoGroup(ag engine.AutoGrouper, g *pgraph.Graph, debug bool, logf func(for
logf("!VertexMerge for: %s into: %s", wStr, vStr)
} else { // success!
logf("success for: %s into: %s", wStr, vStr)
logf("%s into %s", wStr, vStr)
merged = true // woo
}
@@ -66,8 +66,8 @@ func AutoGroup(ag engine.AutoGrouper, g *pgraph.Graph, debug bool, logf func(for
}
}
// It would be great to ensure we didn't add any loops here, but instead
// of checking now, we'll move the check into the main loop.
// It would be great to ensure we didn't add any graph cycles here, but
// instead of checking now, we'll move the check into the main loop.
return nil
}

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2021+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
@@ -595,10 +595,11 @@ func TestPgraphGrouping11(t *testing.T) {
runGraphCmp(t, g1, g2)
}
// simple merge 1
/* simple merge 1
// a1 a2 a1,a2
// \ / >>> | (arrows point downwards)
// b b
*/
func TestPgraphGrouping12(t *testing.T) {
g1, _ := pgraph.NewGraph("g1") // original graph
{
@@ -620,10 +621,11 @@ func TestPgraphGrouping12(t *testing.T) {
runGraphCmp(t, g1, g2)
}
// simple merge 2
/* simple merge 2
// b b
// / \ >>> | (arrows point downwards)
// a1 a2 a1,a2
*/
func TestPgraphGrouping13(t *testing.T) {
g1, _ := pgraph.NewGraph("g1") // original graph
{
@@ -645,10 +647,11 @@ func TestPgraphGrouping13(t *testing.T) {
runGraphCmp(t, g1, g2)
}
// triple merge
/* triple merge
// a1 a2 a3 a1,a2,a3
// \ | / >>> | (arrows point downwards)
// b b
*/
func TestPgraphGrouping14(t *testing.T) {
g1, _ := pgraph.NewGraph("g1") // original graph
{
@@ -673,12 +676,13 @@ func TestPgraphGrouping14(t *testing.T) {
runGraphCmp(t, g1, g2)
}
// chain merge
/* chain merge
// a1 a1
// / \ |
// b1 b2 >>> b1,b2 (arrows point downwards)
// \ / |
// c1 c1
*/
func TestPgraphGrouping15(t *testing.T) {
g1, _ := pgraph.NewGraph("g1") // original graph
{
@@ -708,7 +712,7 @@ func TestPgraphGrouping15(t *testing.T) {
runGraphCmp(t, g1, g2)
}
// re-attach 1 (outer)
/* re-attach 1 (outer)
// technically the second possibility is valid too, depending on which order we
// merge edges in, and if we don't filter out any unnecessary edges afterwards!
// a1 a2 a1,a2 a1,a2
@@ -716,6 +720,7 @@ func TestPgraphGrouping15(t *testing.T) {
// b1 / >>> b1 OR b1 / (arrows point downwards)
// | / | | /
// c1 c1 c1
*/
func TestPgraphGrouping16(t *testing.T) {
g1, _ := pgraph.NewGraph("g1") // original graph
{
@@ -743,12 +748,13 @@ func TestPgraphGrouping16(t *testing.T) {
runGraphCmp(t, g1, g2)
}
// re-attach 2 (inner)
/* re-attach 2 (inner)
// a1 b2 a1
// | / |
// b1 / >>> b1,b2 (arrows point downwards)
// | / |
// c1 c1
*/
func TestPgraphGrouping17(t *testing.T) {
g1, _ := pgraph.NewGraph("g1") // original graph
{
@@ -776,13 +782,14 @@ func TestPgraphGrouping17(t *testing.T) {
runGraphCmp(t, g1, g2)
}
// re-attach 3 (double)
/* re-attach 3 (double)
// similar to "re-attach 1", technically there is a second possibility for this
// a2 a1 b2 a1,a2
// \ | / |
// \ b1 / >>> b1,b2 (arrows point downwards)
// \ | / |
// c1 c1
*/
func TestPgraphGrouping18(t *testing.T) {
g1, _ := pgraph.NewGraph("g1") // original graph
{
@@ -813,10 +820,11 @@ func TestPgraphGrouping18(t *testing.T) {
runGraphCmp(t, g1, g2)
}
// connected merge 0, (no change!)
/* connected merge 0, (no change!)
// a1 a1
// \ >>> \ (arrows point downwards)
// a2 a2
*/
func TestPgraphGroupingConnected0(t *testing.T) {
g1, _ := pgraph.NewGraph("g1") // original graph
{
@@ -835,12 +843,13 @@ func TestPgraphGroupingConnected0(t *testing.T) {
runGraphCmp(t, g1, g2)
}
// connected merge 1, (no change!)
/* connected merge 1, (no change!)
// a1 a1
// \ \
// b >>> b (arrows point downwards)
// \ \
// a2 a2
*/
func TestPgraphGroupingConnected1(t *testing.T) {
g1, _ := pgraph.NewGraph("g1") // original graph
{

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2021+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
@@ -89,6 +89,19 @@ func (ag *baseGrouper) VertexNext() (v1, v2 pgraph.Vertex, err error) {
ag.done = true
}
}
// TODO: is this index swap better or even valid?
//if ag.i < l {
// ag.i++
//}
//if ag.i == l {
// ag.i = 0
// if ag.j < l {
// ag.j++
// }
// if ag.j == l {
// ag.done = true
// }
//}
return
}
@@ -110,7 +123,7 @@ func (ag *baseGrouper) VertexMerge(v1, v2 pgraph.Vertex) (v pgraph.Vertex, err e
return nil, fmt.Errorf("vertexMerge needs to be overridden")
}
// EdgeMerge can be overridden, since it just simple returns the first edge.
// EdgeMerge can be overridden, since it just simply returns the first edge.
func (ag *baseGrouper) EdgeMerge(e1, e2 pgraph.Edge) pgraph.Edge {
return e1 // noop
}

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2021+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2021+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
@@ -25,9 +25,9 @@ import (
"github.com/purpleidea/mgmt/util/errwrap"
)
// VertexMerge merges v2 into v1 by reattaching the edges where appropriate,
// and then by deleting v2 from the graph. Since more than one edge between two
// vertices is not allowed, duplicate edges are merged as well. an edge merge
// VertexMerge merges v2 into v1 by reattaching the edges where appropriate, and
// then by deleting v2 from the graph. Since more than one edge between two
// vertices is not allowed, duplicate edges are merged as well. An edge merge
// function can be provided if you'd like to control how you merge the edges!
func VertexMerge(g *pgraph.Graph, v1, v2 pgraph.Vertex, vertexMergeFn func(pgraph.Vertex, pgraph.Vertex) (pgraph.Vertex, error), edgeMergeFn func(pgraph.Edge, pgraph.Edge) pgraph.Edge) error {
// methodology

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2021+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
@@ -191,7 +191,7 @@ func (obj *Engine) Commit() error {
obj.waits[vertex] = &sync.WaitGroup{}
obj.state[vertex] = &State{
//Graph: obj.graph, // TODO: what happens if we swap the graph?
Graph: obj.graph, // Update if we swap the graph!
Vertex: vertex,
Program: obj.Program,
@@ -329,14 +329,14 @@ func (obj *Engine) Commit() error {
// the changes that we'd made to the previously primary graph. This is
// because this function is meant to atomically swap the graphs safely.
// TODO: update all the `State` structs with the new Graph pointer
//for _, vertex := range obj.graph.Vertices() {
// state, exists := obj.state[vertex]
// if !exists {
// continue
// }
// state.Graph = obj.graph // update pointer to graph
//}
// Update all the `State` structs with the new Graph pointer.
for _, vertex := range obj.graph.Vertices() {
state, exists := obj.state[vertex]
if !exists {
continue
}
state.Graph = obj.graph // update pointer to graph
}
return nil
}

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2021+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2021+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2021+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
@@ -150,8 +150,8 @@ func (obj *Engine) Reversals() error {
}
// TODO: Do we want a way for stored reversals to add edges too?
// It would be great to ensure we didn't add any loops here, but instead
// of checking now, we'll move the check into the main loop.
// It would be great to ensure we didn't add any graph cycles here, but
// instead of checking now, we'll move the check into the main loop.
return nil
}
@@ -291,5 +291,10 @@ func (obj *State) ReversalDelete() error {
}
file := path.Join(dir, ReverseFile) // return a unique file
return errwrap.Wrapf(os.Remove(file), "could not remove reverse state file")
// FIXME: why do we see these removals when there isn't a state file?
if err = os.Remove(file); os.IsNotExist(err) {
return nil // ignore missing files
}
return errwrap.Wrapf(err, "could not remove reverse state file")
}

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2021+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2021+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2021+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2021+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
@@ -32,7 +32,7 @@ import (
// State stores some state about the resource it is mapped to.
type State struct {
// Graph is a pointer to the graph that this vertex is part of.
//Graph pgraph.Graph
Graph *pgraph.Graph
// Vertex is the pointer in the graph that this state corresponds to. It
// can be converted to a `Res` if necessary.
@@ -169,25 +169,63 @@ func (obj *State) Init() error {
}
return res.Refresh()
},
Send: func(st interface{}) error {
res, ok := obj.Vertex.(engine.SendableRes)
if !ok {
panic("res does not support the Sendable trait")
}
// XXX: type check this
//expected := res.Sends()
//if err := XXX_TYPE_CHECK(expected, st); err != nil {
// return err
//}
return res.Send(st) // send the struct
},
Recv: func() map[string]*engine.Send { // TODO: change this API?
res, ok := obj.Vertex.(engine.RecvableRes)
if !ok {
panic("res does not support the Recvable trait")
Send: engine.GenerateSendFunc(res),
Recv: engine.GenerateRecvFunc(res),
// FIXME: pass in a safe, limited query func instead?
// TODO: not implemented, use FilteredGraph
//Graph: func() *pgraph.Graph {
// _, ok := obj.Vertex.(engine.CanGraphQueryRes)
// if !ok {
// panic("res does not support the GraphQuery trait")
// }
// return obj.Graph // we return in a func so it's fresh!
//},
FilteredGraph: func() (*pgraph.Graph, error) {
graph, err := pgraph.NewGraph("filtered")
if err != nil {
return nil, errwrap.Wrapf(err, "could not create graph")
}
return res.Recv()
// filter graph and build a new one...
adjacency := obj.Graph.Adjacency()
for v1 := range adjacency {
// check we're allowed
r1, ok := v1.(engine.GraphQueryableRes)
if !ok {
continue
}
// pass in information on requestor...
if err := r1.GraphQueryAllowed(
engine.GraphQueryableOptionKind(res.Kind()),
engine.GraphQueryableOptionName(res.Name()),
// TODO: add more information...
); err != nil {
continue
}
graph.AddVertex(v1)
for v2, edge := range adjacency[v1] {
r2, ok := v2.(engine.GraphQueryableRes)
if !ok {
continue
}
// pass in information on requestor...
if err := r2.GraphQueryAllowed(
engine.GraphQueryableOptionKind(res.Kind()),
engine.GraphQueryableOptionName(res.Name()),
// TODO: add more information...
); err != nil {
continue
}
//graph.AddVertex(v2) // redundant
graph.AddEdge(v1, v2, edge)
}
}
return graph, nil // we return in a func so it's fresh!
},
World: obj.World,

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2021+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify

70
engine/graphqueryable.go Normal file
View File

@@ -0,0 +1,70 @@
// Mgmt
// Copyright (C) 2013-2021+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> 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 <http://www.gnu.org/licenses/>.
package engine
// GraphQueryableRes is the interface that must be implemented if you want your
// resource to be allowed to be queried from another resource in the graph. This
// is done as a form of explicit authorization tracking so that we can consider
// security aspects more easily. Ultimately, all resource code should be
// trusted, but it's still a good idea to know if a particular resource is even
// able to access information about another one, and if your resource doesn't
// add the trait supporting this, then it won't be allowed.
type GraphQueryableRes interface {
Res // implement everything in Res but add the additional requirements
// GraphQueryAllowed returns nil if you're allowed to query the graph.
GraphQueryAllowed(...GraphQueryableOption) error
}
// GraphQueryableOption is an option that can be used to specify the
// authentication.
type GraphQueryableOption func(*GraphQueryableOptions)
// GraphQueryableOptions represents the different possible configurable options.
type GraphQueryableOptions struct {
// Kind is the kind of the resource making the access.
Kind string
// Name is the name of the resource making the access.
Name string
// TODO: add more options if needed
}
// Apply is a helper function to apply a list of options to the struct. You
// should initialize it with defaults you want, and then apply any you've
// received like this.
func (obj *GraphQueryableOptions) Apply(opts ...GraphQueryableOption) {
for _, optionFunc := range opts { // apply the options
optionFunc(obj)
}
}
// GraphQueryableOptionKind tells the GraphQueryAllowed function what the
// resource kind is.
func GraphQueryableOptionKind(kind string) GraphQueryableOption {
return func(gqo *GraphQueryableOptions) {
gqo.Kind = kind
}
}
// GraphQueryableOptionName tells the GraphQueryAllowed function what the
// resource name is.
func GraphQueryableOptionName(name string) GraphQueryableOption {
return func(gqo *GraphQueryableOptions) {
gqo.Name = name
}
}

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2021+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2021+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2021+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2021+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
@@ -21,6 +21,7 @@ import (
"encoding/gob"
"fmt"
"github.com/purpleidea/mgmt/pgraph"
"github.com/purpleidea/mgmt/util/errwrap"
"gopkg.in/yaml.v2"
@@ -29,8 +30,8 @@ import (
// TODO: should each resource be a sub-package?
var registeredResources = map[string]func() Res{}
// RegisterResource registers a new resource by providing a constructor
// function that returns a resource object ready to be unmarshalled from YAML.
// RegisterResource registers a new resource by providing a constructor function
// that returns a resource object ready to be unmarshalled from YAML.
func RegisterResource(kind string, fn func() Res) {
f := fn()
if kind == "" {
@@ -120,6 +121,20 @@ type Init struct {
// Other functionality:
// Graph is a function that returns the current graph. The returned
// value won't be valid after a graphsync so make sure to call this when
// you are about to use it, and discard it right after.
// FIXME: it might be better to offer a safer, more limited, GraphQuery?
//Graph func() *pgraph.Graph // TODO: not implemented, use FilteredGraph
// FilteredGraph is a function that returns a filtered variant of the
// current graph. Only resource that have allowed themselves to be added
// into this graph will appear. If they did not consent, then those
// vertices and any associated edges, will not be present.
FilteredGraph func() (*pgraph.Graph, error)
// TODO: GraphQuery offers an interface to query the resource graph.
// World provides a connection to the outside world. This is most often
// used for communicating with the distributed database.
World World
@@ -227,8 +242,8 @@ func Validate(res Res) error {
// the Interrupt method to shutdown the resource quickly. Running this method
// may leave the resource in a partial state, however this may be desired if you
// want a faster exit or if you'd prefer a partial state over letting the
// resource complete in a situation where you made an error and you wish to
// exit quickly to avoid data loss. It is usually triggered after multiple ^C
// resource complete in a situation where you made an error and you wish to exit
// quickly to avoid data loss. It is usually triggered after multiple ^C
// signals.
type InterruptableRes interface {
Res

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2021+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
@@ -124,8 +124,8 @@ func (obj *AugeasRes) Close() error {
return nil
}
// Watch is the primary listener for this resource and it outputs events.
// Taken from the File resource.
// Watch is the primary listener for this resource and it outputs events. This
// was taken from the File resource.
// FIXME: DRY - This is taken from the file resource
func (obj *AugeasRes) Watch() error {
var err error
@@ -301,8 +301,8 @@ func (obj *AugeasRes) UIDs() []engine.ResUID {
return []engine.ResUID{x}
}
// UnmarshalYAML is the custom unmarshal handler for this struct.
// It is primarily useful for setting the defaults.
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
// primarily useful for setting the defaults.
func (obj *AugeasRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
type rawRes AugeasRes // indirection to avoid infinite recursion

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2021+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
@@ -121,8 +121,8 @@ const (
)
// AwsRegions is a list of all AWS regions generated using ec2.DescribeRegions.
// cn-north-1 and us-gov-west-1 are not returned, probably due to security.
// List available at http://docs.aws.amazon.com/general/latest/gr/rande.html
// cn-north-1 and us-gov-west-1 are not returned, probably due to security. List
// available at http://docs.aws.amazon.com/general/latest/gr/rande.html
var AwsRegions = []string{
"ap-northeast-1",
"ap-northeast-2",
@@ -187,7 +187,8 @@ type AwsEc2Res struct {
InstanceID string
}
// chanStruct defines the type for a channel used to pass events and errors to watch.
// chanStruct defines the type for a channel used to pass events and errors to
// watch.
type chanStruct struct {
event awsEc2Event
state string
@@ -233,7 +234,8 @@ type ruleDetail struct {
State []string `json:"state"`
}
// postData is the format of the messages received and decoded by snsPostHandler().
// postData is the format of the messages received and decoded by
// snsPostHandler().
type postData struct {
Type string `json:"Type"`
MessageID string `json:"MessageId"`
@@ -247,7 +249,8 @@ type postData struct {
SigningCertURL string `json:"SigningCertURL"`
}
// postMsg is used to unmarshal the postData message if it's an event notification.
// postMsg is used to unmarshal the postData message if it's an event
// notification.
type postMsg struct {
InstanceID string `json:"instance-id"`
State string `json:"state"`
@@ -413,7 +416,8 @@ func (obj *AwsEc2Res) Watch() error {
return obj.longpollWatch()
}
// longpollWatch uses the ec2 api's built in methods to watch ec2 resource state.
// longpollWatch uses the ec2 api's built in methods to watch ec2 resource
// state.
func (obj *AwsEc2Res) longpollWatch() error {
send := false
@@ -510,10 +514,10 @@ func (obj *AwsEc2Res) longpollWatch() error {
}
// snsWatch uses amazon's SNS and CloudWatchEvents APIs to get instance state-
// change notifications pushed to the http endpoint (snsServer) set up below.
// In Init() a CloudWatch rule is created along with a corresponding SNS topic
// that it can publish to. snsWatch creates an http server which listens for
// messages published to the topic and processes them accordingly.
// change notifications pushed to the http endpoint (snsServer) set up below. In
// Init() a CloudWatch rule is created along with a corresponding SNS topic that
// it can publish to. snsWatch creates an http server which listens for messages
// published to the topic and processes them accordingly.
func (obj *AwsEc2Res) snsWatch() error {
send := false
defer obj.wg.Wait()
@@ -795,8 +799,8 @@ type AwsEc2UID struct {
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.
// 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 *AwsEc2Res) UIDs() []engine.ResUID {
x := &AwsEc2UID{
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
@@ -805,8 +809,8 @@ func (obj *AwsEc2Res) UIDs() []engine.ResUID {
return []engine.ResUID{x}
}
// UnmarshalYAML is the custom unmarshal handler for this struct.
// It is primarily useful for setting the defaults.
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
// primarily useful for setting the defaults.
func (obj *AwsEc2Res) UnmarshalYAML(unmarshal func(interface{}) error) error {
type rawRes AwsEc2Res // indirection to avoid infinite recursion
@@ -942,8 +946,8 @@ func (obj *AwsEc2Res) snsVerifySignature(post postData) error {
return nil
}
// snsGetCert downloads and parses the signing certificate from the provided
// URL for message verification.
// snsGetCert downloads and parses the signing certificate from the provided URL
// for message verification.
func (obj *AwsEc2Res) snsGetCert(url string) (*x509.Certificate, error) {
// only download valid certificates from amazon
matchURL, err := regexp.MatchString(SnsCertURLRegex, url)
@@ -1035,8 +1039,8 @@ func (obj *AwsEc2Res) snsDeleteTopic(topicArn string) error {
return nil
}
// snsSubscribe subscribes the endpoint to the sns topic.
// Returning SubscriptionArn here is useless as it is still pending confirmation.
// snsSubscribe subscribes the endpoint to the sns topic. Returning
// SubscriptionArn here is useless as it is still pending confirmation.
func (obj *AwsEc2Res) snsSubscribe(endpoint string, topicArn string) error {
// subscribe to the topic
subInput := &sns.SubscribeInput{
@@ -1052,8 +1056,8 @@ func (obj *AwsEc2Res) snsSubscribe(endpoint string, topicArn string) error {
return nil
}
// snsConfirmSubscription confirms the sns subscription.
// Returning SubscriptionArn here is useless as it is still pending confirmation.
// snsConfirmSubscription confirms the sns subscription. Returning
// SubscriptionArn here is useless as it is still pending confirmation.
func (obj *AwsEc2Res) snsConfirmSubscription(topicArn string, token string) error {
// confirm the subscription
csInput := &sns.ConfirmSubscriptionInput{
@@ -1105,7 +1109,8 @@ func (obj *AwsEc2Res) snsProcessEvent(message, instanceName string) (awsEc2Event
return awsEc2EventNone, nil
}
// snsAuthorize adds the necessary permission for cloudwatch to publish to the SNS topic.
// snsAuthorize adds the necessary permission for cloudwatch to publish to the
// SNS topic.
func (obj *AwsEc2Res) snsAuthorizeCloudWatch(topicArn string) error {
// get the topic attributes, including the security policy
gaInput := &sns.GetTopicAttributesInput{

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2021+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
@@ -229,8 +229,8 @@ func (obj *ConfigEtcdRes) Interrupt() error {
return nil
}
// UnmarshalYAML is the custom unmarshal handler for this struct.
// It is primarily useful for setting the defaults.
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
// primarily useful for setting the defaults.
func (obj *ConfigEtcdRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
type rawRes ConfigEtcdRes // indirection to avoid infinite recursion

View File

@@ -0,0 +1,283 @@
// Mgmt
// Copyright (C) 2013-2021+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> 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 <http://www.gnu.org/licenses/>.
package resources
import (
"context"
"fmt"
"net/url"
"sync"
"github.com/purpleidea/mgmt/engine"
"github.com/purpleidea/mgmt/engine/traits"
"github.com/purpleidea/mgmt/util"
"github.com/purpleidea/mgmt/util/errwrap"
"github.com/hashicorp/consul/api"
)
func init() {
engine.RegisterResource("consul:kv", func() engine.Res { return &ConsulKVRes{} })
}
// ConsulKVRes is a resource that writes a value into a Consul datastore. The
// name of the resource can either be the key name, or the concatenation of the
// server address and the key name: http://127.0.0.1:8500/my-key. If the param
// keys are specified, then those are used. If the Name cannot be properly
// parsed by url.Parse, then it will be considered as the Key's value. If the
// Key is specified explicitly, then we won't use anything from the Name.
type ConsulKVRes struct {
traits.Base
init *engine.Init
// Key is the name of the key. Defaults to the name of the resource.
Key string `lang:"key" yaml:"key"`
// Value is the value for the key.
Value string `lang:"value" yaml:"value"`
// Scheme is the URI scheme for the Consul server. Default: http.
Scheme string `lang:"scheme" yaml:"scheme"`
// Address is the address of the Consul server. Default: 127.0.0.1:8500.
Address string `lang:"address" yaml:"address"`
// Token is used to provide an ACL token to use for this resource.
Token string `lang:"token" yaml:"token"`
client *api.Client
config *api.Config // needed to close the idle connections
once bool // safety token
key string // cache the key name to avoid re-running the parser
}
// Default returns some sensible defaults for this resource.
func (obj *ConsulKVRes) Default() engine.Res {
return &ConsulKVRes{}
}
// Validate if the params passed in are valid data.
func (obj *ConsulKVRes) Validate() error {
s, _, k := obj.inputParser()
if k == "" {
return fmt.Errorf("the Key is empty")
}
if s != "" && s != "http" && s != "https" {
return fmt.Errorf("unknown Scheme")
}
return nil
}
// Init runs some startup code for this resource.
func (obj *ConsulKVRes) Init(init *engine.Init) error {
obj.init = init // save for later
s, a, k := obj.inputParser()
obj.config = api.DefaultConfig()
if s != "" {
obj.config.Scheme = s
}
if a != "" {
obj.config.Address = obj.Address
}
obj.key = k // store the key
obj.init.Logf("using consul key: %s", obj.key)
if obj.Token != "" {
obj.config.Token = obj.Token
}
var err error
obj.client, err = api.NewClient(obj.config)
return errwrap.Wrapf(err, "could not create Consul client")
}
// Close is run by the engine to clean up after the resource is done.
func (obj *ConsulKVRes) Close() error {
if obj.config != nil && obj.config.Transport != nil {
obj.config.Transport.CloseIdleConnections()
}
return nil
}
// Watch is the listener and main loop for this resource and it outputs events.
func (obj *ConsulKVRes) Watch() error {
wg := &sync.WaitGroup{}
defer wg.Wait()
ch := make(chan error)
exit := make(chan struct{})
kv := obj.client.KV()
wg.Add(1)
go func() {
defer close(ch)
defer wg.Done()
opts := &api.QueryOptions{RequireConsistent: true}
ctx, cancel := util.ContextWithCloser(context.Background(), exit)
defer cancel()
opts = opts.WithContext(ctx)
for {
_, meta, err := kv.Get(obj.key, opts)
select {
case ch <- err: // send
if err != nil {
return
}
// WaitIndex = 0, which means that it is the
// first time we run the query, as we are about
// to change the WaitIndex to make a blocking
// query, we can consider the watch started.
opts.WaitIndex = meta.LastIndex
if opts.WaitIndex != 0 {
continue
}
if !obj.once {
obj.init.Running()
obj.once = true
continue
}
// Unexpected situation, bug in consul API...
select {
case ch <- fmt.Errorf("unexpected behaviour in Consul API"):
case <-obj.init.Done: // signal for shutdown request
}
case <-obj.init.Done: // signal for shutdown request
}
return
}
}()
defer close(exit)
for {
select {
case err, ok := <-ch:
if !ok { // channel shutdown
return nil
}
if err != nil {
return errwrap.Wrapf(err, "unknown %s watcher error", obj)
}
if obj.init.Debug {
obj.init.Logf("event!")
}
obj.init.Event()
case <-obj.init.Done: // signal for shutdown request
return nil
}
}
}
// CheckApply is run to check the state and, if apply is true, to apply the
// 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 *ConsulKVRes) CheckApply(apply bool) (bool, error) {
if obj.init.Debug {
obj.init.Logf("consul key: %s", obj.key)
}
kv := obj.client.KV()
pair, _, err := kv.Get(obj.key, nil)
if err != nil {
return false, err
}
if pair != nil && string(pair.Value) == obj.Value {
return true, nil
}
if !apply {
return false, nil
}
p := &api.KVPair{Key: obj.key, Value: []byte(obj.Value)}
_, err = kv.Put(p, nil)
return false, err
}
// Cmp compares two resources and return if they are equivalent.
func (obj *ConsulKVRes) Cmp(r engine.Res) error {
res, ok := r.(*ConsulKVRes)
if !ok {
return fmt.Errorf("not a %s", obj.Kind())
}
if obj.Key != res.Key {
return fmt.Errorf("the Key param differs")
}
if obj.Value != res.Value {
return fmt.Errorf("the Value param differs")
}
if obj.Scheme != res.Scheme {
return fmt.Errorf("the Scheme param differs")
}
if obj.Address != res.Address {
return fmt.Errorf("the Address param differs")
}
if obj.Token != res.Token {
return fmt.Errorf("the Token param differs")
}
return nil
}
// inputParser parses the Name() of a resource and extracts the scheme, address,
// and key name of a consul key. We don't have an error, because if we have one,
// then it means the input must be a raw key. Output of this function is scheme,
// address (includes hostname and port), and key. This also takes our parameters
// in to account, and applies the correct overrides if they are specified there.
func (obj *ConsulKVRes) inputParser() (string, string, string) {
// If the key is specified explicitly, then we're not going to parse the
// resource name for a pattern, and we use our given params as they are.
if obj.Key != "" {
return obj.Scheme, obj.Address, obj.Key
}
// Now we parse...
u, err := url.Parse(obj.Name())
if err != nil {
// If this didn't work, then we know it's explicitly a raw key.
return obj.Scheme, obj.Address, obj.Name()
}
// Otherwise, we use the parse result, and we overwrite any of the
// fields if we have an explicit param that was specified.
k := u.Path
s := u.Scheme
a := u.Host
//if obj.Key != "" { // this is now guaranteed to never happen
// k = obj.Key
//}
if obj.Scheme != "" {
s = obj.Scheme
}
if obj.Address != "" {
a = obj.Address
}
return s, a, k
}

View File

@@ -0,0 +1,71 @@
// Mgmt
// Copyright (C) 2013-2021+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> 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 <http://www.gnu.org/licenses/>.
package resources
import (
"fmt"
"testing"
"github.com/purpleidea/mgmt/engine"
)
func createConsulRes(name string) *ConsulKVRes {
r, err := engine.NewNamedResource("consul:kv", name)
if err != nil {
panic(fmt.Sprintf("could not create resource: %+v", err))
}
res := r.(*ConsulKVRes) // if this panics, the test will panic
return res
}
func TestParseConsulName(t *testing.T) {
n1 := "test"
r1 := createConsulRes(n1)
if s, a, k := r1.inputParser(); s != "" || a != "" || k != "test" {
t.Errorf("unexpected output while parsing `%s`: %s, %s, %s", n1, s, a, k)
}
n2 := "http://127.0.0.1:8500/test"
r2 := createConsulRes(n2)
if s, a, k := r2.inputParser(); s != "http" || a != "127.0.0.1:8500" || k != "/test" {
t.Errorf("unexpected output while parsing `%s`: %s, %s, %s", n2, s, a, k)
}
n3 := "http://127.0.0.1:8500/test"
r3 := createConsulRes(n3)
r3.Scheme = "https"
r3.Address = "example.com"
if s, a, k := r3.inputParser(); s != "https" || a != "example.com" || k != "/test" {
t.Errorf("unexpected output while parsing `%s`: %s, %s, %s", n3, s, a, k)
}
n4 := "http:://127.0.0.1..5:8500/test" // wtf, url.Parse is on drugs...
r4 := createConsulRes(n4)
//if s, a, k := r4.inputParser(); s != "" || a != "" || k != n4 { // what i really expect
if s, a, k := r4.inputParser(); s != "http" || a != "" || k != "" { // what i get
t.Errorf("unexpected output while parsing `%s`: %s, %s, %s", n4, s, a, k)
}
n5 := "http://127.0.0.1:8500/test" // whatever, it's ignored
r5 := createConsulRes(n3)
r5.Key = "some key"
if s, a, k := r5.inputParser(); s != "" || a != "" || k != "some key" {
t.Errorf("unexpected output while parsing `%s`: %s, %s, %s", n5, s, a, k)
}
}

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2021+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
@@ -139,9 +139,9 @@ func (obj *CronRes) Default() engine.Res {
}
}
// makeComposite creates a pointer to a FileRes. The pointer is used to
// validate and initialize the nested file resource and to apply the file state
// in CheckApply.
// makeComposite creates a pointer to a FileRes. The pointer is used to validate
// and initialize the nested file resource and to apply the file state in
// CheckApply.
func (obj *CronRes) makeComposite() (*FileRes, error) {
p, err := obj.UnitFilePath()
if err != nil {
@@ -466,8 +466,8 @@ func (obj *CronRes) AutoEdges() (engine.AutoEdge, error) {
return nil, nil
}
// UIDs includes all params to make a unique identification of this object.
// Most resources only return one although some resources can return multiple.
// 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 *CronRes) UIDs() []engine.ResUID {
unit := fmt.Sprintf("%s.service", obj.Name())
if obj.Unit != "" {
@@ -486,8 +486,8 @@ func (obj *CronRes) UIDs() []engine.ResUID {
return uids
}
// UnmarshalYAML is the custom unmarshal handler for this struct.
// It is primarily useful for setting the defaults.
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
// primarily useful for setting the defaults.
func (obj *CronRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
type rawRes CronRes // indirection to avoid infinite recursion

1177
engine/resources/dhcp.go Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2021+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
@@ -50,8 +50,8 @@ const (
// initCtxTimeout is the length of time, in seconds, before requests are
// cancelled in Init.
initCtxTimeout = 20
// checkApplyCtxTimeout is the length of time, in seconds, before requests
// are cancelled in CheckApply.
// checkApplyCtxTimeout is the length of time, in seconds, before
// requests are cancelled in CheckApply.
checkApplyCtxTimeout = 120
)
@@ -74,11 +74,12 @@ type DockerContainerRes struct {
Env []string `yaml:"env"`
// Ports is a map of port bindings. E.g. {"tcp" => {80 => 8080},}.
Ports map[string]map[int64]int64 `yaml:"ports"`
// APIVersion allows you to override the host's default client API version.
// APIVersion allows you to override the host's default client API
// version.
APIVersion string `yaml:"apiversion"`
// Force, if true, will destroy and redeploy the container if the image is
// incorrect.
// Force, if true, this will destroy and redeploy the container if the
// image is incorrect.
Force bool `yaml:"force"`
client *client.Client // docker api client
@@ -88,7 +89,9 @@ type DockerContainerRes struct {
// Default returns some sensible defaults for this resource.
func (obj *DockerContainerRes) Default() engine.Res {
return &DockerContainerRes{}
return &DockerContainerRes{
State: "running",
}
}
// Validate if the params passed in are valid data.
@@ -98,6 +101,11 @@ func (obj *DockerContainerRes) Validate() error {
return fmt.Errorf("state must be running, stopped or removed")
}
// make sure an image is specified
if obj.Image == "" {
return fmt.Errorf("image must be specified")
}
// validate env
for _, env := range obj.Env {
if !strings.Contains(env, "=") || strings.Contains(env, " ") {
@@ -140,7 +148,7 @@ func (obj *DockerContainerRes) Init(init *engine.Init) error {
defer cancel()
// Initialize the docker client.
obj.client, err = client.NewClient(client.DefaultDockerHost, obj.APIVersion, nil, nil)
obj.client, err = client.NewClientWithOpts(client.WithVersion(obj.APIVersion))
if err != nil {
return errwrap.Wrapf(err, "error creating docker client")
}
@@ -302,7 +310,7 @@ func (obj *DockerContainerRes) CheckApply(apply bool) (bool, error) {
}
}
c, err := obj.client.ContainerCreate(ctx, containerConfig, hostConfig, nil, obj.Name())
c, err := obj.client.ContainerCreate(ctx, containerConfig, hostConfig, nil, nil, obj.Name())
if err != nil {
return false, errwrap.Wrapf(err, "error creating container")
}
@@ -367,52 +375,105 @@ func (obj *DockerContainerRes) Cmp(r engine.Res) error {
if !ok {
return fmt.Errorf("error casting r to *DockerContainerRes")
}
if obj.Name() != res.Name() {
return fmt.Errorf("names differ")
if obj.State != res.State {
return fmt.Errorf("the State differs")
}
if obj.Image != res.Image {
return fmt.Errorf("the Image differs")
}
if err := util.SortedStrSliceCompare(obj.Cmd, res.Cmd); err != nil {
return errwrap.Wrapf(err, "cmd differs")
return errwrap.Wrapf(err, "the Cmd field differs")
}
if err := util.SortedStrSliceCompare(obj.Env, res.Env); err != nil {
return errwrap.Wrapf(err, "env differs")
return errwrap.Wrapf(err, "tne Env field differs")
}
if len(obj.Ports) != len(res.Ports) {
return fmt.Errorf("ports length differs")
return fmt.Errorf("the Ports length differs")
}
for k, v := range obj.Ports {
for p, q := range v {
if w, ok := res.Ports[k][p]; !ok || q != w {
return fmt.Errorf("ports differ")
return fmt.Errorf("the Ports field differs")
}
}
}
if obj.APIVersion != res.APIVersion {
return fmt.Errorf("apiversions differ")
return fmt.Errorf("the APIVersion differs")
}
if obj.Force != res.Force {
return fmt.Errorf("forces differ")
return fmt.Errorf("the Force field differs")
}
return nil
}
// DockerUID is the UID struct for DockerContainerRes.
type DockerUID struct {
// DockerContainerUID is the UID struct for DockerContainerRes.
type DockerContainerUID struct {
engine.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.
// DockerContainerResAutoEdges holds the state of the auto edge generator.
type DockerContainerResAutoEdges struct {
UIDs []engine.ResUID
pointer int
}
// AutoEdges returns edges to any docker:image resource that matches the image
// specified in the docker:container resource definition.
func (obj *DockerContainerRes) AutoEdges() (engine.AutoEdge, error) {
var result []engine.ResUID
var reversed bool
if obj.State != "removed" {
reversed = true
}
result = append(result, &DockerImageUID{
BaseUID: engine.BaseUID{
Reversed: &reversed,
},
image: dockerImageNameTag(obj.Image),
})
return &DockerContainerResAutoEdges{
UIDs: result,
pointer: 0,
}, nil
}
// Next returnes the next automatic edge.
func (obj *DockerContainerResAutoEdges) Next() []engine.ResUID {
if len(obj.UIDs) == 0 {
return nil
}
value := obj.UIDs[obj.pointer]
obj.pointer++
return []engine.ResUID{value}
}
// Test gets results of the earlier Next() call, & returns if we should
// continue.
func (obj *DockerContainerResAutoEdges) Test(input []bool) bool {
if len(obj.UIDs) <= obj.pointer {
return false
}
if len(input) != 1 { // in case we get given bad data
panic(fmt.Sprintf("Expecting a single value!"))
}
return true // keep going
}
// 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 *DockerContainerRes) UIDs() []engine.ResUID {
x := &DockerUID{
x := &DockerContainerUID{
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
name: obj.Name(),
}
return []engine.ResUID{x}
}
// UnmarshalYAML is the custom unmarshal handler for this struct.
// It is primarily useful for setting the defaults.
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
// primarily useful for setting the defaults.
func (obj *DockerContainerRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
type rawRes DockerContainerRes // indirection to avoid infinite recursion

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2021+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
@@ -165,6 +165,7 @@ func setup() error {
},
&container.HostConfig{},
nil,
nil,
"mgmt-test",
)
if err != nil {

View File

@@ -0,0 +1,295 @@
// Mgmt
// Copyright (C) 2013-2021+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> 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 <http://www.gnu.org/licenses/>.
// +build !nodocker
package resources
import (
"context"
"fmt"
"io/ioutil"
"regexp"
"strings"
"time"
"github.com/purpleidea/mgmt/engine"
"github.com/purpleidea/mgmt/engine/traits"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/client"
errwrap "github.com/pkg/errors"
)
const (
// dockerImageInitCtxTimeout is the length of time, in seconds, before
// requests are cancelled in Init.
dockerImageInitCtxTimeout = 20
// dockerImageCheckApplyCtxTimeout is the length of time, in seconds,
// before requests are cancelled in CheckApply.
dockerImageCheckApplyCtxTimeout = 120
)
func init() {
engine.RegisterResource("docker:image", func() engine.Res { return &DockerImageRes{} })
}
// DockerImageRes is a docker image resource. The resource's name must be a
// docker image in any supported format (url, image, or image:tag).
type DockerImageRes struct {
traits.Base // add the base methods without re-implementation
traits.Edgeable
// State of the image must be exists or absent.
State string `yaml:"state"`
// APIVersion allows you to override the host's default client API
// version.
APIVersion string `yaml:"apiversion"`
image string // full image:tag format
client *client.Client // docker api client
init *engine.Init
}
// Default returns some sensible defaults for this resource.
func (obj *DockerImageRes) Default() engine.Res {
return &DockerImageRes{
// TODO: eventually if image supports other properties, this can
// be left out and we could have the state be "unmanaged".
State: "exists",
}
}
// Validate if the params passed in are valid data.
func (obj *DockerImageRes) Validate() error {
// validate state
if obj.State != "exists" && obj.State != "absent" {
return fmt.Errorf("state must be exists or absent")
}
// validate APIVersion
if obj.APIVersion != "" {
verOK, err := regexp.MatchString(`^(v)[1-9]\.[0-9]\d*$`, obj.APIVersion)
if err != nil {
return errwrap.Wrapf(err, "error matching apiversion string")
}
if !verOK {
return fmt.Errorf("invalid apiversion: %s", obj.APIVersion)
}
}
return nil
}
// Init runs some startup code for this resource.
func (obj *DockerImageRes) Init(init *engine.Init) error {
var err error
obj.init = init // save for later
// Save the full image name and tag.
obj.image = dockerImageNameTag(obj.Name())
ctx, cancel := context.WithTimeout(context.Background(), dockerImageInitCtxTimeout*time.Second)
defer cancel()
// Initialize the docker client.
obj.client, err = client.NewClientWithOpts(client.WithVersion(obj.APIVersion))
if err != nil {
return errwrap.Wrapf(err, "error creating docker client")
}
// Validate the image.
resp, err := obj.client.ImageSearch(ctx, obj.image, types.ImageSearchOptions{Limit: 1})
if err != nil {
return errwrap.Wrapf(err, "error searching for image")
}
if len(resp) == 0 {
return fmt.Errorf("image: %s not found", obj.image)
}
return nil
}
// Close is run by the engine to clean up after the resource is done.
func (obj *DockerImageRes) Close() error {
return obj.client.Close() // close the docker client
}
// Watch is the primary listener for this resource and it outputs events.
func (obj *DockerImageRes) Watch() error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
eventChan, errChan := obj.client.Events(ctx, types.EventsOptions{})
// notify engine that we're running
obj.init.Running()
var send = false // send event?
for {
select {
case event, ok := <-eventChan:
if !ok { // channel shutdown
return nil
}
if obj.init.Debug {
obj.init.Logf("%+v", event)
}
send = true
case err, ok := <-errChan:
if !ok {
return nil
}
return err
case <-obj.init.Done: // closed by the engine to signal shutdown
return nil
}
// do all our event sending all together to avoid duplicate msgs
if send {
send = false
obj.init.Event() // notify engine of an event (this can block)
}
}
}
// CheckApply method for Docker resource.
func (obj *DockerImageRes) CheckApply(apply bool) (checkOK bool, err error) {
ctx, cancel := context.WithTimeout(context.Background(), dockerImageCheckApplyCtxTimeout*time.Second)
defer cancel()
s, err := obj.client.ImageList(ctx, types.ImageListOptions{
Filters: filters.NewArgs(filters.Arg("reference", obj.image)),
})
if err != nil {
return false, errwrap.Wrapf(err, "error listing images")
}
if len(s) > 1 {
return false, fmt.Errorf("more than one image found")
}
if obj.State == "absent" && len(s) == 0 {
return true, nil
}
if obj.State == "exists" && len(s) == 1 {
return true, nil
}
if !apply {
return false, nil
}
if obj.State == "absent" {
// TODO: force? prune children?
if _, err := obj.client.ImageRemove(ctx, obj.image, types.ImageRemoveOptions{}); err != nil {
return false, errwrap.Wrapf(err, "error removing image")
}
return false, nil
}
// pull the image
p, err := obj.client.ImagePull(ctx, obj.image, types.ImagePullOptions{})
if err != nil {
return false, errwrap.Wrapf(err, "error pulling image")
}
// Wait for the image to download, EOF signals that it's done.
if _, err := ioutil.ReadAll(p); err != nil {
return false, errwrap.Wrapf(err, "error reading image pull result")
}
return false, nil
}
// Cmp compares two resources and returns an error if they are not equivalent.
func (obj *DockerImageRes) Cmp(r engine.Res) error {
// we can only compare DockerImageRes to others of the same resource kind
res, ok := r.(*DockerImageRes)
if !ok {
return fmt.Errorf("error casting r to *DockerImageRes")
}
if obj.State != res.State {
return fmt.Errorf("the State differs")
}
if obj.APIVersion != res.APIVersion {
return fmt.Errorf("the APIVersion differs")
}
return nil
}
// DockerImageUID is the UID struct for DockerImageRes.
type DockerImageUID struct {
engine.BaseUID
image 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 *DockerImageRes) UIDs() []engine.ResUID {
x := &DockerImageUID{
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
image: dockerImageNameTag(obj.Name()),
}
return []engine.ResUID{x}
}
// AutoEdges returns the AutoEdge interface.
func (obj *DockerImageRes) AutoEdges() (engine.AutoEdge, error) {
return nil, nil
}
// IFF aka if and only if they are equivalent, return true. If not, false.
func (obj *DockerImageUID) IFF(uid engine.ResUID) bool {
res, ok := uid.(*DockerImageUID)
if !ok {
return false
}
return obj.image == res.image
}
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
// primarily useful for setting the defaults.
func (obj *DockerImageRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
type rawRes DockerImageRes // indirection to avoid infinite recursion
def := obj.Default() // get the default
res, ok := def.(*DockerImageRes) // put in the right format
if !ok {
return fmt.Errorf("could not convert to DockerImageRes")
}
raw := rawRes(*res) // convert; the defaults go here
if err := unmarshal(&raw); err != nil {
return err
}
*obj = DockerImageRes(raw) // restore from indirection with type conversion!
return nil
}
// dockerImageNameTag does a naive check to see if the input includes a tag or
// is a url, and if not, appends the `:latest` tag to ensure disambiguation.
func dockerImageNameTag(image string) string {
if strings.Contains(image, ":") {
return image
}
return image + ":latest"
}

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2021+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
@@ -24,6 +24,7 @@ import (
"fmt"
"os/exec"
"os/user"
"sort"
"strings"
"sync"
"syscall"
@@ -54,7 +55,7 @@ type ExecRes struct {
// only be used when a Shell is *not* specified. The advantage of this
// is that you don't have to worry about escape characters.
Args []string `yaml:"args"`
// Cmd is the dir to run the command in. If empty, then this will use
// Cwd is the dir to run the command in. If empty, then this will use
// the working directory of the calling process. (This process is mgmt,
// not the process being run here.)
Cwd string `yaml:"cwd"`
@@ -65,6 +66,9 @@ type ExecRes struct {
// running command. If the Kill is received before the process exits,
// then this be treated as an error.
Timeout uint64 `yaml:"timeout"`
// Env allows the user to specify environment variables for script
// execution. These are taken using a map of format of VAR_NAME -> value.
Env map[string]string `yaml:"env"`
// Watch is the command to run to detect event changes. Each line of
// output from this command is treated as an event.
@@ -138,6 +142,12 @@ func (obj *ExecRes) Validate() error {
}
}
// check that environment variables' format is valid
for key := range obj.Env {
if err := isNameValid(key); err != nil {
return errwrap.Wrapf(err, "invalid variable name")
}
}
return nil
}
@@ -214,19 +224,21 @@ func (obj *ExecRes) Watch() error {
exitErr, ok := err.(*exec.ExitError) // embeds an os.ProcessState
if !ok {
// command failed in some bad way
return errwrap.Wrapf(err, "unknown error")
return errwrap.Wrapf(err, "watchcmd failed in some bad way")
}
pStateSys := exitErr.Sys() // (*os.ProcessState) Sys
wStatus, ok := pStateSys.(syscall.WaitStatus)
if !ok {
return errwrap.Wrapf(err, "error running cmd")
return errwrap.Wrapf(err, "could not get exit status of watchcmd")
}
exitStatus := wStatus.ExitStatus()
obj.init.Logf("watchcmd exited with: %d", exitStatus)
if exitStatus != 0 {
return errwrap.Wrapf(err, "unexpected exit status of zero")
if exitStatus == 0 {
// i'm not sure if this could happen
return errwrap.Wrapf(err, "unexpected watchcmd exit status of zero")
}
return err // i'm not sure if this could happen
obj.init.Logf("watchcmd exited with: %d", exitStatus)
return errwrap.Wrapf(err, "watchcmd errored")
}
// each time we get a line of output, we loop!
@@ -298,16 +310,17 @@ func (obj *ExecRes) CheckApply(apply bool) (bool, error) {
exitErr, ok := err.(*exec.ExitError) // embeds an os.ProcessState
if !ok {
// command failed in some bad way
return false, err
return false, errwrap.Wrapf(err, "ifcmd failed in some bad way")
}
pStateSys := exitErr.Sys() // (*os.ProcessState) Sys
wStatus, ok := pStateSys.(syscall.WaitStatus)
if !ok {
return false, errwrap.Wrapf(err, "error running cmd")
return false, errwrap.Wrapf(err, "could not get exit status of ifcmd")
}
exitStatus := wStatus.ExitStatus()
if exitStatus == 0 {
return false, fmt.Errorf("unexpected exit status of zero")
// i'm not sure if this could happen
return false, errwrap.Wrapf(err, "unexpected ifcmd exit status of zero")
}
obj.init.Logf("ifcmd exited with: %d", exitStatus)
@@ -368,6 +381,18 @@ func (obj *ExecRes) CheckApply(apply bool) (bool, error) {
defer cancel()
cmd := exec.CommandContext(ctx, cmdName, cmdArgs...)
cmd.Dir = obj.Cwd // run program in pwd if ""
envKeys := []string{}
for key := range obj.Env {
envKeys = append(envKeys, key)
}
sort.Strings(envKeys)
cmdEnv := []string{}
for _, k := range envKeys {
cmdEnv = append(cmdEnv, k+"="+obj.Env[k])
}
cmd.Env = cmdEnv
// ignore signals sent to parent process (we're in our own group)
cmd.SysProcAttr = &syscall.SysProcAttr{
Setpgid: true,
@@ -438,7 +463,7 @@ func (obj *ExecRes) CheckApply(apply bool) (bool, error) {
return false, errwrap.Wrapf(err, "cmd timeout, exit status: %d", exitStatus)
}
return false, fmt.Errorf("unknown cmd error, signal: %s, exit status: %d", sig, exitStatus)
return false, errwrap.Wrapf(err, "unknown cmd error, signal: %s, exit status: %d", sig, exitStatus)
} else if err != nil {
return false, errwrap.Wrapf(err, "general cmd error")
@@ -545,25 +570,38 @@ type ExecUID struct {
// ExecResAutoEdges holds the state of the auto edge generator.
type ExecResAutoEdges struct {
edges []engine.ResUID
edges []engine.ResUID
pointer int
}
// Next returns the next automatic edge.
func (obj *ExecResAutoEdges) Next() []engine.ResUID {
return obj.edges
if len(obj.edges) == 0 {
return nil
}
value := obj.edges[obj.pointer]
obj.pointer++
return []engine.ResUID{value}
}
// Test gets results of the earlier Next() call, & returns if we should continue!
// Test gets results of the earlier Next() call, & returns if we should
// continue!
func (obj *ExecResAutoEdges) Test(input []bool) bool {
return false // never keep going
// TODO: we could return false if we find as many edges as the number of different path's in cmdFiles()
if len(obj.edges) <= obj.pointer {
return false
}
if len(input) != 1 { // in case we get given bad data
panic(fmt.Sprintf("Expecting a single value!"))
}
return true // keep going
}
// AutoEdges returns the AutoEdge interface. In this case the systemd units.
func (obj *ExecRes) AutoEdges() (engine.AutoEdge, error) {
var data []engine.ResUID
var reversed = true
for _, x := range obj.cmdFiles() {
var reversed = true
data = append(data, &PkgFileUID{
BaseUID: engine.BaseUID{
Name: obj.Name(),
@@ -572,14 +610,44 @@ func (obj *ExecRes) AutoEdges() (engine.AutoEdge, error) {
},
path: x, // what matters
})
data = append(data, &FileUID{
BaseUID: engine.BaseUID{
Name: obj.Name(),
Kind: obj.Kind(),
Reversed: &reversed,
},
path: x,
})
}
if obj.User != "" {
data = append(data, &UserUID{
BaseUID: engine.BaseUID{
Name: obj.Name(),
Kind: obj.Kind(),
Reversed: &reversed,
},
name: obj.User,
})
}
if obj.Group != "" {
data = append(data, &GroupUID{
BaseUID: engine.BaseUID{
Name: obj.Name(),
Kind: obj.Kind(),
Reversed: &reversed,
},
name: obj.Group,
})
}
return &ExecResAutoEdges{
edges: data,
edges: data,
pointer: 0,
}, nil
}
// UIDs includes all params to make a unique identification of this object.
// Most resources only return one, although some resources can return multiple.
// 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 *ExecRes) UIDs() []engine.ResUID {
x := &ExecUID{
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
@@ -609,8 +677,8 @@ func (obj *ExecRes) Sends() interface{} {
}
}
// UnmarshalYAML is the custom unmarshal handler for this struct.
// It is primarily useful for setting the defaults.
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
// primarily useful for setting the defaults.
func (obj *ExecRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
type rawRes ExecRes // indirection to avoid infinite recursion
@@ -690,9 +758,9 @@ type cmdOutput struct {
}
// cmdOutputRunner wraps the Cmd in with a StdoutPipe scanner and reads for
// errors. It runs Start and Wait, and errors runtime things in the channel.
// If it can't start up the command, it will fail early. Once it's running, it
// will return the channel which can be used for the duration of the process.
// errors. It runs Start and Wait, and errors runtime things in the channel. If
// it can't start up the command, it will fail early. Once it's running, it will
// return the channel which can be used for the duration of the process.
// Cancelling the context merely unblocks the sending on the output channel, it
// does not Kill the cmd process. For that you must do it yourself elsewhere.
func (obj *ExecRes) cmdOutputRunner(ctx context.Context, cmd *exec.Cmd) (chan *cmdOutput, error) {
@@ -800,3 +868,20 @@ func (obj *wrapWriter) Write(p []byte) (int, error) {
func (obj *wrapWriter) String() string {
return obj.Buffer.String()
}
// isNameValid checks that environment variable name is valid.
func isNameValid(varName string) error {
if varName == "" {
return fmt.Errorf("variable name cannot be an empty string")
}
for i := range varName {
c := varName[i]
if i == 0 && '0' <= c && c <= '9' {
return fmt.Errorf("variable name cannot begin with number")
}
if !(c == '_' || '0' <= c && c <= '9' || 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z') {
return fmt.Errorf("invalid character in variable name")
}
}
return nil
}

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2021+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
@@ -28,6 +28,8 @@ import (
"time"
"github.com/purpleidea/mgmt/engine"
"github.com/purpleidea/mgmt/engine/graph/autoedge"
"github.com/purpleidea/mgmt/pgraph"
)
func fakeExecInit(t *testing.T) (*engine.Init, *ExecSends) {
@@ -257,3 +259,77 @@ func TestExecTimeoutBehaviour(t *testing.T) {
// no error
}
func TestExecAutoEdge1(t *testing.T) {
g, err := pgraph.NewGraph("TestGraph")
if err != nil {
t.Errorf("error creating graph: %v", err)
return
}
resUser, err := engine.NewNamedResource("user", "someuser")
if err != nil {
t.Errorf("error creating user resource: %v", err)
return
}
resGroup, err := engine.NewNamedResource("group", "somegroup")
if err != nil {
t.Errorf("error creating group resource: %v", err)
return
}
resFile, err := engine.NewNamedResource("file", "/somefile")
if err != nil {
t.Errorf("error creating group resource: %v", err)
return
}
resExec, err := engine.NewNamedResource("exec", "somefile")
if err != nil {
t.Errorf("error creating exec resource: %v", err)
return
}
exc := resExec.(*ExecRes)
exc.Cmd = resFile.Name()
exc.User = resUser.Name()
exc.Group = resGroup.Name()
g.AddVertex(resUser, resGroup, resFile, resExec)
if i := g.NumEdges(); i != 0 {
t.Errorf("should have 0 edges instead of: %d", i)
return
}
debug := testing.Verbose() // set via the -test.v flag to `go test`
logf := func(format string, v ...interface{}) {
t.Logf("test: "+format, v...)
}
if err := autoedge.AutoEdge(g, debug, logf); err != nil {
t.Errorf("error running autoedges: %v", err)
return
}
expected, err := pgraph.NewGraph("Expected")
if err != nil {
t.Errorf("error creating graph: %v", err)
return
}
expectEdge := func(from, to pgraph.Vertex) {
edge := &engine.Edge{Name: fmt.Sprintf("%s -> %s (expected)", from, to)}
expected.AddEdge(from, to, edge)
}
expectEdge(resFile, resExec)
expectEdge(resUser, resExec)
expectEdge(resGroup, resExec)
vertexCmp := func(v1, v2 pgraph.Vertex) (bool, error) { return v1 == v2, nil } // pointer compare is sufficient
edgeCmp := func(e1, e2 pgraph.Edge) (bool, error) { return true, nil } // we don't care about edges here
if err := expected.GraphCmp(g, vertexCmp, edgeCmp); err != nil {
t.Errorf("graph doesn't match expected: %s", err)
return
}
}

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2021+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
@@ -29,21 +29,52 @@ import (
"path/filepath"
"strconv"
"strings"
"sync"
"syscall"
"github.com/purpleidea/mgmt/engine"
"github.com/purpleidea/mgmt/engine/traits"
engineUtil "github.com/purpleidea/mgmt/engine/util"
"github.com/purpleidea/mgmt/lang/funcs/vars"
"github.com/purpleidea/mgmt/lang/interfaces"
"github.com/purpleidea/mgmt/lang/types"
"github.com/purpleidea/mgmt/recwatch"
"github.com/purpleidea/mgmt/util"
"github.com/purpleidea/mgmt/util/errwrap"
)
func init() {
engine.RegisterResource("file", func() engine.Res { return &FileRes{} })
engine.RegisterResource(KindFile, func() engine.Res { return &FileRes{} })
// const.res.file.state.exists = "exists"
// const.res.file.state.absent = "absent"
vars.RegisterResourceParams(KindFile, map[string]map[string]func() interfaces.Var{
ParamFileState: {
FileStateExists: func() interfaces.Var {
return &types.StrValue{
V: FileStateExists,
}
},
FileStateAbsent: func() interfaces.Var {
return &types.StrValue{
V: FileStateAbsent,
}
},
// TODO: consider removing this field entirely
"undefined": func() interfaces.Var {
return &types.StrValue{
V: FileStateUndefined, // empty string
}
},
},
})
}
const (
// KindFile is the kind string used to identify this resource.
KindFile = "file"
// ParamFileState is the name of the state field parameter.
ParamFileState = "state"
// FileStateExists is the string that represents that the file should be
// present.
FileStateExists = "exists"
@@ -53,13 +84,20 @@ const (
// FileStateUndefined means the file state has not been specified.
// TODO: consider moving to *string and express this state as a nil.
FileStateUndefined = ""
// FileModeAllowAssign specifies whether we only use ugo=rwx style
// assignment (false) or if we also allow ugo+-rwx style too (true). I
// think that it's possibly illogical to allow imperative mode
// specifiers in a declarative language, so let's leave it off for now.
FileModeAllowAssign = false
)
// FileRes is a file and directory resource. Dirs are defined by names ending
// in a slash.
// FileRes is a file and directory resource. Dirs are defined by names ending in
// a slash.
type FileRes struct {
traits.Base // add the base methods without re-implementation
traits.Edgeable
traits.GraphQueryable // allow others to query this res in the res graph
//traits.Groupable // TODO: implement this
traits.Recvable
traits.Reversible
@@ -81,11 +119,38 @@ type FileRes struct {
State string `lang:"state" yaml:"state"`
// Content specifies the file contents to use. If this is nil, they are
// left undefined. It cannot be combined with Source.
// left undefined. It cannot be combined with the Source or Fragments
// parameters.
Content *string `lang:"content" yaml:"content"`
// Source specifies the source contents for the file resource. It cannot
// be combined with the Content parameter.
// be combined with the Content or Fragments parameters. It must be an
// absolute path, and it can point to a file or a directory. If it
// points to a file, then that will will be copied throuh directly. If
// it points to a directory, then it will copy the directory "rsync
// style" onto the file destination. As a result, if this is a file,
// then the main file res must be a file, and if it is a directory, then
// this must be a directory. To meaningfully copy a full directory, you
// also need to specify the Recurse parameter, which is currently
// required. If you want an existing dir to be turned into a file (or
// vice-versa) instead of erroring, then you'll also need to specify the
// Force parameter. If source is undefined and the file path is a
// directory, then a directory will be created. If left undefined, and
// combined with the Purge option too, then any unmanaged file in this
// dir will be removed.
Source string `lang:"source" yaml:"source"`
// Fragments specifies that the file is built from a list of individual
// files. If one of the files is a directory, then the list of files in
// that directory are the fragments to combine. Multiple of these can be
// used together, although most simple cases will probably only either
// involve a single directory path or a fixed list of individual files.
// All paths are absolute and as a result must start with a slash. The
// directories (if any) must end with a slash as well. This cannot be
// combined with the Content or Source parameters. If a file with param
// is reversed, the reversed file is one that has `Content` set instead.
// Automatic edges will be added from these fragments. This currently
// isn't recursive in that if a fragment is a directory, this only
// searches one level deep at the moment.
Fragments []string `lang:"fragments" yaml:"fragments"`
// Owner specifies the file owner. You can specify either the string
// name, or a string representation of the owner integer uid.
@@ -94,14 +159,17 @@ type FileRes struct {
// name, or a string representation of the group integer gid.
Group string `lang:"group" yaml:"group"`
// Mode is the mode of the file as a string representation of the octal
// form.
// TODO: add symbolic representations
// form or symbolic form.
Mode string `lang:"mode" yaml:"mode"`
Recurse bool `lang:"recurse" yaml:"recurse"`
Force bool `lang:"force" yaml:"force"`
// Purge specifies that when true, any unmanaged file in this file
// directory will be removed. As a result, this file resource must be a
// directory. This isn't particularly meaningful if you don't also set
// Recurse to true. This doesn't work with Content or Fragments.
Purge bool `lang:"purge" yaml:"purge"`
sha256sum string
recWatcher *recwatch.RecWatcher
sha256sum string
}
// getPath returns the actual path to use for this resource. It computes this
@@ -137,10 +205,22 @@ func (obj *FileRes) isDir() bool {
// the case where the mode is not specified. The caller should check obj.Mode is
// not empty.
func (obj *FileRes) mode() (os.FileMode, error) {
m, err := strconv.ParseInt(obj.Mode, 8, 32)
if err != nil {
return os.FileMode(0), errwrap.Wrapf(err, "mode should be an octal number (%s)", obj.Mode)
if n, err := strconv.ParseInt(obj.Mode, 8, 32); err == nil {
return os.FileMode(n), nil
}
// Try parsing symbolically by first getting the files current mode.
stat, err := os.Stat(obj.getPath())
if err != nil {
return os.FileMode(0), errwrap.Wrapf(err, "failed to get the current file mode")
}
modes := strings.Split(obj.Mode, ",")
m, err := engineUtil.ParseSymbolicModes(modes, stat.Mode(), FileModeAllowAssign)
if err != nil {
return os.FileMode(0), errwrap.Wrapf(err, "mode should be an octal number or symbolic mode (%s)", obj.Mode)
}
return os.FileMode(m), nil
}
@@ -173,18 +253,55 @@ func (obj *FileRes) Validate() error {
return fmt.Errorf("the State is invalid")
}
if obj.State == FileStateAbsent && obj.Content != nil {
return fmt.Errorf("can't specify Content for an absent file")
isContent := obj.Content != nil
isSrc := obj.Source != ""
isFrag := len(obj.Fragments) > 0
if (isContent && isSrc) || (isSrc && isFrag) || (isFrag && isContent) {
return fmt.Errorf("can only specify one of Content, Source, and Fragments")
}
if obj.Content != nil && obj.Source != "" {
return fmt.Errorf("can't specify both Content and Source")
if obj.State == FileStateAbsent && (isContent || isSrc || isFrag) {
return fmt.Errorf("can't specify file Content, Source, or Fragments when State is %s", FileStateAbsent)
}
if obj.isDir() && obj.Content != nil { // makes no sense
return fmt.Errorf("can't specify Content when creating a Dir")
// The path and Source must either both be dirs or both not be.
srcIsDir := strings.HasSuffix(obj.Source, "/")
if isSrc && (obj.isDir() != srcIsDir) {
return fmt.Errorf("the path and Source must either both be dirs or both not be")
}
if obj.isDir() && (isContent || isFrag) { // makes no sense
return fmt.Errorf("can't specify Content or Fragments when creating a Dir")
}
// TODO: is this really a requirement that we want to enforce?
if isSrc && obj.isDir() && srcIsDir && !obj.Recurse {
return fmt.Errorf("you'll want to Recurse when you have a Source dir to copy")
}
// TODO: do we want to enforce this sort of thing?
if obj.Purge && !obj.Recurse {
return fmt.Errorf("you'll want to Recurse when you have a Purge to do")
}
if isSrc && !obj.isDir() && !srcIsDir && obj.Recurse {
return fmt.Errorf("you can't recurse when copying a single file")
}
for _, frag := range obj.Fragments {
// absolute paths begin with a slash
if !strings.HasPrefix(frag, "/") {
return fmt.Errorf("the frag (`%s`) isn't an absolute path", frag)
}
}
if obj.Purge && (isContent || isFrag) {
return fmt.Errorf("can't combine Purge with Content or Fragments")
}
// XXX: should this work with obj.Purge && obj.Source != "" or not?
//if obj.Purge && obj.Source != "" {
// return fmt.Errorf("can't Purge when Source is specified")
//}
// TODO: should we silently ignore these errors or include them?
//if obj.State == FileStateAbsent && obj.Owner != "" {
// return fmt.Errorf("can't specify Owner for an absent file")
@@ -220,11 +337,6 @@ func (obj *FileRes) Validate() error {
}
}
// XXX: should this specify that we create an empty directory instead?
//if obj.Source == "" && obj.isDir() {
// return fmt.Errorf("can't specify an empty source when creating a Dir.")
//}
return nil
}
@@ -242,19 +354,112 @@ func (obj *FileRes) Close() error {
return nil
}
// Watch is the primary listener for this resource and it outputs events.
// This one is a file watcher for files and directories.
// Modify with caution, it is probably important to write some test cases first!
// If the Watch returns an error, it means that something has gone wrong, and it
// must be restarted. On a clean exit it returns nil.
// FIXME: Also watch the source directory when using obj.Source !!!
// Watch is the primary listener for this resource and it outputs events. This
// one is a file watcher for files and directories. Modify with caution, it is
// probably important to write some test cases first! If the Watch returns an
// error, it means that something has gone wrong, and it must be restarted. On a
// clean exit it returns nil.
func (obj *FileRes) Watch() error {
var err error
obj.recWatcher, err = recwatch.NewRecWatcher(obj.getPath(), obj.Recurse)
// TODO: chan *recwatch.Event instead?
inputEvents := make(chan recwatch.Event)
defer close(inputEvents)
wg := &sync.WaitGroup{}
defer wg.Wait()
exit := make(chan struct{})
// TODO: should this be after (later in the file) than the `defer recWatcher.Close()` ?
// TODO: should this be after (later in the file) the `defer recWatcher.Close()` ?
defer close(exit)
recWatcher, err := recwatch.NewRecWatcher(obj.getPath(), obj.Recurse)
if err != nil {
return err
}
defer obj.recWatcher.Close()
defer recWatcher.Close()
// watch the various inputs to this file resource too!
if obj.Source != "" {
// This block is virtually identical to the below one.
recurse := strings.HasSuffix(obj.Source, "/") // isDir
rw, err := recwatch.NewRecWatcher(obj.Source, recurse)
if err != nil {
return err
}
defer rw.Close()
wg.Add(1)
go func() {
defer wg.Done()
for {
// TODO: *recwatch.Event instead?
var event recwatch.Event
var ok bool
var shutdown bool
select {
case event, ok = <-rw.Events(): // recv
case <-exit: // unblock
return
}
if !ok {
err := fmt.Errorf("channel shutdown")
event = recwatch.Event{Error: err}
shutdown = true
}
select {
case inputEvents <- event: // send
if shutdown { // optimization to free early
return
}
case <-exit: // unblock
return
}
}
}()
}
for _, frag := range obj.Fragments {
// This block is virtually identical to the above one.
recurse := false // TODO: is it okay for depth==1 dirs?
//recurse := strings.HasSuffix(frag, "/") // isDir
rw, err := recwatch.NewRecWatcher(frag, recurse)
if err != nil {
return err
}
defer rw.Close()
wg.Add(1)
go func() {
defer wg.Done()
for {
// TODO: *recwatch.Event instead?
var event recwatch.Event
var ok bool
var shutdown bool
select {
case event, ok = <-rw.Events(): // recv
case <-exit: // unblock
return
}
if !ok {
err := fmt.Errorf("channel shutdown")
event = recwatch.Event{Error: err}
shutdown = true
}
select {
case inputEvents <- event: // send
if shutdown { // optimization to free early
return
}
case <-exit: // unblock
return
}
}
}()
}
obj.init.Running() // when started, notify engine that we're running
@@ -265,9 +470,12 @@ func (obj *FileRes) Watch() error {
}
select {
case event, ok := <-obj.recWatcher.Events():
case event, ok := <-recWatcher.Events():
if !ok { // channel shutdown
return nil
// TODO: Should this be an error? Previously it
// was a `return nil`, and i'm not sure why...
//return nil
return fmt.Errorf("unexpected close")
}
if err := event.Error; err != nil {
return errwrap.Wrapf(err, "unknown %s watcher error", obj)
@@ -277,6 +485,18 @@ func (obj *FileRes) Watch() error {
}
send = true
case event, ok := <-inputEvents:
if !ok {
return fmt.Errorf("unexpected close")
}
if err := event.Error; err != nil {
return errwrap.Wrapf(err, "unknown %s input watcher error", obj)
}
if obj.init.Debug { // don't access event.Body if event.Error isn't nil
obj.init.Logf("input event(%s): %v", event.Body.Name, event.Body.Op)
}
send = true
case <-obj.init.Done: // closed by the engine to signal shutdown
return nil
}
@@ -485,11 +705,14 @@ func (obj *FileRes) dirCheckApply(apply bool) (bool, error) {
// syncCheckApply is the CheckApply operation for a source and destination dir.
// It is recursive and can create directories directly, and files via the usual
// fileCheckApply method. It returns checkOK and error as is normally expected.
func (obj *FileRes) syncCheckApply(apply bool, src, dst string) (bool, error) {
// If excludes is specified, none of those files there will be deleted by this,
// with the exception that a sync *can* convert a file to a dir, or vice-versa.
func (obj *FileRes) syncCheckApply(apply bool, src, dst string, excludes []string) (bool, error) {
if obj.init.Debug {
obj.init.Logf("syncCheckApply: %s -> %s", src, dst)
}
if src == "" || dst == "" {
// an src of "" is now supported, if dst is a dir
if dst == "" {
return false, fmt.Errorf("the src and dst must not be empty")
}
@@ -499,11 +722,14 @@ func (obj *FileRes) syncCheckApply(apply bool, src, dst string) (bool, error) {
srcIsDir := strings.HasSuffix(src, "/")
dstIsDir := strings.HasSuffix(dst, "/")
if srcIsDir != dstIsDir {
if srcIsDir != dstIsDir && src != "" {
return false, fmt.Errorf("the src and dst must be both either files or directories")
}
if src == "" && !dstIsDir {
return false, fmt.Errorf("dst must be a dir if we have an empty src")
}
if !srcIsDir && !dstIsDir {
if !srcIsDir && !dstIsDir && src != "" {
if obj.init.Debug {
obj.init.Logf("syncCheckApply: %s -> %s", src, dst)
}
@@ -524,18 +750,23 @@ func (obj *FileRes) syncCheckApply(apply bool, src, dst string) (bool, error) {
}
// else: if srcIsDir && dstIsDir
srcFiles, err := ReadDir(src) // if src does not exist...
if err != nil && !os.IsNotExist(err) { // an empty map comes out below!
return false, err
smartSrc := make(map[string]FileInfo)
if src != "" {
srcFiles, err := ReadDir(src) // if src does not exist...
if err != nil && !os.IsNotExist(err) { // an empty map comes out below!
return false, err
}
smartSrc = mapPaths(srcFiles)
obj.init.Logf("syncCheckApply: srcFiles: %v", srcFiles)
}
dstFiles, err := ReadDir(dst)
if err != nil && !os.IsNotExist(err) {
return false, err
}
//obj.init.Logf("syncCheckApply: srcFiles: %v", srcFiles)
//obj.init.Logf("syncCheckApply: dstFiles: %v", dstFiles)
smartSrc := mapPaths(srcFiles)
smartDst := mapPaths(dstFiles)
obj.init.Logf("syncCheckApply: dstFiles: %v", dstFiles)
for relPath, fileInfo := range smartSrc {
absSrc := fileInfo.AbsPath // absolute path
@@ -581,7 +812,7 @@ func (obj *FileRes) syncCheckApply(apply bool, src, dst string) (bool, error) {
obj.init.Logf("syncCheckApply: recurse: %s -> %s", absSrc, absDst)
}
if obj.Recurse {
if c, err := obj.syncCheckApply(apply, absSrc, absDst); err != nil { // recurse
if c, err := obj.syncCheckApply(apply, absSrc, absDst, excludes); err != nil { // recurse
return false, errwrap.Wrapf(err, "syncCheckApply: recurse failed")
} else if !c { // don't let subsequent passes make this true
checkOK = false
@@ -596,6 +827,19 @@ func (obj *FileRes) syncCheckApply(apply bool, src, dst string) (bool, error) {
if !apply && len(smartDst) > 0 { // we know there are files to remove!
return false, nil // so just exit now
}
// isExcluded specifies if the path is part of an excluded path. For
// example, if we exclude /tmp/foo/bar from deletion, then we don't want
// to delete /tmp/foo/bar *or* /tmp/foo/ *or* /tmp/ b/c they're parents.
isExcluded := func(p string) bool {
for _, x := range excludes {
if util.HasPathPrefix(x, p) {
return true
}
}
return false
}
// any files that now remain in smartDst need to be removed...
for relPath, fileInfo := range smartDst {
absSrc := src + relPath // absolute dest (should not exist!)
@@ -611,6 +855,9 @@ func (obj *FileRes) syncCheckApply(apply bool, src, dst string) (bool, error) {
// think the symmetry is more elegant and correct here for now
// Avoiding this is also useful if we had a recurse limit arg!
if true { // switch
if isExcluded(absDst) { // skip removing excluded files
continue
}
obj.init.Logf("syncCheckApply: removing: %s", absCleanDst)
if apply {
if err := os.RemoveAll(absCleanDst); err != nil { // dangerous ;)
@@ -622,11 +869,14 @@ func (obj *FileRes) syncCheckApply(apply bool, src, dst string) (bool, error) {
}
_ = absSrc
//obj.init.Logf("syncCheckApply: recurse rm: %s -> %s", absSrc, absDst)
//if c, err := obj.syncCheckApply(apply, absSrc, absDst); err != nil {
//if c, err := obj.syncCheckApply(apply, absSrc, absDst, excludes); err != nil {
// return false, errwrap.Wrapf(err, "syncCheckApply: recurse rm failed")
//} else if !c { // don't let subsequent passes make this true
// checkOK = false
//}
//if isExcluded(absDst) { // skip removing excluded files
// continue
//}
//obj.init.Logf("syncCheckApply: removing: %s", absCleanDst)
//if apply { // safety
// if err := os.Remove(absCleanDst); err != nil {
@@ -690,7 +940,7 @@ func (obj *FileRes) stateCheckApply(apply bool) (bool, error) {
// Optimization: we shouldn't even look at obj.Content here, but we can
// skip this empty file creation here since we know we're going to be
// making it there anyways. This way we save the extra fopen noise.
if obj.Content != nil {
if obj.Content != nil || len(obj.Fragments) > 0 {
return false, nil // pretend we actually made it
}
@@ -718,6 +968,7 @@ func (obj *FileRes) contentCheckApply(apply bool) (bool, error) {
return true, nil
}
// Actually write the file. This is similar to fragmentsCheckApply.
bufferSrc := bytes.NewReader([]byte(*obj.Content))
sha256sum, checkOK, err := obj.fileCheckApply(apply, bufferSrc, obj.getPath(), obj.sha256sum)
if sha256sum != "" { // empty values mean errored or didn't hash
@@ -736,11 +987,48 @@ func (obj *FileRes) sourceCheckApply(apply bool) (bool, error) {
obj.init.Logf("sourceCheckApply(%t)", apply)
// source is not defined, leave it alone...
if obj.Source == "" {
if obj.Source == "" && !obj.Purge {
return true, nil
}
checkOK, err := obj.syncCheckApply(apply, obj.Source, obj.getPath())
excludes := []string{}
// If we're running a purge, do it here.
if obj.Purge {
graph, err := obj.init.FilteredGraph()
if err != nil {
return false, errwrap.Wrapf(err, "can't read filtered graph")
}
for _, vertex := range graph.Vertices() {
res, ok := vertex.(engine.Res)
if !ok {
// programming error
return false, fmt.Errorf("not a Res")
}
if res.Kind() != KindFile {
continue // only interested in files
}
if res.Name() == obj.Name() {
continue // skip me!
}
fileRes, ok := res.(*FileRes)
if !ok {
// programming error
return false, fmt.Errorf("not a FileRes")
}
p := fileRes.getPath() // if others use it, make public!
if !util.HasPathPrefix(p, obj.getPath()) {
continue
}
excludes = append(excludes, p)
}
}
if obj.init.Debug {
obj.init.Logf("syncCheckApply: excludes: %+v", excludes)
}
// XXX: should this work with obj.Purge && obj.Source != "" or not?
checkOK, err := obj.syncCheckApply(apply, obj.Source, obj.getPath(), excludes)
if err != nil {
obj.init.Logf("syncCheckApply: error: %v", err)
return false, err
@@ -749,6 +1037,66 @@ func (obj *FileRes) sourceCheckApply(apply bool) (bool, error) {
return checkOK, nil
}
// fragmentsCheckApply performs a CheckApply for the file fragments.
func (obj *FileRes) fragmentsCheckApply(apply bool) (bool, error) {
obj.init.Logf("fragmentsCheckApply(%t)", apply)
// fragments is not defined, leave it alone...
if len(obj.Fragments) == 0 {
return true, nil
}
content := ""
// TODO: In the future we could have a flag that merges and then sorts
// all the individual files in each directory before they are combined.
for _, frag := range obj.Fragments {
// It's a single file. Add it to what we're building...
if isDir := strings.HasSuffix(frag, "/"); !isDir {
out, err := ioutil.ReadFile(frag)
if err != nil {
return false, errwrap.Wrapf(err, "could not read file fragment")
}
content += string(out)
continue
}
// We're a dir, peer inside...
files, err := ioutil.ReadDir(frag)
if err != nil {
return false, errwrap.Wrapf(err, "could not read fragment directory")
}
// TODO: Add a sort and filter option so that we can choose the
// way we iterate through this directory to build out the file.
for _, file := range files {
if file.IsDir() { // skip recursive solutions for now...
continue
}
f := path.Join(frag, file.Name())
out, err := ioutil.ReadFile(f)
if err != nil {
return false, errwrap.Wrapf(err, "could not read directory file fragment")
}
content += string(out)
}
}
// Actually write the file. This is similar to contentCheckApply.
bufferSrc := bytes.NewReader([]byte(content))
// NOTE: We pass in an invalidated sha256sum cache since we don't cache
// all the individual files, and it could all change without us knowing.
// TODO: Is the sha256sum caching even having an effect at all here ???
sha256sum, checkOK, err := obj.fileCheckApply(apply, bufferSrc, obj.getPath(), "")
if sha256sum != "" { // empty values mean errored or didn't hash
// this can be valid even when the whole function errors
obj.sha256sum = sha256sum // cache value
}
if err != nil {
return false, err
}
// if no err, but !ok, then...
return checkOK, nil // success
}
// chownCheckApply performs a CheckApply for the file ownership.
func (obj *FileRes) chownCheckApply(apply bool) (bool, error) {
obj.init.Logf("chownCheckApply(%t)", apply)
@@ -858,7 +1206,8 @@ func (obj *FileRes) CheckApply(apply bool) (bool, error) {
checkOK := true
// run stateCheckApply before contentCheckApply and sourceCheckApply
// Run stateCheckApply before contentCheckApply, sourceCheckApply, and
// fragmentsCheckApply.
if c, err := obj.stateCheckApply(apply); err != nil {
return false, err
} else if !c {
@@ -874,6 +1223,11 @@ func (obj *FileRes) CheckApply(apply bool) (bool, error) {
} else if !c {
checkOK = false
}
if c, err := obj.fragmentsCheckApply(apply); err != nil {
return false, err
} else if !c {
checkOK = false
}
if c, err := obj.chownCheckApply(apply); err != nil {
return false, err
@@ -918,6 +1272,14 @@ func (obj *FileRes) Cmp(r engine.Res) error {
if obj.Source != res.Source {
return fmt.Errorf("the Source differs")
}
if len(obj.Fragments) != len(res.Fragments) {
return fmt.Errorf("the number of Fragments differs")
}
for i, x := range obj.Fragments {
if frag := res.Fragments[i]; x != frag {
return fmt.Errorf("the fragment at index %d differs", i)
}
}
if obj.Owner != res.Owner {
return fmt.Errorf("the Owner differs")
@@ -937,6 +1299,9 @@ func (obj *FileRes) Cmp(r engine.Res) error {
if obj.Force != res.Force {
return fmt.Errorf("the Force option differs")
}
if obj.Purge != res.Purge {
return fmt.Errorf("the Purge option differs")
}
return nil
}
@@ -958,6 +1323,11 @@ func (obj *FileUID) IFF(uid engine.ResUID) bool {
// FileResAutoEdges holds the state of the auto edge generator.
type FileResAutoEdges struct {
// We do all of these first...
frags []engine.ResUID
fdone bool
// Then this is the second part...
data []engine.ResUID
pointer int
found bool
@@ -965,6 +1335,12 @@ type FileResAutoEdges struct {
// Next returns the next automatic edge.
func (obj *FileResAutoEdges) Next() []engine.ResUID {
// We do all of these first...
if !obj.fdone && len(obj.frags) > 0 {
return obj.frags // return them all at the same time
}
// Then this is the second part...
if obj.found {
panic("Shouldn't be called anymore!")
}
@@ -976,8 +1352,16 @@ func (obj *FileResAutoEdges) Next() []engine.ResUID {
return []engine.ResUID{value} // we return one, even though api supports N
}
// Test gets results of the earlier Next() call, & returns if we should continue!
// Test gets results of the earlier Next() call, & returns if we should
// continue!
func (obj *FileResAutoEdges) Test(input []bool) bool {
// We do all of these first...
if !obj.fdone && len(obj.frags) > 0 {
obj.fdone = true // mark as done
return true // keep going
}
// Then this is the second part...
// if there aren't any more remaining
if len(obj.data) <= obj.pointer {
return false
@@ -1013,15 +1397,32 @@ func (obj *FileRes) AutoEdges() (engine.AutoEdge, error) {
path: x, // what matters
}) // build list
}
// Ensure any file or dir fragments come first.
frags := []engine.ResUID{}
for _, frag := range obj.Fragments {
var reversed = true // cheat by passing a pointer
frags = append(frags, &FileUID{
BaseUID: engine.BaseUID{
Name: obj.Name(),
Kind: obj.Kind(),
Reversed: &reversed,
},
path: frag, // what matters
}) // build list
}
return &FileResAutoEdges{
frags: frags,
data: data,
pointer: 0,
found: false,
}, nil
}
// UIDs includes all params to make a unique identification of this object.
// Most resources only return one, although some resources can return multiple.
// 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 *FileRes) UIDs() []engine.ResUID {
x := &FileUID{
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
@@ -1047,8 +1448,8 @@ func (obj *FileRes) CollectPattern(pattern string) {
obj.Dirname = pattern // XXX: simplistic for now
}
// UnmarshalYAML is the custom unmarshal handler for this struct.
// It is primarily useful for setting the defaults.
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
// primarily useful for setting the defaults.
func (obj *FileRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
type rawRes FileRes // indirection to avoid infinite recursion
@@ -1075,18 +1476,24 @@ func (obj *FileRes) Copy() engine.CopyableRes {
s := *obj.Content
content = &s
}
fragments := []string{}
for _, frag := range obj.Fragments {
fragments = append(fragments, frag)
}
return &FileRes{
Path: obj.Path,
Dirname: obj.Dirname,
Basename: obj.Basename,
State: obj.State, // TODO: if this becomes a pointer, copy the string!
Content: content,
Source: obj.Source,
Owner: obj.Owner,
Group: obj.Group,
Mode: obj.Mode,
Recurse: obj.Recurse,
Force: obj.Force,
Path: obj.Path,
Dirname: obj.Dirname,
Basename: obj.Basename,
State: obj.State, // TODO: if this becomes a pointer, copy the string!
Content: content,
Source: obj.Source,
Fragments: fragments,
Owner: obj.Owner,
Group: obj.Group,
Mode: obj.Mode,
Recurse: obj.Recurse,
Force: obj.Force,
Purge: obj.Purge,
}
}
@@ -1133,8 +1540,9 @@ func (obj *FileRes) Reversed() (engine.ReversibleRes, error) {
// If we've specified content, we might need to restore the original, OR
// if we're removing the file with a `state => "absent"`, save it too...
// We do this whether we specified content with Content or w/ Fragments.
// The `res.State != FileStateAbsent` check is an optional optimization.
if (obj.Content != nil || obj.State == FileStateAbsent) && res.State != FileStateAbsent {
if ((obj.Content != nil || len(obj.Fragments) > 0) || obj.State == FileStateAbsent) && res.State != FileStateAbsent {
content, err := ioutil.ReadFile(obj.getPath())
if err != nil && !os.IsNotExist(err) {
return nil, errwrap.Wrapf(err, "could not read file for reversal storage")
@@ -1154,6 +1562,13 @@ func (obj *FileRes) Reversed() (engine.ReversibleRes, error) {
return nil, fmt.Errorf("can't reverse with Source yet")
}
// We suck in the previous file contents above when Fragments is used...
// This is basically the very same code path as when we reverse Content.
// TODO: Do we want to do it this way or is there a better reverse path?
if len(obj.Fragments) > 0 {
res.Fragments = []string{}
}
// There is a race if the operating system is adding/changing/removing
// the file between the ioutil.Readfile at the top and here. If there is
// a discrepancy between the two, then you might get an unexpected
@@ -1191,6 +1606,18 @@ func (obj *FileRes) Reversed() (engine.ReversibleRes, error) {
return res, nil
}
// GraphQueryAllowed returns nil if you're allowed to query the graph. This
// function accepts information about the requesting resource so we can
// determine the access with some form of fine-grained control.
func (obj *FileRes) GraphQueryAllowed(opts ...engine.GraphQueryableOption) error {
options := &engine.GraphQueryableOptions{} // default options
options.Apply(opts...) // apply the options
if options.Kind != KindFile {
return fmt.Errorf("only other files can access my information")
}
return nil
}
// smartPath adds a trailing slash to the path if it is a directory.
func smartPath(fileInfo os.FileInfo) string {
smartPath := fileInfo.Name() // absolute path

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2021+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2021+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
@@ -271,8 +271,8 @@ func (obj *GroupUID) IFF(uid engine.ResUID) bool {
return true
}
// UIDs includes all params to make a unique identification of this object.
// Most resources only return one, although some resources can return multiple.
// 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 *GroupRes) UIDs() []engine.ResUID {
x := &GroupUID{
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
@@ -282,8 +282,8 @@ func (obj *GroupRes) UIDs() []engine.ResUID {
return []engine.ResUID{x}
}
// UnmarshalYAML is the custom unmarshal handler for this struct.
// It is primarily useful for setting the defaults.
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
// primarily useful for setting the defaults.
func (obj *GroupRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
type rawRes GroupRes // indirection to avoid infinite recursion

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2021+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
@@ -46,12 +46,12 @@ var ErrResourceInsufficientParameters = errors.New("insufficient parameters for
// HostnameRes is a resource that allows setting and watching the hostname.
//
// StaticHostname is the one configured in /etc/hostname or a similar file.
// It is chosen by the local user. It is not always in sync with the current
// host name as returned by the gethostname() system call.
// StaticHostname is the one configured in /etc/hostname or a similar file. It
// is chosen by the local user. It is not always in sync with the current host
// name as returned by the gethostname() system call.
//
// TransientHostname is the one configured via the kernel's sethostbyname().
// It can be different from the static hostname in case DHCP or mDNS have been
// TransientHostname is the one configured via the kernel's sethostbyname(). It
// can be different from the static hostname in case DHCP or mDNS have been
// configured to change the name based on network information.
//
// PrettyHostname is a free-form UTF8 host name for presentation to the user.
@@ -248,8 +248,8 @@ type HostnameUID struct {
transientHostname string
}
// UIDs includes all params to make a unique identification of this object.
// Most resources only return one, although some resources can return multiple.
// 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 *HostnameRes) UIDs() []engine.ResUID {
x := &HostnameUID{
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
@@ -261,8 +261,8 @@ func (obj *HostnameRes) UIDs() []engine.ResUID {
return []engine.ResUID{x}
}
// UnmarshalYAML is the custom unmarshal handler for this struct.
// It is primarily useful for setting the defaults.
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
// primarily useful for setting the defaults.
func (obj *HostnameRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
type rawRes HostnameRes // indirection to avoid infinite recursion

808
engine/resources/http.go Normal file
View File

@@ -0,0 +1,808 @@
// Mgmt
// Copyright (C) 2013-2021+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> 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 <http://www.gnu.org/licenses/>.
package resources
import (
"bytes"
"context"
"fmt"
"io"
"net"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/purpleidea/mgmt/engine"
"github.com/purpleidea/mgmt/engine/traits"
"github.com/purpleidea/mgmt/util/errwrap"
securefilepath "github.com/cyphar/filepath-securejoin"
)
func init() {
engine.RegisterResource("http:server", func() engine.Res { return &HTTPServerRes{} })
engine.RegisterResource("http:file", func() engine.Res { return &HTTPFileRes{} })
}
const (
// HTTPUseSecureJoin specifies that we should add in a "secure join" lib
// so that we avoid the ../../etc/passwd and symlink problems.
HTTPUseSecureJoin = true
)
// HTTPServerRes is an http server resource. It serves files, but does not
// actually apply any state. The name is used as the address to listen on,
// unless the Address field is specified, and in that case it is used instead.
// This resource can offer up files for serving that are specified either inline
// in this resource by specifying an http root, or as http:file resources which
// will get autogrouped into this resource at runtime. The two methods can be
// combined as well.
//
// This server also supports autogrouping some more magical resources into it.
// For example, the http:flag and http:ui resources add in magic endpoints.
//
// This server is not meant as a featureful replacement for the venerable and
// modern httpd servers out there, but rather as a simple, dynamic, integrated
// alternative for bootstrapping new machines and clusters in an elegant way.
//
// TODO: add support for TLS
// XXX: Add an http:flag resource that lets an http client set a flag somewhere!
// XXX: Add a http:ui resource that functions can read data from!
// XXX: The http:ui resource can also take in values from those functions!
type HTTPServerRes struct {
traits.Base // add the base methods without re-implementation
traits.Edgeable // XXX: add autoedge support
traits.Groupable // can have HTTPFileRes grouped into it
init *engine.Init
// Address is the listen address to use for the http server. It is
// common to use `:80` (the standard) to listen on TCP port 80 on all
// addresses.
Address string `lang:"address" yaml:"address"`
// Timeout is the maximum duration in seconds to use for unspecified
// timeouts. In other words, when this value is specified, it is used as
// the value for the other *Timeout values when they aren't used. Put
// another way, this makes it easy to set all the different timeouts
// with a single parameter.
Timeout *uint64 `lang:"timeout" yaml:"timeout"`
// ReadTimeout is the maximum duration in seconds for reading during the
// http request. If it is zero, then there is no timeout. If this is
// unspecified, then the value of Timeout is used instead if it is set.
// For more information, see the golang net/http Server documentation.
ReadTimeout *uint64 `lang:"read_timeout" yaml:"read_timeout"`
// WriteTimeout is the maximum duration in seconds for writing during
// the http request. If it is zero, then there is no timeout. If this is
// unspecified, then the value of Timeout is used instead if it is set.
// For more information, see the golang net/http Server documentation.
WriteTimeout *uint64 `lang:"write_timeout" yaml:"write_timeout"`
// ShutdownTimeout is the maximum duration in seconds to wait for the
// server to shutdown gracefully before calling Close. By default it is
// nice to let client connections terminate gracefully, however it might
// take longer than we are willing to wait, particularly if one is long
// polling or running a very long download. As a result, you can set a
// timeout here. The default is zero which means it will wait
// indefinitely. The shutdown process can also be cancelled by the
// interrupt handler which this resource supports. If this is
// unspecified, then the value of Timeout is used instead if it is set.
ShutdownTimeout *uint64 `lang:"shutdown_timeout" yaml:"shutdown_timeout"`
// Root is the root directory that we should serve files from. If it is
// not specified, then it is not used. Any http file resources will have
// precedence over anything in here, in case the same path exists twice.
// TODO: should we have a flag to determine the precedence rules here?
Root string `lang:"root" yaml:"root"`
// TODO: should we allow adding a list of one-of files directly here?
interruptChan chan struct{}
conn net.Listener
serveMux *http.ServeMux // can't share the global one between resources!
server *http.Server
}
// Default returns some sensible defaults for this resource.
func (obj *HTTPServerRes) Default() engine.Res {
return &HTTPServerRes{}
}
// getAddress returns the actual address to use. When Address is not specified,
// we use the Name.
func (obj *HTTPServerRes) getAddress() string {
if obj.Address != "" {
return obj.Address
}
return obj.Name()
}
// getReadTimeout determines the value for ReadTimeout, because if unspecified,
// this will default to the value of Timeout.
func (obj *HTTPServerRes) getReadTimeout() *uint64 {
if obj.ReadTimeout != nil {
return obj.ReadTimeout
}
return obj.Timeout // might be nil
}
// getWriteTimeout determines the value for WriteTimeout, because if
// unspecified, this will default to the value of Timeout.
func (obj *HTTPServerRes) getWriteTimeout() *uint64 {
if obj.WriteTimeout != nil {
return obj.WriteTimeout
}
return obj.Timeout // might be nil
}
// getShutdownTimeout determines the value for ShutdownTimeout, because if
// unspecified, this will default to the value of Timeout.
func (obj *HTTPServerRes) getShutdownTimeout() *uint64 {
if obj.ShutdownTimeout != nil {
return obj.ShutdownTimeout
}
return obj.Timeout // might be nil
}
// Validate checks if the resource data structure was populated correctly.
func (obj *HTTPServerRes) Validate() error {
if obj.getAddress() == "" {
return fmt.Errorf("empty address")
}
host, _, err := net.SplitHostPort(obj.getAddress())
if err != nil {
return errwrap.Wrapf(err, "the Address is in an invalid format: %s", obj.getAddress())
}
if host != "" {
// TODO: should we allow fqdn's here?
ip := net.ParseIP(host)
if ip == nil {
return fmt.Errorf("the Address is not a valid IP: %s", host)
}
}
if obj.Root != "" && !strings.HasPrefix(obj.Root, "/") {
return fmt.Errorf("the Root must be absolute")
}
if obj.Root != "" && !strings.HasSuffix(obj.Root, "/") {
return fmt.Errorf("the Root must be a dir")
}
// XXX: validate that the autogrouped resources don't have paths that
// conflict with each other. We can only have a single unique entry for
// what handles a /whatever URL.
return nil
}
// Init runs some startup code for this resource.
func (obj *HTTPServerRes) Init(init *engine.Init) error {
obj.init = init // save for later
// No need to error in Validate if Timeout is ignored, but log it.
// These are all specified, so Timeout effectively does nothing.
a := obj.ReadTimeout != nil
b := obj.WriteTimeout != nil
c := obj.ShutdownTimeout != nil
if obj.Timeout != nil && (a && b && c) {
obj.init.Logf("the Timeout param is being ignored")
}
// NOTE: If we don't Init anything that's autogrouped, then it won't
// even get an Init call on it.
// TODO: should we do this in the engine? Do we want to decide it here?
for _, res := range obj.GetGroup() { // grouped elements
if err := res.Init(init); err != nil {
return errwrap.Wrapf(err, "autogrouped Init failed")
}
}
obj.interruptChan = make(chan struct{})
return nil
}
// Close is run by the engine to clean up after the resource is done.
func (obj *HTTPServerRes) Close() error {
return nil
}
// Watch is the primary listener for this resource and it outputs events.
func (obj *HTTPServerRes) Watch() error {
// TODO: I think we could replace all this with:
//obj.conn, err := net.Listen("tcp", obj.getAddress())
// ...but what is the advantage?
addr, err := net.ResolveTCPAddr("tcp", obj.getAddress())
if err != nil {
return errwrap.Wrapf(err, "could not resolve address")
}
obj.conn, err = net.ListenTCP("tcp", addr)
if err != nil {
return errwrap.Wrapf(err, "could not start listener")
}
defer obj.conn.Close()
obj.serveMux = http.NewServeMux() // do it here in case Watch restarts!
obj.serveMux.HandleFunc("/", obj.handler())
readTimeout := uint64(0)
if i := obj.getReadTimeout(); i != nil {
readTimeout = *i
}
writeTimeout := uint64(0)
if i := obj.getWriteTimeout(); i != nil {
writeTimeout = *i
}
obj.server = &http.Server{
Addr: obj.getAddress(),
Handler: obj.serveMux,
ReadTimeout: time.Duration(readTimeout) * time.Second,
WriteTimeout: time.Duration(writeTimeout) * time.Second,
//MaxHeaderBytes: 1 << 20, XXX: should we add a param for this?
}
obj.init.Running() // when started, notify engine that we're running
var closeError error
closeSignal := make(chan struct{})
wg := &sync.WaitGroup{}
defer wg.Wait()
shutdownChan := make(chan struct{}) // server shutdown finished signal
wg.Add(1)
go func() {
defer wg.Done()
select {
case <-obj.interruptChan:
// TODO: should we bubble up the error from Close?
// TODO: do we need a mutex around this Close?
obj.server.Close() // kill it quickly!
case <-shutdownChan:
// let this exit
}
}()
wg.Add(1)
go func() {
defer wg.Done()
defer close(closeSignal)
err := obj.server.Serve(obj.conn) // blocks until Shutdown() is called!
if err == nil || err == http.ErrServerClosed {
return
}
// if this returned on its own, then closeSignal can be used...
closeError = errwrap.Wrapf(err, "the server errored")
}()
// When Shutdown is called, Serve, ListenAndServe, and ListenAndServeTLS
// immediately return ErrServerClosed. Make sure the program doesn't
// exit and waits instead for Shutdown to return.
defer func() {
defer close(shutdownChan) // signal that shutdown is finished
ctx := context.Background()
if i := obj.getShutdownTimeout(); i != nil && *i > 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, time.Duration(*i)*time.Second)
defer cancel()
}
err := obj.server.Shutdown(ctx) // shutdown gracefully
if err == context.DeadlineExceeded {
// TODO: should we bubble up the error from Close?
// TODO: do we need a mutex around this Close?
obj.server.Close() // kill it now
}
}()
startupChan := make(chan struct{})
close(startupChan) // send one initial signal
var send = false // send event?
for {
if obj.init.Debug {
obj.init.Logf("Looping...")
}
select {
case <-startupChan:
startupChan = nil
send = true
case <-closeSignal: // something shut us down early
return closeError
case <-obj.init.Done: // closed by the engine to signal shutdown
return nil
}
// do all our event sending all together to avoid duplicate msgs
if send {
send = false
obj.init.Event() // notify engine of an event (this can block)
}
}
}
// CheckApply never has anything to do for this resource, so it always succeeds.
// It does however check that certain runtime requirements (such as the Root dir
// existing if one was specified) are fulfilled.
func (obj *HTTPServerRes) CheckApply(apply bool) (bool, error) {
if obj.init.Debug {
obj.init.Logf("CheckApply")
}
// XXX: We don't want the initial CheckApply to return true until the
// Watch has started up, so we must block here until that's the case...
// Cheap runtime validation!
if obj.Root != "" {
fileInfo, err := os.Stat(obj.Root)
if err != nil {
return false, errwrap.Wrapf(err, "can't stat Root dir")
}
if !fileInfo.IsDir() {
return false, fmt.Errorf("the Root path is not a dir")
}
}
return true, nil // always succeeds, with nothing to do!
}
// Cmp compares two resources and returns an error if they are not equivalent.
func (obj *HTTPServerRes) Cmp(r engine.Res) error {
// we can only compare HTTPServerRes to others of the same resource kind
res, ok := r.(*HTTPServerRes)
if !ok {
return fmt.Errorf("res is not the same kind")
}
if obj.Address != res.Address {
return fmt.Errorf("the Address differs")
}
if (obj.Timeout == nil) != (res.Timeout == nil) { // xor
return fmt.Errorf("the Timeout differs")
}
if obj.Timeout != nil && res.Timeout != nil {
if *obj.Timeout != *res.Timeout { // compare the values
return fmt.Errorf("the value of Timeout differs")
}
}
if (obj.ReadTimeout == nil) != (res.ReadTimeout == nil) {
return fmt.Errorf("the ReadTimeout differs")
}
if obj.ReadTimeout != nil && res.ReadTimeout != nil {
if *obj.ReadTimeout != *res.ReadTimeout {
return fmt.Errorf("the value of ReadTimeout differs")
}
}
if (obj.WriteTimeout == nil) != (res.WriteTimeout == nil) {
return fmt.Errorf("the WriteTimeout differs")
}
if obj.WriteTimeout != nil && res.WriteTimeout != nil {
if *obj.WriteTimeout != *res.WriteTimeout {
return fmt.Errorf("the value of WriteTimeout differs")
}
}
if (obj.ShutdownTimeout == nil) != (res.ShutdownTimeout == nil) {
return fmt.Errorf("the ShutdownTimeout differs")
}
if obj.ShutdownTimeout != nil && res.ShutdownTimeout != nil {
if *obj.ShutdownTimeout != *res.ShutdownTimeout {
return fmt.Errorf("the value of ShutdownTimeout differs")
}
}
// TODO: We could do this sort of thing to skip checking Timeout when it
// is not used, but for the moment, this is overkill and not needed yet.
//a := obj.ReadTimeout != nil
//b := obj.WriteTimeout != nil
//c := obj.ShutdownTimeout != nil
//if !(obj.Timeout != nil && (a && b && c)) {
// // the Timeout param is not being ignored
//}
if obj.Root != res.Root {
return fmt.Errorf("the Root differs")
}
return nil
}
// Interrupt is called to ask the execution of this resource to end early. It
// will cause the server Shutdown to end abruptly instead of leading open client
// connections terminate gracefully. It does this by causing the server Close
// method to run.
func (obj *HTTPServerRes) Interrupt() error {
close(obj.interruptChan) // this should cause obj.server.Close() to run!
return nil
}
// Copy copies the resource. Don't call it directly, use engine.ResCopy instead.
// TODO: should this copy internal state?
func (obj *HTTPServerRes) Copy() engine.CopyableRes {
var timeout, readTimeout, writeTimeout, shutdownTimeout *uint64
if obj.Timeout != nil {
x := *obj.Timeout
timeout = &x
}
if obj.ReadTimeout != nil {
x := *obj.ReadTimeout
readTimeout = &x
}
if obj.WriteTimeout != nil {
x := *obj.WriteTimeout
writeTimeout = &x
}
if obj.ShutdownTimeout != nil {
x := *obj.ShutdownTimeout
shutdownTimeout = &x
}
return &HTTPServerRes{
Address: obj.Address,
Timeout: timeout,
ReadTimeout: readTimeout,
WriteTimeout: writeTimeout,
ShutdownTimeout: shutdownTimeout,
Root: obj.Root,
}
}
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
// primarily useful for setting the defaults.
func (obj *HTTPServerRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
type rawRes HTTPServerRes // indirection to avoid infinite recursion
def := obj.Default() // get the default
res, ok := def.(*HTTPServerRes) // put in the right format
if !ok {
return fmt.Errorf("could not convert to HTTPServerRes")
}
raw := rawRes(*res) // convert; the defaults go here
if err := unmarshal(&raw); err != nil {
return err
}
*obj = HTTPServerRes(raw) // restore from indirection with type conversion!
return nil
}
// GroupCmp returns whether two resources can be grouped together or not. Can
// these two resources be merged, aka, does this resource support doing so? Will
// resource allow itself to be grouped _into_ this obj?
func (obj *HTTPServerRes) GroupCmp(r engine.GroupableRes) error {
res1, ok1 := r.(*HTTPFileRes) // different from what we usually do!
if ok1 {
// If the http file resource has the Server field specified,
// then it must match against our name field if we want it to
// group with us.
if res1.Server != "" && res1.Server != obj.Name() {
return fmt.Errorf("resource groups with a different server name")
}
return nil
}
return fmt.Errorf("resource is not the right kind")
}
// readHandler handles all the incoming download requests from clients.
func (obj *HTTPServerRes) handler() func(http.ResponseWriter, *http.Request) {
// TODO: we could statically pre-compute some stuff here...
return func(w http.ResponseWriter, req *http.Request) {
if obj.init.Debug {
obj.init.Logf("Client: %s", req.RemoteAddr)
}
// TODO: would this leak anything security sensitive in our log?
obj.init.Logf("URL: %s", req.URL)
if obj.init.Debug {
obj.init.Logf("Path: %s", req.URL.Path)
}
// We only allow GET at the moment.
if req.Method != http.MethodGet {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
requestPath := req.URL.Path // TODO: is this what we want here?
//var handle io.Reader // TODO: simplify?
var handle io.ReadSeeker
// Look through the autogrouped resources!
// TODO: can we improve performance by only searching here once?
for _, x := range obj.GetGroup() { // grouped elements
res, ok := x.(*HTTPFileRes) // convert from Res
if !ok {
continue
}
if requestPath != res.getPath() {
continue // not me
}
if obj.init.Debug {
obj.init.Logf("Got grouped file: %s", res.String())
}
var err error
handle, err = res.getContent()
if err != nil {
obj.init.Logf("could not get content for: %s", requestPath)
msg, httpStatus := toHTTPError(err)
http.Error(w, msg, httpStatus)
return
}
break
}
// Look in root if we have one, and we haven't got a file yet...
if obj.Root != "" && handle == nil {
p := filepath.Join(obj.Root, requestPath) // normal unsafe!
if !strings.HasPrefix(p, obj.Root) { // root ends with /
// user might have tried a ../../etc/passwd hack
obj.init.Logf("join inconsistency: %s", p)
http.NotFound(w, req) // lie to them...
return
}
if HTTPUseSecureJoin {
var err error
p, err = securefilepath.SecureJoin(obj.Root, requestPath)
if err != nil {
obj.init.Logf("secure join fail: %s", p)
http.NotFound(w, req) // lie to them...
return
}
}
if obj.init.Debug {
obj.init.Logf("Got file at root: %s", p)
}
var err error
handle, err = os.Open(p)
if err != nil {
obj.init.Logf("could not open: %s", p)
msg, httpStatus := toHTTPError(err)
http.Error(w, msg, httpStatus)
return
}
}
// We never found a file...
if handle == nil {
if obj.init.Debug || true { // XXX: maybe we should always do this?
obj.init.Logf("File not found: %s", requestPath)
}
http.NotFound(w, req)
return
}
// Determine the last-modified time if we can.
modtime := time.Now()
if f, ok := handle.(*os.File); ok {
fi, err := f.Stat()
if err == nil {
modtime = fi.ModTime()
}
// TODO: if Stat errors, should we fail the whole thing?
}
// XXX: is requestPath what we want for the name field?
http.ServeContent(w, req, requestPath, modtime, handle)
//obj.init.Logf("%d bytes sent", n) // XXX: how do we know (on the server-side) if it worked?
return
}
}
// HTTPFileRes is a file that exists within an http server. The name is used as
// the public path of the file, unless the filename field is specified, and in
// that case it is used instead. The way this works is that it autogroups at
// runtime with an existing http resource, and in doing so makes the file
// associated with this resource available for serving from that http server.
type HTTPFileRes struct {
traits.Base // add the base methods without re-implementation
traits.Edgeable // XXX: add autoedge support
traits.Groupable // can be grouped into HTTPServerRes
init *engine.Init
// Server is the name of the http server resource to group this into. If
// it is omitted, and there is only a single http resource, then it will
// be grouped into it automatically. If there is more than one main http
// resource being used, then the grouping behaviour is *undefined* when
// this is not specified, and it is not recommended to leave this blank!
Server string `lang:"server" yaml:"server"`
// Filename is the name of the file this data should appear as on the
// http server.
Filename string `lang:"filename" yaml:"filename"`
// Path is the absolute path to a file that should be used as the source
// for this file resource. It must not be combined with the data field.
Path string `lang:"path" yaml:"path"`
// Data is the file content that should be used as the source for this
// file resource. It must not be combined with the path field.
// TODO: should this be []byte instead?
Data string `lang:"data" yaml:"data"`
}
// Default returns some sensible defaults for this resource.
func (obj *HTTPFileRes) Default() engine.Res {
return &HTTPFileRes{}
}
// getPath returns the actual path we respond to. When Filename is not
// specified, we use the Name. Note that this is the filename that will be seen
// on the http server, it is *not* the source path to the actual file contents
// being sent by the server.
func (obj *HTTPFileRes) getPath() string {
if obj.Filename != "" {
return obj.Filename
}
return obj.Name()
}
// getContent returns the content that we expect from this resource. It depends
// on whether the user specified the Path or Data fields, and whether the Path
// exists or not.
func (obj *HTTPFileRes) getContent() (io.ReadSeeker, error) {
if obj.Path != "" && obj.Data != "" {
// programming error! this should have been caught in Validate!
return nil, fmt.Errorf("must not specify Path and Data")
}
if obj.Path != "" {
return os.Open(obj.Path)
}
return bytes.NewReader([]byte(obj.Data)), nil
}
// Validate checks if the resource data structure was populated correctly.
func (obj *HTTPFileRes) Validate() error {
if obj.getPath() == "" {
return fmt.Errorf("empty filename")
}
// FIXME: does getPath need to start with a slash?
if obj.Path != "" && !strings.HasPrefix(obj.Path, "/") {
return fmt.Errorf("the Path must be absolute")
}
if obj.Path != "" && obj.Data != "" {
return fmt.Errorf("must not specify Path and Data")
}
// NOTE: if obj.Path == "" && obj.Data == "" then we have an empty file!
return nil
}
// Init runs some startup code for this resource.
func (obj *HTTPFileRes) Init(init *engine.Init) error {
obj.init = init // save for later
return nil
}
// Close is run by the engine to clean up after the resource is done.
func (obj *HTTPFileRes) Close() error {
return nil
}
// Watch is the primary listener for this resource and it outputs events. This
// particular one does absolutely nothing but block until we've received a done
// signal.
func (obj *HTTPFileRes) Watch() error {
obj.init.Running() // when started, notify engine that we're running
select {
case <-obj.init.Done: // closed by the engine to signal shutdown
}
//obj.init.Event() // notify engine of an event (this can block)
return nil
}
// CheckApply never has anything to do for this resource, so it always succeeds.
func (obj *HTTPFileRes) CheckApply(apply bool) (bool, error) {
if obj.init.Debug {
obj.init.Logf("CheckApply")
}
return true, nil // always succeeds, with nothing to do!
}
// Cmp compares two resources and returns an error if they are not equivalent.
func (obj *HTTPFileRes) Cmp(r engine.Res) error {
// we can only compare HTTPFileRes to others of the same resource kind
res, ok := r.(*HTTPFileRes)
if !ok {
return fmt.Errorf("res is not the same kind")
}
if obj.Server != res.Server {
return fmt.Errorf("the Server field differs")
}
if obj.Filename != res.Filename {
return fmt.Errorf("the Filename differs")
}
if obj.Path != res.Path {
return fmt.Errorf("the Path differs")
}
if obj.Data != res.Data {
return fmt.Errorf("the Data differs")
}
return nil
}
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
// primarily useful for setting the defaults.
func (obj *HTTPFileRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
type rawRes HTTPFileRes // indirection to avoid infinite recursion
def := obj.Default() // get the default
res, ok := def.(*HTTPFileRes) // put in the right format
if !ok {
return fmt.Errorf("could not convert to HTTPFileRes")
}
raw := rawRes(*res) // convert; the defaults go here
if err := unmarshal(&raw); err != nil {
return err
}
*obj = HTTPFileRes(raw) // restore from indirection with type conversion!
return nil
}
// toHTTPError returns a non-specific HTTP error message and status code for a
// given non-nil error value. It's important that toHTTPError does not actually
// return err.Error(), since msg and httpStatus are returned to users, and
// historically Go's ServeContent always returned just "404 Not Found" for all
// errors. We don't want to start leaking information in error messages.
// NOTE: This was copied and modified slightly from the golang net/http package.
// See: https://github.com/golang/go/issues/38375
func toHTTPError(err error) (msg string, httpStatus int) {
if os.IsNotExist(err) {
//return "404 page not found", http.StatusNotFound
return http.StatusText(http.StatusNotFound), http.StatusNotFound
}
if os.IsPermission(err) {
//return "403 Forbidden", http.StatusForbidden
return http.StatusText(http.StatusForbidden), http.StatusForbidden
}
// Default:
//return "500 Internal Server Error", http.StatusInternalServerError
return http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError
}

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2021+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
@@ -34,10 +34,12 @@ func init() {
engine.RegisterResource("kv", func() engine.Res { return &KVRes{} })
}
// KVResSkipCmpStyle represents the different styles of comparison when using SkipLessThan.
// KVResSkipCmpStyle represents the different styles of comparison when using
// SkipLessThan.
type KVResSkipCmpStyle int
// These are the different allowed comparison styles. Most folks will want SkipCmpStyleInt.
// These are the different allowed comparison styles. Most folks will want
// SkipCmpStyleInt.
const (
SkipCmpStyleInt KVResSkipCmpStyle = iota
SkipCmpStyleString
@@ -308,8 +310,8 @@ type KVUID struct {
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.
// 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 *KVRes) UIDs() []engine.ResUID {
x := &KVUID{
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
@@ -318,8 +320,8 @@ func (obj *KVRes) UIDs() []engine.ResUID {
return []engine.ResUID{x}
}
// UnmarshalYAML is the custom unmarshal handler for this struct.
// It is primarily useful for setting the defaults.
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
// primarily useful for setting the defaults.
func (obj *KVRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
type rawRes KVRes // indirection to avoid infinite recursion

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2021+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
@@ -403,8 +403,8 @@ func (obj *MountUID) IFF(uid engine.ResUID) bool {
return obj.name == res.name
}
// UIDs includes all params to make a unique identification of this object.
// Most resources only return one although some resources can return multiple.
// 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 *MountRes) UIDs() []engine.ResUID {
x := &MountUID{
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
@@ -413,8 +413,8 @@ func (obj *MountRes) UIDs() []engine.ResUID {
return []engine.ResUID{x}
}
// UnmarshalYAML is the custom unmarshal handler for this struct.
// It is primarily useful for setting the defaults.
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
// primarily useful for setting the defaults.
func (obj *MountRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
type rawRes MountRes // indirection to avoid infinite recursion
@@ -499,8 +499,8 @@ func (obj *MountRes) fstabEntryRemove(file string, mount *fstab.Mount) error {
return obj.fstabWrite(file, mounts)
}
// fstabWrite generates an fstab file with the given mounts, and writes them
// to the provided fstab file.
// fstabWrite generates an fstab file with the given mounts, and writes them to
// the provided fstab file.
func (obj *MountRes) fstabWrite(file string, mounts fstab.Mounts) error {
// build the file contents
contents := fmt.Sprintf("# Generated by %s at %d", obj.init.Program, time.Now().UnixNano()) + "\n"
@@ -541,9 +541,9 @@ func mountExists(file string, mount *fstab.Mount) (bool, error) {
return false, nil
}
// mountCompare compares two mounts. It is assumed that the first comes from
// a resource definition, and the second comes from /proc/mounts. It compares
// the two after resolving the loopback device's file path (if necessary,) and
// mountCompare compares two mounts. It is assumed that the first comes from a
// resource definition, and the second comes from /proc/mounts. It compares the
// two after resolving the loopback device's file path (if necessary,) and
// ignores freq and passno, as they may differ between the definition and
// /proc/mounts.
func mountCompare(def, proc *fstab.Mount) (bool, error) {
@@ -599,8 +599,8 @@ func mountReload() error {
return nil
}
// restartUnit restarts the given dbus unit and waits for it to finish
// starting up. If restartTimeout is exceeded, it will return an error.
// restartUnit restarts the given dbus unit and waits for it to finish starting
// up. If restartTimeout is exceeded, it will return an error.
func restartUnit(conn *dbus.Conn, unit string) error {
// timeout if we don't get the JobRemoved event
ctx, cancel := context.WithTimeout(context.TODO(), dbusRestartCtxTimeout*time.Second)

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2021+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2021+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2021+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
@@ -231,8 +231,8 @@ type MsgUID struct {
body string
}
// UIDs includes all params to make a unique identification of this object.
// Most resources only return one, although some resources can return multiple.
// 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 *MsgRes) UIDs() []engine.ResUID {
x := &MsgUID{
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
@@ -241,8 +241,8 @@ func (obj *MsgRes) UIDs() []engine.ResUID {
return []engine.ResUID{x}
}
// UnmarshalYAML is the custom unmarshal handler for this struct.
// It is primarily useful for setting the defaults.
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
// primarily useful for setting the defaults.
func (obj *MsgRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
type rawRes MsgRes // indirection to avoid infinite recursion

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2021+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2021+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
@@ -81,17 +81,32 @@ const (
socketFile = "pipe.sock" // path in vardir to store our socket file
)
// NetRes is a network interface resource based on netlink. It manages the
// state of a network link. Configuration is also stored in a networkd
// configuration file, so the network is available upon reboot.
// NetRes is a network interface resource based on netlink. It manages the state
// of a network link. Configuration is also stored in a networkd configuration
// file, so the network is available upon reboot. The name of the resource is
// the string representing the network interface name. This could be "eth0" for
// example.
type NetRes struct {
traits.Base // add the base methods without re-implementation
init *engine.Init
State string `yaml:"state"` // up, down, or empty
Addrs []string `yaml:"addrs"` // list of addresses in cidr format
Gateway string `yaml:"gateway"` // gateway address
// State is the desired state of the interface. It can be "up", "down",
// or the empty string to leave that unspecified.
State string `lang:"state" yaml:"state"`
// Addrs is the list of addresses to set on the interface. They must
// each be in CIDR notation such as: 192.0.2.42/24 for example.
Addrs []string `lang:"addrs" yaml:"addrs"`
// Gateway represents the default route to set for the interface.
Gateway string `lang:"gateway" yaml:"gateway"`
// IPForward is a boolean that sets whether we should forward incoming
// packets onward when this is set. It default to unspecified, which
// downstream (in the systemd-networkd configuration) defaults to false.
// XXX: this could also be "ipv4" or "ipv6", add those as a second option?
IPForward *bool `lang:"ip_forward" yaml:"ip_forward"`
iface *iface // a struct containing the net.Interface and netlink.Link
unitFilePath string // the interface unit file path
@@ -99,8 +114,8 @@ type NetRes struct {
socketFile string // path for storing the pipe socket file
}
// nlChanStruct defines the channel used to send netlink messages and errors
// to the event processing loop in Watch.
// nlChanStruct defines the channel used to send netlink messages and errors to
// the event processing loop in Watch.
type nlChanStruct struct {
msg []syscall.NetlinkMessage
err error
@@ -371,8 +386,8 @@ func (obj *NetRes) addrCheckApply(apply bool) (bool, error) {
return false, nil
}
// gatewayCheckApply checks if the interface has the correct default gateway
// and adds/deletes routes as necessary.
// gatewayCheckApply checks if the interface has the correct default gateway and
// adds/deletes routes as necessary.
func (obj *NetRes) gatewayCheckApply(apply bool) (bool, error) {
// get all routes from the interface
routes, err := netlink.RouteList(obj.iface.link, netlink.FAMILY_V4)
@@ -548,8 +563,8 @@ func (obj *NetUID) IFF(uid engine.ResUID) bool {
return obj.name == res.name
}
// UIDs includes all params to make a unique identification of this object.
// Most resources only return one although some resources can return multiple.
// 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 *NetRes) UIDs() []engine.ResUID {
x := &NetUID{
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
@@ -558,8 +573,8 @@ func (obj *NetRes) UIDs() []engine.ResUID {
return []engine.ResUID{x}
}
// UnmarshalYAML is the custom unmarshal handler for this struct.
// It is primarily useful for setting the defaults.
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
// primarily useful for setting the defaults.
func (obj *NetRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
type rawRes NetRes // indirection to avoid infinite recursion
@@ -590,6 +605,13 @@ func (obj *NetRes) unitFileContents() []byte {
if obj.Gateway != "" {
u = append(u, fmt.Sprintf("Gateway=%s", obj.Gateway))
}
if obj.IPForward != nil {
b := "false"
if *obj.IPForward {
b = "true"
}
u = append(u, fmt.Sprintf("IPForward=%s", b))
}
c := strings.Join(u, "\n")
return []byte(c)
}
@@ -625,8 +647,8 @@ func (obj *iface) linkUpDown(state string) error {
return netlink.LinkSetDown(obj.link)
}
// getAddrs returns a list of strings containing all of the interface's
// IP addresses in CIDR format.
// getAddrs returns a list of strings containing all of the interface's IP
// addresses in CIDR format.
func (obj *iface) getAddrs() ([]string, error) {
var ifaceAddrs []string
a, err := obj.iface.Addrs()
@@ -694,8 +716,8 @@ func (obj *iface) kernelApply(addrs []string) error {
return nil
}
// addrApplyDelete, checks the interface's addresses and deletes any that are not
// in the list/definition.
// addrApplyDelete, checks the interface's addresses and deletes any that are
// not in the list/definition.
func (obj *iface) addrApplyDelete(objAddrs []string) error {
ifaceAddrs, err := obj.getAddrs()
if err != nil {

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2021+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2021+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
@@ -103,8 +103,8 @@ type NoopUID struct {
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.
// 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 *NoopRes) UIDs() []engine.ResUID {
x := &NoopUID{
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
@@ -126,8 +126,8 @@ func (obj *NoopRes) GroupCmp(r engine.GroupableRes) error {
return nil // noop resources can always be grouped together!
}
// UnmarshalYAML is the custom unmarshal handler for this struct.
// It is primarily useful for setting the defaults.
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
// primarily useful for setting the defaults.
func (obj *NoopRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
type rawRes NoopRes // indirection to avoid infinite recursion

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2021+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2021+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
@@ -73,8 +73,8 @@ func (obj *NspawnRes) Default() engine.Res {
}
}
// makeComposite creates a pointer to a SvcRes. The pointer is used to
// validate and initialize the nested svc.
// makeComposite creates a pointer to a SvcRes. The pointer is used to validate
// and initialize the nested svc.
func (obj *NspawnRes) makeComposite() (*SvcRes, error) {
res, err := engine.NewNamedResource("svc", fmt.Sprintf(nspawnServiceTmpl, obj.Name()))
if err != nil {
@@ -113,7 +113,7 @@ func (obj *NspawnRes) Validate() error {
return errwrap.Wrapf(err, "makeComposite failed in validate")
}
if err := svc.Validate(); err != nil { // composite resource
return errwrap.Wrapf(err, "validate failed for embedded svc: %s", obj.svc)
return errwrap.Wrapf(err, "validate failed for embedded svc: %s", svc)
}
return nil
}
@@ -128,10 +128,7 @@ func (obj *NspawnRes) Init(init *engine.Init) error {
}
obj.svc = svc
// TODO: we could build a new init that adds a prefix to the logger...
if err := obj.svc.Init(init); err != nil {
return err
}
return nil
return obj.svc.Init(init)
}
// Close is run by the engine to clean up after the resource is done.
@@ -304,8 +301,8 @@ func (obj *NspawnUID) IFF(uid engine.ResUID) bool {
return obj.name == res.name
}
// UIDs includes all params to make a unique identification of this object.
// Most resources only return one although some resources can return multiple.
// 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 *NspawnRes) UIDs() []engine.ResUID {
x := &NspawnUID{
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
@@ -314,8 +311,8 @@ func (obj *NspawnRes) UIDs() []engine.ResUID {
return append([]engine.ResUID{x}, obj.svc.UIDs()...)
}
// UnmarshalYAML is the custom unmarshal handler for this struct.
// It is primarily useful for setting the defaults.
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
// primarily useful for setting the defaults.
func (obj *NspawnRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
type rawRes NspawnRes // indirection to avoid infinite recursion

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2021+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
@@ -37,7 +37,8 @@ const (
Paranoid = false // enable if you see any ghosts
)
// constants which might need to be tweaked or which contain special dbus strings.
// constants which might need to be tweaked or which contain special dbus
// strings.
const (
// FIXME: if PkBufferSize is too low, install seems to drop signals
PkBufferSize = 1000
@@ -71,7 +72,7 @@ var (
}
)
//type enum_filter uint64
// type enum_filter uint64
// https://github.com/hughsie/PackageKit/blob/master/lib/packagekit-glib2/pk-enum.c
const ( //static const PkEnumMatch enum_filter[]
PkFilterEnumUnknown uint64 = 1 << iota // "unknown"
@@ -154,7 +155,8 @@ type Conn struct {
Logf func(format string, v ...interface{})
}
// PkPackageIDActionData is a struct that is returned by PackagesToPackageIDs in the map values.
// PkPackageIDActionData is a struct that is returned by PackagesToPackageIDs in
// the map values.
type PkPackageIDActionData struct {
Found bool
Installed bool
@@ -185,7 +187,8 @@ func (obj *Conn) Close() error {
return obj.conn.Close()
}
// internal helper to add signal matches to the bus, should only be called once
// matchSignal is an internal helper to add signal matches to the bus. It should
// only be called once.
func (obj *Conn) matchSignal(ch chan *dbus.Signal, path dbus.ObjectPath, iface string, signals []string) (func() error, error) {
if obj.Debug {
obj.Logf("matchSignal(%v, %v, %s, %v)", ch, path, iface, signals)
@@ -565,7 +568,8 @@ loop:
return nil
}
// GetFilesByPackageID gets the list of files that are contained inside a list of packageIDs.
// GetFilesByPackageID gets the list of files that are contained inside a list
// of packageIDs.
func (obj *Conn) GetFilesByPackageID(packageIDs []string) (files map[string][]string, err error) {
// NOTE: the maximum number of files in an RPM is 52116 in Fedora 23
// https://gist.github.com/purpleidea/b98e60dcd449e1ac3b8a
@@ -634,7 +638,8 @@ loop:
return
}
// GetUpdates gets a list of packages that are installed and which can be updated, mod filter.
// GetUpdates gets a list of packages that are installed and which can be
// updated, mod filter.
func (obj *Conn) GetUpdates(filter uint64) ([]string, error) {
if obj.Debug {
obj.Logf("GetUpdates()")
@@ -876,7 +881,8 @@ func (obj *Conn) PackagesToPackageIDs(packageMap map[string]string, filter uint6
return result, nil
}
// FilterPackageIDs returns a list of packageIDs which match the set of package names in packages.
// FilterPackageIDs returns a list of packageIDs which match the set of package
// names in packages.
func FilterPackageIDs(m map[string]*PkPackageIDActionData, packages []string) ([]string, error) {
result := []string{}
for _, k := range packages {
@@ -890,7 +896,8 @@ func FilterPackageIDs(m map[string]*PkPackageIDActionData, packages []string) ([
return result, nil
}
// FilterState returns a map of whether each package queried matches the particular state.
// FilterState returns a map of whether each package queried matches the
// particular state.
func FilterState(m map[string]*PkPackageIDActionData, packages []string, state string) (result map[string]bool, err error) {
result = make(map[string]bool)
pkgs := []string{} // bad pkgs that don't have a bool state
@@ -920,7 +927,8 @@ func FilterState(m map[string]*PkPackageIDActionData, packages []string, state s
return result, err
}
// FilterPackageState returns all packages that are in package and match the specific state.
// FilterPackageState returns all packages that are in package and match the
// specific state.
func FilterPackageState(m map[string]*PkPackageIDActionData, packages []string, state string) (result []string, err error) {
result = []string{}
for _, k := range packages {
@@ -946,7 +954,8 @@ func FilterPackageState(m map[string]*PkPackageIDActionData, packages []string,
return result, err
}
// FlagInData asks whether a flag exists inside the data portion of a packageID field?
// FlagInData asks whether a flag exists inside the data portion of a packageID
// field?
func FlagInData(flag, data string) bool {
flags := strings.Split(data, ":")
for _, f := range flags {

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2021+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
@@ -322,8 +322,8 @@ type PasswordUID struct {
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.
// 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 *PasswordRes) UIDs() []engine.ResUID {
x := &PasswordUID{
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
@@ -347,8 +347,8 @@ func (obj *PasswordRes) Sends() interface{} {
}
}
// UnmarshalYAML is the custom unmarshal handler for this struct.
// It is primarily useful for setting the defaults.
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
// primarily useful for setting the defaults.
func (obj *PasswordRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
type rawRes PasswordRes // indirection to avoid infinite recursion

329
engine/resources/pippet.go Normal file
View File

@@ -0,0 +1,329 @@
// Mgmt
// Copyright (C) 2013-2021+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> 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 <http://www.gnu.org/licenses/>.
package resources
import (
"encoding/json"
"fmt"
"io"
"os/exec"
"sync"
"github.com/purpleidea/mgmt/engine"
"github.com/purpleidea/mgmt/engine/traits"
"github.com/purpleidea/mgmt/util/errwrap"
)
var pippetReceiverInstance *pippetReceiver
var pippetReceiverOnce sync.Once
func init() {
engine.RegisterResource("pippet", func() engine.Res { return &PippetRes{} })
}
// PippetRes is a wrapper resource for puppet. It implements the functional
// equivalent of an exec resource that calls "puppet resource <type> <title>
// <params>", but offers superior performance through a long-running Puppet
// process that receives resources through a pipe (hence the name).
type PippetRes struct {
traits.Base // add the base methods without re-implementation
traits.Refreshable
init *engine.Init
// Type is the exact name of the wrapped Puppet resource type, e.g.
// "file", "mount". This needs not be a core type. It can be a type
// from a module. The Puppet installation local to the mgmt agent
// machine must be able recognize it. It has to be a native type though,
// as opposed to defined types from your Puppet manifest code.
Type string `yaml:"type" json:"type"`
// Title is used by Puppet as the resource title. Puppet will often
// assign special meaning to the title, e.g. use it as the path for a
// file resource, or the name of a package.
Title string `yaml:"title" json:"title"`
// Params is expected to be a hash in YAML format, pairing resource
// parameter names with their respective values, e.g. { ensure: present
// }
Params string `yaml:"params" json:"params"`
runner *pippetReceiver
}
// Default returns an example Pippet resource.
func (obj *PippetRes) Default() engine.Res {
return &PippetRes{
Params: "{}", // use an empty params hash per default
}
}
// Validate never errors out. We don't know the set of potential types that
// Puppet supports. Resource names are arbitrary. We cannot really validate the
// parameter YAML, because we cannot assume that it can be unmarshalled into a
// map[string]string; Puppet supports complex parameter values.
func (obj *PippetRes) Validate() error {
return nil
}
// Init makes sure that the PippetReceiver object is initialized.
func (obj *PippetRes) Init(init *engine.Init) error {
obj.init = init // save for later
obj.runner = getPippetReceiverInstance()
return obj.runner.Register()
}
// Close is run by the engine to clean up after the resource is done.
func (obj *PippetRes) Close() error {
return obj.runner.Unregister()
}
// Watch is the primary listener for this resource and it outputs events.
func (obj *PippetRes) Watch() error {
obj.init.Running() // when started, notify engine that we're running
select {
case <-obj.init.Done: // closed by the engine to signal shutdown
}
//obj.init.Event() // notify engine of an event (this can block)
return nil
}
// CheckApply synchronizes the resource if required.
func (obj *PippetRes) CheckApply(apply bool) (bool, error) {
changed, err := applyPippetRes(obj.runner, obj)
if err != nil {
return false, fmt.Errorf("pippet: %s[%s]: ERROR - %v", obj.Type, obj.Title, err)
}
return !changed, nil
}
// Cmp compares two resources and returns an error if they are not equivalent.
func (obj *PippetRes) Cmp(r engine.Res) error {
res, ok := r.(*PippetRes)
if !ok {
return fmt.Errorf("not a %s", obj.Kind())
}
if obj.Type != res.Type {
return fmt.Errorf("the Type param differs")
}
if obj.Title != res.Title {
return fmt.Errorf("the Title param differs")
}
// FIXME: This is a lie. Parameter lists can be equivalent but not
// lexically identical (e.g. whitespace differences, parameter order).
// This is difficult to handle because we cannot casually unmarshall the
// YAML content.
if obj.Params != res.Params {
return fmt.Errorf("the Param param differs")
}
return nil
}
// PippetUID is the UID struct for PippetRes.
type PippetUID struct {
engine.BaseUID
resourceType string
resourceTitle 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 *PippetRes) UIDs() []engine.ResUID {
x := &PippetUID{
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
resourceType: obj.Type,
resourceTitle: obj.Title,
}
return []engine.ResUID{x}
}
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
// primarily useful for setting the defaults.
func (obj *PippetRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
type rawRes PippetRes // indirection to avoid infinite recursion
def := obj.Default() // get the default
res, ok := def.(*PippetRes) // put in the right format
if !ok {
return fmt.Errorf("could not convert to PippetRes")
}
raw := rawRes(*res) // convert; the defaults go here
if err := unmarshal(&raw); err != nil {
return err
}
*obj = PippetRes(raw) // restore from indirection with type conversion!
return nil
}
// PippetRunner is the interface used to communicate with the PippetReceiver
// object. Its main purpose is dependency injection.
type PippetRunner interface {
LockApply()
UnlockApply()
InputStream() io.WriteCloser
OutputStream() io.ReadCloser
}
// PippetResult is the structured return value type for the PippetReceiver's
// Apply function.
type PippetResult struct {
Error bool
Failed bool
Changed bool
Exception string
}
// GetPippetReceiverInstance returns a pointer to the PippetReceiver object. The
// PippetReceiver is supposed to be a singleton object. The pippet resource code
// should always use the PippetReceiverInstance function to gain access to the
// pippetReceiver object. Other objects of type pippetReceiver should not be
// created.
func getPippetReceiverInstance() *pippetReceiver {
for pippetReceiverInstance == nil {
pippetReceiverOnce.Do(func() { pippetReceiverInstance = &pippetReceiver{} })
}
return pippetReceiverInstance
}
type pippetReceiver struct {
stdin io.WriteCloser
stdout io.ReadCloser
registerMutex sync.Mutex
applyMutex sync.Mutex
registered int
}
// Init runs the Puppet process that will perform the work of synchronizing
// resources that are sent to its stdin. The process will keep running until
// Close is called. Init should not be called directly. It is implicitly called
// by the Register function.
func (obj *pippetReceiver) Init() error {
cmd := exec.Command("puppet", "yamlresource", "receive", "--color=no")
var err error
obj.stdin, err = cmd.StdinPipe()
if err != nil {
return err
}
obj.stdout, err = cmd.StdoutPipe()
if err != nil {
return errwrap.Append(err, obj.stdin.Close())
}
if err = cmd.Start(); err != nil {
return errwrap.Append(err, obj.stdin.Close())
}
buf := make([]byte, 80)
if _, err = obj.stdout.Read(buf); err != nil {
return errwrap.Append(err, obj.stdin.Close())
}
return nil
}
// Register should be called by any user (i.e., any pippet resource) before
// using the PippetRunner functions on this receiver object. Register implicitly
// takes care of calling Init if required.
func (obj *pippetReceiver) Register() error {
obj.registerMutex.Lock()
defer obj.registerMutex.Unlock()
obj.registered = obj.registered + 1
if obj.registered > 1 {
return nil
}
// count was increased from 0 to 1, we need to (re-)init
var err error
if err = obj.Init(); err != nil {
obj.registered = 0
}
return err
}
// Unregister should be called by any object that registered itself using the
// Register function, and which no longer needs the receiver. This should
// typically happen at closing time of the pippet resource that registered
// itself. Unregister implicitly calls Close in case all registered resources
// have unregistered.
func (obj *pippetReceiver) Unregister() error {
obj.registerMutex.Lock()
defer obj.registerMutex.Unlock()
obj.registered = obj.registered - 1
if obj.registered == 0 {
return obj.Close()
}
if obj.registered < 0 {
return fmt.Errorf("pippet runner: ERROR: unregistered more resources than were registered")
}
return nil
}
// LockApply locks the pippetReceiver's mutex for an "Apply" transaction.
func (obj *pippetReceiver) LockApply() {
obj.applyMutex.Lock()
}
// UnlockApply unlocks the pippetReceiver's mutex for an "Apply" transaction.
func (obj *pippetReceiver) UnlockApply() {
obj.applyMutex.Unlock()
}
// InputStream returns the pippetReceiver's pipe writer.
func (obj *pippetReceiver) InputStream() io.WriteCloser {
return obj.stdin
}
// OutputStream returns the pippetReceiver's pipe reader.
func (obj *pippetReceiver) OutputStream() io.ReadCloser {
return obj.stdout
}
// Close stops the backend puppet process by closing its stdin handle. It should
// not be called directly. It is implicitly called by the Unregister function if
// appropriate.
func (obj *pippetReceiver) Close() error {
return obj.stdin.Close()
}
// applyPippetRes does the actual work of making Puppet synchronize a resource.
func applyPippetRes(runner PippetRunner, resource *PippetRes) (bool, error) {
runner.LockApply()
defer runner.UnlockApply()
if err := json.NewEncoder(runner.InputStream()).Encode(resource); err != nil {
return false, errwrap.Wrapf(err, "failed to send resource to puppet")
}
result := PippetResult{
Error: true,
Exception: "missing output fields",
}
if err := json.NewDecoder(runner.OutputStream()).Decode(&result); err != nil {
return false, errwrap.Wrapf(err, "failed to read response from puppet")
}
if result.Error {
return false, fmt.Errorf("puppet did not sync: %s", result.Exception)
}
if result.Failed {
return false, fmt.Errorf("puppet failed to sync")
}
return result.Changed, nil
}

View File

@@ -0,0 +1,136 @@
// Mgmt
// Copyright (C) 2013-2021+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> 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 <http://www.gnu.org/licenses/>.
// +build !root
package resources
import (
"io"
"testing"
)
type nullWriteCloser struct {
}
type fakePippetReceiver struct {
stdin nullWriteCloser
stdout *io.PipeReader
Locked bool
}
func (obj nullWriteCloser) Write(data []byte) (int, error) {
return len(data), nil
}
func (obj nullWriteCloser) Close() error {
return nil
}
func (obj *fakePippetReceiver) LockApply() {
obj.Locked = true
}
func (obj *fakePippetReceiver) UnlockApply() {
obj.Locked = false
}
func (obj *fakePippetReceiver) InputStream() io.WriteCloser {
return obj.stdin
}
func (obj *fakePippetReceiver) OutputStream() io.ReadCloser {
return obj.stdout
}
func newFakePippetReceiver(jsonTestOutput string) *fakePippetReceiver {
output, input := io.Pipe()
result := &fakePippetReceiver{
stdout: output,
}
go func() {
// this will appear on the fake stdout
input.Write([]byte(jsonTestOutput))
}()
return result
}
var pippetTestRes = &PippetRes{
Type: "notify",
Title: "testmessage",
Params: `{msg: "This is a test"}`,
}
func TestNormalPuppetOutput(t *testing.T) {
r := newFakePippetReceiver(`{"resource":"Notify[test]","failed":false,"changed":true,"noop":false,"error":false,"exception":null}`)
changed, err := applyPippetRes(r, pippetTestRes)
if err != nil {
t.Errorf("normal Puppet output led to an apply error: %v", err)
}
if !changed {
t.Errorf("return values of applyPippetRes did not reflect the changed state")
}
}
func TestUnchangedPuppetOutput(t *testing.T) {
r := newFakePippetReceiver(`{"resource":"Notify[test]","failed":false,"changed":false,"noop":false,"error":false,"exception":null}`)
changed, err := applyPippetRes(r, pippetTestRes)
if err != nil {
t.Errorf("normal Puppet output led to an apply error: %v", err)
}
if changed {
t.Errorf("return values of applyPippetRes did not reflect the changed state")
}
}
func TestFailingPuppetOutput(t *testing.T) {
r := newFakePippetReceiver(`{"resource":"Notify[test]","failed":false,"changed":false,"noop":false,"error":true,"exception":"I failed!"}`)
_, err := applyPippetRes(r, pippetTestRes)
if err == nil {
t.Errorf("failing Puppet output led to an apply error: %v", err)
}
}
func TestEmptyPuppetOutput(t *testing.T) {
t.Skip("empty output will currently make the application (and the test) hang")
}
func TestPartialPuppetOutput(t *testing.T) {
r := newFakePippetReceiver(`{"resource":"Notify[test]","failed":false,"changed":true}`)
_, err := applyPippetRes(r, pippetTestRes)
if err == nil {
t.Errorf("partial Puppet output did not lead to an apply error")
}
}
func TestMalformedPuppetOutput(t *testing.T) {
r := newFakePippetReceiver(`oops something went wrong!!1!eleven`)
_, err := applyPippetRes(r, pippetTestRes)
if err == nil {
t.Errorf("malformed Puppet output did not lead to an apply error")
}
}

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2021+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
@@ -100,8 +100,8 @@ func (obj *PkgRes) Close() error {
return nil
}
// Watch is the primary listener for this resource and it outputs events.
// It uses the PackageKit UpdatesChanged signal to watch for changes.
// Watch is the primary listener for this resource and it outputs events. It
// uses the PackageKit UpdatesChanged signal to watch for changes.
// TODO: https://github.com/hughsie/PackageKit/issues/109
// TODO: https://github.com/hughsie/PackageKit/issues/110
func (obj *PkgRes) Watch() error {
@@ -504,7 +504,8 @@ func (obj *PkgResAutoEdges) Next() []engine.ResUID {
return result
}
// Test gets results of the earlier Next() call, & returns if we should continue!
// Test gets results of the earlier Next() call, & returns if we should
// continue!
func (obj *PkgResAutoEdges) Test(input []bool) bool {
if !obj.testIsNext {
panic("expecting a call to Next()")
@@ -591,8 +592,8 @@ func (obj *PkgRes) AutoEdges() (engine.AutoEdge, error) {
}, nil
}
// UIDs includes all params to make a unique identification of this object.
// Most resources only return one, although some resources can return multiple.
// 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 *PkgRes) UIDs() []engine.ResUID {
x := &PkgUID{
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
@@ -611,9 +612,9 @@ func (obj *PkgRes) UIDs() []engine.ResUID {
return result
}
// GroupCmp returns whether two resources can be grouped together or not.
// Can these two resources be merged, aka, does this resource support doing so?
// Will resource allow itself to be grouped _into_ this obj?
// GroupCmp returns whether two resources can be grouped together or not. Can
// these two resources be merged, aka, does this resource support doing so? Will
// resource allow itself to be grouped _into_ this obj?
func (obj *PkgRes) GroupCmp(r engine.GroupableRes) error {
res, ok := r.(*PkgRes)
if !ok {
@@ -631,8 +632,8 @@ func (obj *PkgRes) GroupCmp(r engine.GroupableRes) error {
return nil
}
// UnmarshalYAML is the custom unmarshal handler for this struct.
// It is primarily useful for setting the defaults.
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
// primarily useful for setting the defaults.
func (obj *PkgRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
type rawRes PkgRes // indirection to avoid infinite recursion
@@ -651,7 +652,8 @@ func (obj *PkgRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
return nil
}
// ReturnSvcInFileList returns a list of svc names for matches like: `/usr/lib/systemd/system/*.service`.
// ReturnSvcInFileList returns a list of svc names for matches like:
// `/usr/lib/systemd/system/*.service`.
func ReturnSvcInFileList(fileList []string) []string {
result := []string{}
for _, x := range fileList {

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2021+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2021+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
@@ -133,8 +133,8 @@ type PrintUID struct {
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.
// 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() []engine.ResUID {
x := &PrintUID{
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
@@ -156,8 +156,8 @@ func (obj *PrintRes) GroupCmp(r engine.GroupableRes) error {
return nil // grouped together if we were asked to
}
// UnmarshalYAML is the custom unmarshal handler for this struct.
// It is primarily useful for setting the defaults.
// 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

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2021+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
@@ -23,12 +23,17 @@ import (
"fmt"
"io/ioutil"
"os"
"os/user"
"path"
"strconv"
"strings"
"sync"
"syscall"
"testing"
"time"
"github.com/purpleidea/mgmt/engine"
"github.com/purpleidea/mgmt/pgraph"
"github.com/purpleidea/mgmt/util"
"github.com/purpleidea/mgmt/util/errwrap"
)
@@ -116,7 +121,8 @@ func (obj *changedStep) Action() error {
}
func (obj *changedStep) Expect() error { return nil }
// NewChangedStep waits up to this many ms for a CheckApply action to occur. Watch function to startup.
// NewChangedStep waits up to this many ms for a CheckApply action to occur.
// Watch function to startup.
func NewChangedStep(ms uint, expect bool) Step {
return &changedStep{
ms: ms,
@@ -171,7 +177,29 @@ func FileExpect(p, s string) Step { // path & string
}
}
// FileExpect takes a path and a string to write to that file, and builds a Step
// FileOwnerExpect takes a path and a uid to expect from that file, and builds a
// Step that checks that out of them.
func FileOwnerExpect(p, o string) Step { // path & owner
return &manualStep{
action: func() error { return nil },
expect: func() error {
var stat syscall.Stat_t
if err := syscall.Stat(p, &stat); err != nil {
return err
}
i, err := strconv.ParseUint(o, 10, 32)
if err != nil {
return err
}
if i != uint64(stat.Uid) {
return fmt.Errorf("file uid did not match in %s", p)
}
return nil
},
}
}
// FileWrite takes a path and a string to write to that file, and builds a Step
// that does that to them.
func FileWrite(p, s string) Step { // path & string
return &manualStep{
@@ -192,6 +220,15 @@ func ErrIsNotExistOK(e error) error {
return errwrap.Wrapf(e, "unexpected error")
}
// GetUID returns the UID of the user running this test.
func GetUID() (string, error) {
u, err := user.Lookup(os.Getenv("USER"))
if err != nil {
return "", err
}
return u.Uid, nil
}
func TestResources1(t *testing.T) {
type test struct { // an individual test
name string
@@ -225,7 +262,7 @@ func TestResources1(t *testing.T) {
p := "/tmp/whatever"
s := "hello, world\n"
res.Path = p
res.State = "exists"
res.State = FileStateExists
contents := s
res.Content = &contents
@@ -284,12 +321,48 @@ func TestResources1(t *testing.T) {
cleanup: func() error { return os.Remove(f) },
})
}
{
r := makeRes("exec", "x2")
res := r.(*ExecRes) // if this panics, the test will panic
res.Env = map[string]string{
"boiling": "one hundred",
}
f := "/tmp/whatever"
res.Cmd = fmt.Sprintf("env | grep boiling > %s", f)
res.Shell = "/bin/bash"
res.IfCmd = "! diff <(cat /tmp/whatever) <(echo boiling=one hundred)"
res.IfShell = "/bin/bash"
res.WatchCmd = fmt.Sprintf("/usr/bin/inotifywait -e modify -m %s", f)
res.WatchShell = "/bin/bash"
timeline := []Step{
NewStartupStep(1000 * 60), // startup
NewChangedStep(1000*60, false), // did we do something?
FileExpect(f, "boiling=one hundred\n"), // check initial state
NewClearChangedStep(1000 * 15), // did we do something?
FileWrite(f, "this is stuff!\n"), // change state
NewChangedStep(1000*60, false), // did we do something?
FileExpect(f, "boiling=one hundred\n"), // check again
sleep(1), // we can sleep too!
}
testCases = append(testCases, test{
name: "exec with env",
res: res,
fail: false,
timeline: timeline,
expect: func() error { return nil },
// build file for inotifywait
startup: func() error { return ioutil.WriteFile(f, []byte("starting...\n"), 0666) },
cleanup: func() error { return os.Remove(f) },
})
}
{
r := makeRes("file", "r1")
res := r.(*FileRes) // if this panics, the test will panic
p := "/tmp/emptyfile"
res.Path = p
res.State = "exists"
res.State = FileStateExists
timeline := []Step{
NewStartupStep(1000 * 60), // startup
@@ -313,7 +386,7 @@ func TestResources1(t *testing.T) {
res := r.(*FileRes) // if this panics, the test will panic
p := "/tmp/existingfile"
res.Path = p
res.State = "exists"
res.State = FileStateExists
content := "some existing text\n"
timeline := []Step{
@@ -332,6 +405,33 @@ func TestResources1(t *testing.T) {
cleanup: func() error { return os.Remove(p) },
})
}
{
r := makeRes("file", "r1")
res := r.(*FileRes) // if this panics, the test will panic
p := "/tmp/ownerfile"
uid, _ := GetUID()
res.Path = p
res.State = FileStateExists
res.Owner = uid
content := "some test file owned by uid " + uid
timeline := []Step{
NewStartupStep(1000 * 60), // startup
NewChangedStep(1000*60, true), // did we do something?
FileExpect(p, content), // check file content
FileOwnerExpect(p, uid), // check uid of the file
}
testCases = append(testCases, test{
name: "uid test file",
res: res,
fail: false,
timeline: timeline,
expect: func() error { return nil },
startup: func() error { return ioutil.WriteFile(p, []byte(content), 0666) },
cleanup: func() error { return os.Remove(p) },
})
}
names := []string{}
for index, tc := range testCases { // run all the tests
@@ -420,15 +520,20 @@ func TestResources1(t *testing.T) {
},
}
t.Logf("test #%d: running startup()", index)
if err := startup(); err != nil {
t.Errorf("test #%d: FAIL", index)
t.Errorf("test #%d: could not startup: %+v", index, err)
if startup != nil {
t.Logf("test #%d: running startup()", index)
if err := startup(); err != nil {
t.Errorf("test #%d: FAIL", index)
t.Errorf("test #%d: could not startup: %+v", index, err)
}
}
// run init
t.Logf("test #%d: running Init", index)
err = res.Init(init)
defer func() {
if cleanup == nil {
return
}
t.Logf("test #%d: running cleanup()", index)
if err := cleanup(); err != nil {
t.Errorf("test #%d: FAIL", index)
@@ -580,6 +685,29 @@ func TestResources2(t *testing.T) {
cleanup func() error // function to run as cleanup
}
type initOptions struct {
// graph is the graph that should be passed in with Init
graph *pgraph.Graph
// TODO: add more options if needed
// logf specifies the log function for Init to pass through...
logf func(format string, v ...interface{})
}
type initOption func(*initOptions)
addGraph := func(graph *pgraph.Graph) initOption {
return func(io *initOptions) {
io.graph = graph
}
}
addLogf := func(logf func(format string, v ...interface{})) initOption {
return func(io *initOptions) {
io.logf = logf
}
}
// resValidate runs Validate on the res.
resValidate := func(res engine.Res) func() error {
// run Close
@@ -588,9 +716,18 @@ func TestResources2(t *testing.T) {
}
}
// resInit runs Init on the res.
resInit := func(res engine.Res) func() error {
resInit := func(res engine.Res, opts ...initOption) func() error {
io := &initOptions{} // defaults
for _, optionFunc := range opts { // apply the options
optionFunc(io)
}
logf := func(format string, v ...interface{}) {
// noop for now
if io.logf == nil {
return
}
io.logf(fmt.Sprintf("test: ")+format+"\n", v...)
}
init := &engine.Init{
//Debug: debug,
@@ -603,6 +740,22 @@ func TestResources2(t *testing.T) {
Recv: func() map[string]*engine.Send {
return map[string]*engine.Send{}
},
// Copied from state.go
FilteredGraph: func() (*pgraph.Graph, error) {
//graph, err := pgraph.NewGraph("filtered")
//if err != nil {
// return nil, errwrap.Wrapf(err, "could not create graph")
//}
// Hack: We just add ourself as allowed since
// we're just a one-vertex test suite...
//graph.AddVertex(res) // hack!
//return graph, nil // we return in a func so it's fresh!
if io.graph == nil {
return nil, fmt.Errorf("use addGraph to add one here")
}
return io.graph, nil
},
}
// run Init
return func() error {
@@ -621,7 +774,7 @@ func TestResources2(t *testing.T) {
return errwrap.Wrapf(e, "error from CheckApply did not match expected")
}
if checkOK != expCheckOK {
return fmt.Errorf("result from CheckApply did not match expected: `%t` != `%t`", checkOK, expCheckOK)
return fmt.Errorf("result from CheckApply did not match expected: got: %t exp: %t", checkOK, expCheckOK)
}
return nil
}
@@ -702,10 +855,29 @@ func TestResources2(t *testing.T) {
return nil
}
}
fileExists := func(p string, dir bool) func() error {
// does the file exist?
return func() error {
fi, err := os.Stat(p)
if err != nil {
return fmt.Errorf("file was supposed to be present, got: %+v", err)
}
if fi.IsDir() != dir {
if dir {
return fmt.Errorf("not a dir")
}
return fmt.Errorf("not a regular file")
}
return nil
}
}
fileAbsent := func(p string) func() error {
// does the file exist?
return func() error {
_, err := os.Stat(p)
if err == nil {
return fmt.Errorf("file exists, expecting absent")
}
if !os.IsNotExist(err) {
return fmt.Errorf("file was supposed to be absent, got: %+v", err)
}
@@ -723,18 +895,27 @@ func TestResources2(t *testing.T) {
return nil
}
}
fileMkdir := func(p string, all bool) func() error {
// mkdir at the path
return func() error {
if all {
return os.MkdirAll(p, 0777)
}
return os.Mkdir(p, 0777)
}
}
testCases := []test{}
{
//file "/tmp/somefile" {
// state => "exists",
// state => $const.res.file.state.exists,
// content => "some new text\n",
//}
r1 := makeRes("file", "r1")
res := r1.(*FileRes) // if this panics, the test will panic
p := "/tmp/somefile"
res.Path = p
res.State = "exists"
res.State = FileStateExists
content := "some new text\n"
res.Content = &content
@@ -766,7 +947,7 @@ func TestResources2(t *testing.T) {
res := r1.(*FileRes) // if this panics, the test will panic
p := "/tmp/somefile"
res.Path = p
//res.State = "exists" // not specified!
//res.State = FileStateExists // not specified!
content := "some new text\n"
res.Content = &content
@@ -799,7 +980,7 @@ func TestResources2(t *testing.T) {
res := r1.(*FileRes) // if this panics, the test will panic
p := "/tmp/somefile"
res.Path = p
//res.State = "exists" // not specified!
//res.State = FileStateExists // not specified!
content := "some new text\n"
res.Content = &content
@@ -823,14 +1004,14 @@ func TestResources2(t *testing.T) {
}
{
//file "/tmp/somefile" {
// state => "absent",
// state => $const.res.file.state.absent,
//}
// and no existing file exists!
r1 := makeRes("file", "r1")
res := r1.(*FileRes) // if this panics, the test will panic
p := "/tmp/somefile"
res.Path = p
res.State = "absent"
res.State = FileStateAbsent
timeline := []func() error{
fileRemove(p), // nothing here
@@ -852,14 +1033,14 @@ func TestResources2(t *testing.T) {
}
{
//file "/tmp/somefile" {
// state => "absent",
// state => $const.res.file.state.absent,
//}
// and a file already exists!
r1 := makeRes("file", "r1")
res := r1.(*FileRes) // if this panics, the test will panic
p := "/tmp/somefile"
res.Path = p
res.State = "absent"
res.State = FileStateAbsent
timeline := []func() error{
fileWrite(p, "whatever"),
@@ -882,7 +1063,7 @@ func TestResources2(t *testing.T) {
{
//file "/tmp/somefile" {
// content => "some new text\n",
// state => "exists",
// state => $const.res.file.state.exists,
//
// Meta:reverse => true,
//}
@@ -890,7 +1071,7 @@ func TestResources2(t *testing.T) {
res := r1.(*FileRes) // if this panics, the test will panic
p := "/tmp/somefile"
res.Path = p
res.State = "exists"
res.State = FileStateExists
content := "some new text\n"
res.Content = &content
original := "this is the original state\n" // original state
@@ -951,7 +1132,7 @@ func TestResources2(t *testing.T) {
res := r1.(*FileRes) // if this panics, the test will panic
p := "/tmp/somefile"
res.Path = p
//res.State = "exists" // unspecified
//res.State = FileStateExists // unspecified
content := "some new text\n"
res.Content = &content
original := "this is the original state\n" // original state
@@ -1016,7 +1197,7 @@ func TestResources2(t *testing.T) {
res := r1.(*FileRes) // if this panics, the test will panic
p := "/tmp/somefile"
res.Path = p
//res.State = "exists" // unspecified
//res.State = FileStateExists // unspecified
content := "some new text\n"
res.Content = &content
var r2 engine.Res // future reversed resource
@@ -1065,7 +1246,7 @@ func TestResources2(t *testing.T) {
}
{
//file "/tmp/somefile" {
// state => "absent",
// state => $const.res.file.state.absent,
//
// Meta:reverse => true,
//}
@@ -1073,7 +1254,7 @@ func TestResources2(t *testing.T) {
res := r1.(*FileRes) // if this panics, the test will panic
p := "/tmp/somefile"
res.Path = p
res.State = "absent"
res.State = FileStateAbsent
original := "this is the original state\n" // original state
var r2 engine.Res // future reversed resource
@@ -1121,7 +1302,388 @@ func TestResources2(t *testing.T) {
cleanup: func() error { return nil },
})
}
{
//file "/tmp/somefile" {
// state => $const.res.file.state.exists,
// fragments => [
// "/tmp/frag1",
// "/tmp/fragdir1/",
// "/tmp/frag2",
// "/tmp/fragdir2/",
// "/tmp/frag3",
// ],
//}
r1 := makeRes("file", "r1")
res := r1.(*FileRes) // if this panics, the test will panic
p := "/tmp/somefile"
res.Path = p
res.State = FileStateExists
res.Fragments = []string{
"/tmp/frag1",
"/tmp/fragdir1/",
"/tmp/frag2",
"/tmp/fragdir2/",
"/tmp/frag3",
}
frag1 := "frag1\n"
f1 := "f1\n"
f2 := "f2\n"
f3 := "f3\n"
frag2 := "frag2\n"
f1d2 := "f1 from fragdir2\n"
f2d2 := "f2 from fragdir2\n"
f3d2 := "f3 from fragdir2\n"
frag3 := "frag3\n"
content := frag1 + f1 + f2 + f3 + frag2 + f1d2 + f2d2 + f3d2 + frag3
timeline := []func() error{
fileWrite("/tmp/frag1", frag1),
fileWrite("/tmp/frag2", frag2),
fileWrite("/tmp/frag3", frag3),
fileMkdir("/tmp/fragdir1/", true),
fileWrite("/tmp/fragdir1/f1", f1),
fileWrite("/tmp/fragdir1/f2", f2),
fileWrite("/tmp/fragdir1/f3", f3),
fileMkdir("/tmp/fragdir2/", true),
fileWrite("/tmp/fragdir2/f1", f1d2),
fileWrite("/tmp/fragdir2/f2", f2d2),
fileWrite("/tmp/fragdir2/f3", f3d2),
fileWrite(p, "whatever"),
resValidate(r1),
resInit(r1),
resCheckApply(r1, false), // changed
fileExpect(p, content),
resCheckApply(r1, true), // it's already good
resClose(r1),
fileExpect(p, content), // ensure it exists
}
testCases = append(testCases, test{
name: "file fragments",
timeline: timeline,
expect: func() error { return nil },
startup: func() error { return nil },
cleanup: func() error { return os.Remove(p) },
})
}
{
//file "/tmp/somefile" {
// state => $const.res.file.state.exists,
// source => "/tmp/somefiletocopy",
//}
r1 := makeRes("file", "r1")
res := r1.(*FileRes) // if this panics, the test will panic
p := "/tmp/somefile"
p2 := "/tmp/somefiletocopy"
content := "hello this is some file to copy\n"
res.Path = p
res.State = FileStateExists
res.Source = p2
timeline := []func() error{
fileAbsent(p), // ensure it's absent
fileWrite(p2, content),
resValidate(r1),
resInit(r1),
resCheckApply(r1, false), // changed
fileExpect(p, content), // should be created like this
fileExpect(p2, content), // should not change
resCheckApply(r1, true), // it's already good
fileExpect(p, content), // should already be like this
fileExpect(p2, content), // should not change either
resClose(r1),
}
testCases = append(testCases, test{
name: "copy file with source",
timeline: timeline,
expect: func() error { return nil },
startup: func() error { return nil },
cleanup: func() error { return os.Remove(p) },
})
}
{
//file "/tmp/somedir/" {
// state => $const.res.file.state.exists,
//}
r1 := makeRes("file", "r1")
res := r1.(*FileRes) // if this panics, the test will panic
p := "/tmp/somedir/"
res.Path = p
res.State = FileStateExists
timeline := []func() error{
fileAbsent(p), // ensure it's absent
resValidate(r1),
resInit(r1),
resCheckApply(r1, false), // changed
fileExists(p, true), // ensure it's a dir
resCheckApply(r1, true), // it's already good
fileExists(p, true), // ensure it's a dir
resClose(r1),
}
testCases = append(testCases, test{
name: "make empty directory",
timeline: timeline,
expect: func() error { return nil },
startup: func() error { return nil },
cleanup: func() error { return os.RemoveAll(p) },
})
}
{
//file "/tmp/somedir/" {
// state => $const.res.file.state.exists,
// source => /tmp/somedirtocopy/,
// recurse => true,
//}
r1 := makeRes("file", "r1")
res := r1.(*FileRes) // if this panics, the test will panic
p := "/tmp/somedir/"
p2 := "/tmp/somedirtocopy/"
res.Path = p
res.State = FileStateExists
res.Source = p2
res.Recurse = true
f1 := path.Join(p, "f1")
f2 := path.Join(p, "f2")
d1 := path.Join(p, "d1/")
d2 := path.Join(p, "d2/")
d1f1 := path.Join(p, "d1/f1")
d1f2 := path.Join(p, "d1/f2")
d2f1 := path.Join(p, "d2/f1")
d2f2 := path.Join(p, "d2/f2")
d2f3 := path.Join(p, "d2/f3")
xf1 := path.Join(p2, "f1")
xf2 := path.Join(p2, "f2")
xd1 := path.Join(p2, "d1/")
xd2 := path.Join(p2, "d2/")
xd1f1 := path.Join(p2, "d1/f1")
xd1f2 := path.Join(p2, "d1/f2")
xd2f1 := path.Join(p2, "d2/f1")
xd2f2 := path.Join(p2, "d2/f2")
xd2f3 := path.Join(p2, "d2/f3")
timeline := []func() error{
fileMkdir(p2, true),
fileWrite(xf1, "f1\n"),
fileWrite(xf2, "f2\n"),
fileMkdir(xd1, true),
fileMkdir(xd2, true),
fileWrite(xd1f1, "d1f1\n"),
fileWrite(xd1f2, "d1f2\n"),
fileWrite(xd2f1, "d2f1\n"),
fileWrite(xd2f2, "d2f2\n"),
fileWrite(xd2f3, "d2f3\n"),
resValidate(r1),
resInit(r1),
resCheckApply(r1, false), // changed
fileExists(p, true), // ensure it's a dir
fileExists(f1, false), // ensure it's a file
fileExists(f2, false),
fileExists(d1, true), // ensure it's a dir
fileExists(d2, true),
fileExists(d1f1, false),
fileExists(d1f2, false),
fileExists(d2f1, false),
fileExists(d2f2, false),
fileExists(d2f3, false),
resCheckApply(r1, true), // it's already good
resClose(r1),
}
testCases = append(testCases, test{
name: "source dir copy",
timeline: timeline,
expect: func() error { return nil },
startup: func() error { return nil },
cleanup: func() error { return os.RemoveAll(p) },
})
}
{
//file "/tmp/somedir/" {
// state => $const.res.file.state.exists,
// recurse => true,
// purge => true,
//}
r1 := makeRes("file", "r1")
res := r1.(*FileRes) // if this panics, the test will panic
p := "/tmp/somedir/"
res.Path = p
res.State = FileStateExists
res.Recurse = true
res.Purge = true
f1 := path.Join(p, "f1")
f2 := path.Join(p, "f2")
d1 := path.Join(p, "d1/")
d2 := path.Join(p, "d2/")
d1f1 := path.Join(p, "d1/f1")
d1f2 := path.Join(p, "d1/f2")
d2f1 := path.Join(p, "d2/f1")
d2f2 := path.Join(p, "d2/f2")
d2f3 := path.Join(p, "d2/f3")
graph, err := pgraph.NewGraph("test")
if err != nil {
panic("can't make graph")
}
graph.AddVertex(res) // add self
timeline := []func() error{
fileMkdir(p, true),
fileWrite(f1, "f1\n"),
fileWrite(f2, "f2\n"),
fileMkdir(d1, true),
fileMkdir(d2, true),
fileWrite(d1f1, "d1f1\n"),
fileWrite(d1f2, "d1f2\n"),
fileWrite(d2f1, "d2f1\n"),
fileWrite(d2f2, "d2f2\n"),
fileWrite(d2f3, "d2f3\n"),
resValidate(r1),
resInit(r1, addGraph(graph)),
resCheckApply(r1, false), // changed
fileExists(p, true), // ensure it's a dir
fileAbsent(f1), // ensure it's absent
fileAbsent(f2),
fileAbsent(d1),
fileAbsent(d2),
fileAbsent(d1f1),
fileAbsent(d1f2),
fileAbsent(d2f1),
fileAbsent(d2f2),
fileAbsent(d2f3),
resCheckApply(r1, true), // it's already good
resClose(r1),
}
testCases = append(testCases, test{
name: "dir purge",
timeline: timeline,
expect: func() error { return nil },
startup: func() error { return nil },
cleanup: func() error { return os.RemoveAll(p) },
})
}
{
//file "/tmp/somedir/" {
// state => $const.res.file.state.exists,
// recurse => true,
// purge => true,
//}
// TODO: should State be required for these to not delete them?
//file "/tmp/somedir/hello" {
//}
//file "/tmp/somedir/nested-dir/" {
//}
//file "/tmp/somedir/nested-dir/nestedfileindir" {
//}
r1 := makeRes("file", "r1")
res := r1.(*FileRes) // if this panics, the test will panic
p := "/tmp/somedir/"
res.Path = p
res.State = FileStateExists
res.Recurse = true
res.Purge = true
f1 := path.Join(p, "f1")
f2 := path.Join(p, "f2")
d1 := path.Join(p, "d1/")
d2 := path.Join(p, "d2/")
d1f1 := path.Join(p, "d1/f1")
d1f2 := path.Join(p, "d1/f2")
d2f1 := path.Join(p, "d2/f1")
d2f2 := path.Join(p, "d2/f2")
d2f3 := path.Join(p, "d2/f3")
r2 := makeRes("file", "r2")
res2 := r2.(*FileRes)
p2 := path.Join(p, "hello")
res2.Path = p2
p2c := "i am a hello file\n"
// TODO: should State be required for this to not delete it?
r3 := makeRes("file", "r3")
res3 := r3.(*FileRes)
p3 := path.Join(p, "nested-dir/")
res3.Path = p3
// TODO: should State be required for this to not delete it?
r4 := makeRes("file", "r4")
res4 := r4.(*FileRes)
p4 := path.Join(p3, "nestedfileindir")
res4.Path = p4
p4c := "i am a nested file\n"
// TODO: should State be required for this to not delete it?
graph, err := pgraph.NewGraph("test")
if err != nil {
panic("can't make graph")
}
graph.AddVertex(res, res2, res3, res4)
timeline := []func() error{
fileMkdir(p, true),
fileWrite(f1, "f1\n"),
fileWrite(f2, "f2\n"),
fileMkdir(d1, true),
fileMkdir(d2, true),
fileWrite(d1f1, "d1f1\n"),
fileWrite(d1f2, "d1f2\n"),
fileWrite(d2f1, "d2f1\n"),
fileWrite(d2f2, "d2f2\n"),
fileWrite(d2f3, "d2f3\n"),
fileWrite(p2, p2c),
fileMkdir(p3, true),
fileWrite(p4, p4c),
resValidate(r2),
resInit(r2),
//resCheckApply(r2, false), // not really needed in test
resClose(r2),
resValidate(r3),
resInit(r3),
//resCheckApply(r3, false), // not really needed in test
resClose(r3),
resValidate(r4),
resInit(r4),
//resCheckApply(r4, false), // not really needed in test
resClose(r4),
resValidate(r1),
resInit(r1, addGraph(graph), addLogf(nil)), // show the full graph
resCheckApply(r1, false), // changed
fileExists(p, true), // ensure it's a dir
fileAbsent(f1), // ensure it's absent
fileAbsent(f2),
fileAbsent(d1),
fileAbsent(d2),
fileAbsent(d1f1),
fileAbsent(d1f2),
fileAbsent(d2f1),
fileAbsent(d2f2),
fileAbsent(d2f3),
fileExists(p2, false), // ensure it's a file XXX !!!
fileExists(p3, true), // ensure it's a dir
fileExists(p4, false),
resCheckApply(r1, true), // it's already good
resClose(r1),
}
testCases = append(testCases, test{
name: "dir purge with others inside",
timeline: timeline,
expect: func() error { return nil },
startup: func() error { return nil },
cleanup: func() error { return os.RemoveAll(p) },
})
}
names := []string{}
for index, tc := range testCases { // run all the tests
if tc.name == "" {
@@ -1144,12 +1706,17 @@ func TestResources2(t *testing.T) {
// t.Logf(fmt.Sprintf("test #%d: ", index)+format, v...)
//}
t.Logf("test #%d: running startup()", index)
if err := startup(); err != nil {
t.Errorf("test #%d: FAIL", index)
t.Errorf("test #%d: could not startup: %+v", index, err)
if startup != nil {
t.Logf("test #%d: running startup()", index)
if err := startup(); err != nil {
t.Errorf("test #%d: FAIL", index)
t.Errorf("test #%d: could not startup: %+v", index, err)
}
}
defer func() {
if cleanup == nil {
return
}
t.Logf("test #%d: running cleanup()", index)
if err := cleanup(); err != nil {
t.Errorf("test #%d: FAIL", index)

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2021+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
@@ -419,7 +419,8 @@ func (obj *SvcResAutoEdges) Next() []engine.ResUID {
return []engine.ResUID{value} // we return one, even though api supports N
}
// Test gets results of the earlier Next() call, & returns if we should continue!
// Test gets results of the earlier Next() call, & returns if we should
// continue!
func (obj *SvcResAutoEdges) Test(input []bool) bool {
// if there aren't any more remaining
if len(obj.data) <= obj.pointer {
@@ -513,8 +514,8 @@ func (obj *SvcRes) AutoEdges() (engine.AutoEdge, error) {
return engineUtil.AutoEdgeCombiner(fileEdge, cronEdge)
}
// UIDs includes all params to make a unique identification of this object.
// Most resources only return one, although some resources can return multiple.
// 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 *SvcRes) UIDs() []engine.ResUID {
x := &SvcUID{
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
@@ -536,8 +537,8 @@ func (obj *SvcRes) UIDs() []engine.ResUID {
// return fmt.Errorf("not possible at the moment")
//}
// UnmarshalYAML is the custom unmarshal handler for this struct.
// It is primarily useful for setting the defaults.
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
// primarily useful for setting the defaults.
func (obj *SvcRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
type rawRes SvcRes // indirection to avoid infinite recursion

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2021+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
@@ -367,8 +367,8 @@ type TestUID struct {
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.
// 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() []engine.ResUID {
x := &TestUID{
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
@@ -405,8 +405,8 @@ func (obj *TestRes) Sends() interface{} {
}
}
// UnmarshalYAML is the custom unmarshal handler for this struct.
// It is primarily useful for setting the defaults.
// 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

Some files were not shown because too many files have changed in this diff Show More