319 Commits

Author SHA1 Message Date
James Shubin
c5efe7a17b lang, engine: Remove unneeded error wrapping
These situations basically never fail, and if they do, we certainly
don't need more context. This simplifies things a bit.
2023-04-20 18:02:40 -04:00
James Shubin
7075b8b973 pgraph: Add LookupEdge function
The new LookupEdge function lets you find which vertices are associated
with a particular edge, if any.
2023-04-20 15:45:46 -04:00
James Shubin
3f5957d30e docs: readme: Add new talk and clarify README
This should make it more obvious how to reach this content as it might
not have been particularly clear to new users where to find this
information.
2023-04-07 11:51:43 -04:00
James Shubin
bc29957d1e lang: Add a useful debug message to tests
When running this test a lot in series, it helps to have more obvious
debug output.
2023-03-31 19:29:52 -04:00
James Shubin
289835039a lang: Remove SetValue from the engine
This removes the calling of SetValue from the engine, and instead
replaces it with the Table() API. The downside is that this is likely
slower, and the current API with locking being exposed publicly is kind
of ugly. The upside is that this might make building the new engine
easier.

Future versions might remove locking from the API if we can avoid making
any accesses to expressions. Currently this happens within Logf/SafeLogf
which is our main (only?) usage at the moment. Logging could become
smarter perhaps. Alternatively, we might pass in a "setter" function
that gets called safely from within the engine. This could wrap SetValue
and the locking functions wouldn't be part of the public API.
2023-03-31 19:29:52 -04:00
James Shubin
b1e08ef231 util: deadline: Add a simple test for deadline and testing
This is a simple test/example placeholder if we want to remember how to
run deadlines with contexts for testing.
2023-03-24 16:53:42 -04:00
James Shubin
8a463767bf lang: funcs: Make printable function names more unique
This helps a lot with debugging.
2023-03-24 16:50:01 -04:00
James Shubin
c598e4d289 engine, etcd: Update code for latest gofmt fixes
Latest version of golang broken gofmt again...
2023-03-14 16:43:08 -04:00
James Shubin
a7624a2bf9 legal: Happy 2023 everyone...
Done with:

ack '2022+' -l | xargs sed -i -e 's/2022+/2023+/g'

Checked manually with:

git add -p

Hello to future James from 2024, and Happy Hacking!
2023-03-05 18:31:52 -05:00
James Shubin
d20fcbd845 lang: Fixup tests that broke
I should not have changed the error string when I unified the pgraph
library.
2023-03-03 14:47:30 -05:00
James Shubin
5d664855de lang: interfaces, funcs: Implement fmt.Stringer for functions
This adds the requirement that all function implementations provider a
String() string method so that these can be used as vertices in the
pgraph library. If we eventually move to generics for the pgraph DAG,
then this might not matter, but it's not bad that these have names
either.
2023-03-03 14:12:09 -05:00
James Shubin
8366cf0873 pgraph: Add a named error for detected cycles
This makes it useful to find when a topological sort failed.
2023-02-25 23:20:05 -05:00
James Shubin
a41789a746 examples, lang: funcs: core: example: Update vumeter
This patch moves to use the sox package instead of arecord for getting
microphone data, and it also validates that both sox and rec and
installed. We also add a standalone example.
2023-02-08 12:14:11 -05:00
James Shubin
cde3251dd8 test: shell: Disable flaky network test 2022-11-11 22:42:29 -05:00
James Shubin
7c394bf735 lang: Move the Edge struct into the interfaces package
This makes it consumable from more than one package and avoids future
cycles.
2022-11-11 20:28:22 -05:00
James Shubin
76e0345609 pgraph: Make some functions variadic for consistency
This will make it more useful when consuming these functions in a Txn
API which might be more convenient as a big one-liner.
2022-11-11 20:20:14 -05:00
James Shubin
d8820fa185 make: Improve crossbuild docs 2022-10-24 02:38:38 -04:00
James Shubin
b6502693e4 make, golang: Work around new go.mod issues
This updates the versions of virtually all our deps. Ever since golang
switched to go.mod we were unable to do so due to blockages with the
retract mechanism that one dep had used. Here's what seems like a
workaround for now. Suggestions from expert welcome.
2022-10-17 19:36:12 -04:00
James Shubin
f7e5402966 make: Remove missing clean target
This is no longer present.
2022-09-12 14:27:39 -04:00
James Shubin
1e6a825412 lang: funcs: Add cancel methods in a different way
Previously go vet found:
"this return statement may be reached without using the cancel var
defined on line..."
2022-09-12 14:11:25 -04:00
Samuel Gélineau
c23065aacd lang: funcs: Add SystemFunc
Runs a string as a shell command, then produces each line from stdout.
2022-09-11 21:58:39 -04:00
James Shubin
04f5ba67a2 lang: Small build fixes 2022-09-11 21:43:57 -04:00
James Shubin
b87fa6715b misc: Exclude more directories from ack matching
This is almost always what we want.
2022-09-11 20:55:37 -04:00
James Shubin
f6f3298e03 misc: Use go install instead of go get for build binaries 2022-09-11 20:55:37 -04:00
James Shubin
6bfd781947 lang: Replace the go-bindata usage with embed
This doesn't let us have nested mcl at the moment, but we could improve
on this with an embed API for each package. For now this makes building
the project easier.
2022-09-11 20:55:37 -04:00
James Shubin
aff6331211 misc, docs: Move to golang 1.18
Hopefully this fixes some yucky things.
2022-09-11 20:55:37 -04:00
James Shubin
d547c39a16 misc, docs: Move to golang 1.17
There are a few small source fixes and other tidbits to move past 1.16,
but we can do those later.
2022-08-16 19:30:20 -04:00
James Shubin
3cea422365 legal: Happy 2022 everyone...
Done with:

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

Checked manually with:

git add -p

Hello to future James from 2023, and Happy Hacking!
2022-08-05 23:06:27 -04:00
James Shubin
ac39606386 lang: Misc changes from an old feature branch 2022-08-04 14:49:24 -04:00
James Shubin
12ae44d563 lang: funcs: core: fmt: Add important comment to printf 2022-08-04 14:36:56 -04:00
James Shubin
57b37d9005 pgraph: Misc cleanups and additions that I had kicking around
These changes had been sitting in a feature branch for a while. Push
them up.
2022-08-04 14:36:56 -04:00
James Shubin
9d5cc07567 lang: funcs: core: iter: Add map iterator function part3
This flattens the type unification of the map function so that the
solver has more to work with. It's possible that some scenarios might
solve faster, or without recursion, after this improvement.
2022-08-04 14:24:38 -04:00
James Shubin
75d4d767c6 lang: funcs: core: iter: Add map iterator function part2
This adds the Unify method to our map function and also switches the arg
order because I decided it would look nicer. Completely untested.
2022-08-04 14:24:05 -04:00
James Shubin
0be4b86230 lang: funcs: core: iter: Add map iterator function
Sadly this doesn't all work yet, but the tests and xmap function are
approximately correct. Eventually we add filter and reduce too!
2022-08-04 14:23:16 -04:00
James Shubin
784d15b012 all: Misc housekeeping for new golang versions 2022-08-04 14:16:33 -04:00
James Shubin
00f6045b12 util: safepath: Add a new Dir method and tests
This adds a new helper method. It should be considered for other types
as well.
2022-05-10 23:44:48 -04:00
Jef Masereel
b26f842de1 engine: resources: Add hetzner:vm resource
Hetzner cloud resource using hcloud-go. Requires polling via Meta:poll param. This first commit provides a stable vm resource with support for the basic functions of creating, deleting and updating a live server instance. SSH key handling does still require some attention to make sure checkapply can detect and update live changes to the specified keylist. A dedicated hetzner:sshkeys resource might be in order to make sure the keyset is handled correctly if there are multiple hetzner:vm resources running under the same Hetzner project. All remarks for future improvements are indicated with a TODO prefix
2022-04-20 14:24:25 -04:00
James Shubin
0ab2406db9 engine: Pass through the program version
We forgot this in a few places.
2022-02-17 13:34:26 -05:00
Joe Groocock
bf7e45439b engine: util: Skip unexported fields in struct field mapping
Unexported fields should be ignored when mapping structs from Golang to
mcl, as this avoids issues where certain structs cannot be used in mcl
representation due to their structure that may be incompatible with
conversion. Disallowing private fields allows those to still be included
in the struct but ignored when mapping the types, allowing the mcl
representation of the type to stil be valid

Resolves: https://github.com/purpleidea/mgmt/issues/684
Signed-off-by: Joe Groocock <me@frebib.net>
2021-12-01 22:06:06 +00:00
dantefromhell
0652273fe1 misc: Add systemd service file to release packages
According to [1] "/usr/lib/systemd/system/" is where systemd files from installed packages are stored in Arch Linux

Big thanks to @jordansissel for the in-depth explanation of this PRs effects:
> This PR replaces --prefix with the fpm dir package type's feature "path mapping" which lets you map a local file to a destination file when creating the package using this syntax: localpath=destinationpath

[1] https://wiki.archlinux.org/title/Systemd#Writing_unit_files
2021-11-09 21:42:50 +00:00
James Shubin
5927a54208 docs: Improve autogenerate godoc
There were a bunch of packages that weren't well documented. With the
recent split up of the lang package, I figured it would be more helpful
for new contributors who want to learn the structure of the project.
2021-10-26 00:12:18 -04:00
James Shubin
b46db59948 readme: Update to new godoc link
Thanks to Drew DeVault for hosting this.
2021-10-22 17:44:08 -04:00
James Shubin
23b5a4729f lang: Split lang package out into many subpackages
This is a giant refactor to split the giant lang package into many
subpackages. The most difficult piece was figuring out how to extract
the extra ast structs into their own package, because they needed to
call two functions which also needed to import the ast.

The solution was to separate out those functions into their own
packages, and to pass them into the ast at the root when they're needed,
and to let the relevant ast portions call a handle.

This isn't terribly ugly because we already had a giant data struct
woven through the ast.

The bad part is rebasing any WIP work on top of this.
2021-10-21 04:13:04 -04:00
James Shubin
8ae47bd490 test: Disable flaky shell tests
We need to improve and add these things back eventually, but for now I
don't want intermittent failures.
2021-10-21 01:17:52 -04:00
James Shubin
1796d20399 lang: Split the download code out into its own package
More cleanups.
2021-10-21 00:05:37 -04:00
James Shubin
5ac2447b85 lang: Split out the GAPI code for the lang frontend
This can go in a separate package.
2021-10-21 00:03:07 -04:00
James Shubin
db445c3a8e lang: Move the inputs logic into a separate package 2021-10-20 18:13:46 -04:00
James Shubin
de2914978d lang: Move interpret function to a separate package 2021-10-20 17:50:21 -04:00
James Shubin
09812a7bfc lang: Rename prog struct field to body
This is more consistent and avoids the future prog.prog stuttering.
2021-10-20 16:24:14 -04:00
James Shubin
2eb3b541f4 lang: Fix typo 2021-10-20 15:32:43 -04:00
James Shubin
e9791ff92c lang: funcs: core: fmt: Add variant verb for printf
There's no reason we can't support a %v variant verb. Of course it makes
type unification more difficult, and certain uses of this will produce
unsolvable situations, but it's useful for debugging, and fun to have.
2021-10-11 00:36:29 -04:00
James Shubin
88516546fa lib: Move to go modules and shed a few tears
The old system with vendor/ and git submodules worked great,
unfortunately FUD around git submodules seemed to scare people away and
golang moved to a go.mod system that adds a new lock file format instead
of using the built-in git version. It's now almost impossible to use
modern golang without this, so we've switched.

So much for the golang compatibility promise-- turns out it doesn't
apply to the useful parts that I actually care about like this.

Thanks to frebib for his incredibly valuable contributions to this
patch. This snide commit message is mine alone.

This patch also mixes in some changes due to legacy golang as we've also
bumped the minimum version to 1.16 in the docs and tests.

Lastly, we had to disable some tests and fix up a few other misc things
to get this passing. We've definitely hot bugs in the go.mod system, and
our Makefile tries to workaround those.
2021-10-05 08:34:51 -04:00
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
James Shubin
6a7d904fae misc: Improve tagging script
This way we can push the tag *after* all the builds succeed. If
something goes wrong, we can always delete our local tag and try again.
2019-10-04 06:49:04 -04:00
James Shubin
d4043d3f86 misc, make: Add full file path into fpm script
This is needed for our fancier, unique file names.
2019-10-04 06:43:17 -04:00
James Shubin
b4902a4f58 make: Add a unique token to the package file name
This unique token is necessary so that storing the files in the same dir
(basically a GitHub release) or in the SHA1SUMS file doesn't cause a
conflict.
2019-10-04 06:06:44 -04:00
James Shubin
ffe402f201 misc: Add fedora-30 mkosi+fpm build environment
Good example of how to add a new distro or version.
2019-10-04 06:02:08 -04:00
James Shubin
09cc7da282 misc: Add proper archlinux prefix in build script 2019-10-04 06:01:23 -04:00
James Shubin
2d2dad41f4 todo: Update the TODO file so that it has a sane purpose
We stored some stuff in GitHub, and some stuff here. We can keep using
this, but let's do it for the stuff that hasn't changed in a while.
2019-10-04 04:11:26 -04:00
bjanssens
5f7c0a86dd art: Add the requested art
Signed-off-by: bjanssens <bjanssens@inuits.eu>
2019-10-04 09:23:20 +02:00
Donald Bakong
fc1c631c98 engine: resources: Change Res API from Compare to Cmp
This will be done by refactoring the current method, to return an error
message instead of a boolean value. This will also update a typo on the
user res.
2019-09-27 18:10:58 -04:00
James Shubin
89bdafacb8 misc: Refactor Makefile slightly
We could make this even better in the future with lists.
2019-09-23 06:57:26 -04:00
James Shubin
73b6b3f129 misc: Remove old image building cruft 2019-09-23 06:50:26 -04:00
James Shubin
b2a495f593 misc: Add mkosi target for ubuntu bionic
The name of these is pretty weird.
2019-09-23 06:50:00 -04:00
James Shubin
65ee904377 misc: Work around old golang in ubuntu
Hopefully this helps.
2019-09-23 06:48:55 -04:00
James Shubin
13f59230b5 misc: Split Makefile PHONY target into multiple lines
AIUI this is valid make. Please correct me if I'm wrong.
2019-09-23 06:48:55 -04:00
James Shubin
36d2a0de1e misc: Make mkosi building suitable for different distro versions
We'd like to be able to build both Fedora N and N-1 at the same time if
possible. This makes it more generally applicable for this scenario, as
well as for other distros.
2019-09-23 06:48:55 -04:00
James Shubin
a4db9fc8e5 misc: Add mkosi based package building with fpm
Building distro packages is great, however if they aren't built in the
correct environment with the associated dependencies, then they won't
work properly on those distros.

This patch adds an `mkosi` based image building environment that builds
the packages in their respective distros, and then copies them out into
our releases directory.

You'll now want to `make tag && make mkosi && make release` to get a new
release out. We use a small hack to trick the `make release` portion to
not re-build the distro packages if they're already present in the
releases/ directory for that version.

This commit depends on a very recent version of mkosi (it was tested
with git master) and also depends on two currently unmerged patches:
https://github.com/systemd/mkosi/pull/363 and
https://github.com/systemd/mkosi/pull/365
2019-09-20 12:32:41 -04:00
James Shubin
9dae5ef83b engine: resources: Improve the file res and add strict state
This might be slightly controversial, in that you must specify the state
if a file would need to be created to perform the action. We no longer
implicitly assume that just specifying content is enough. As it turns
out, I believe this is safer and more correct. The code to implement
this turns out to be much more logical and simplified, and does this
removes an ambiguous corner case from the reversed resource code.

Some discussion in: https://github.com/purpleidea/mgmt/issues/540

This patch also does a bit of related cleanup.
2019-09-14 16:07:53 -04:00
James Shubin
e8842a740c lang: Remove duplicate log message
Looks like we had two copies of the same message by accident.
2019-09-11 04:26:15 -04:00
James Shubin
0d3807ad09 lang, test: Fix copy paste error with log message
This changes this to the correct error message.
2019-09-11 04:26:15 -04:00
James Shubin
5c27a249b7 engine: resources: Add reversible API and file resource
This adds the first reversible resource (file) and the necessary engine
API hooks to make it all work. This allows a special "reversed" resource
to be added to the subsequent graph in the stream when an earlier
version "disappears". This disappearance can happen if it was previously
in an if statement that then becomes false.

It might be wise to combine the use of this meta parameter with the use
of the `realize` meta parameter to ensure that your reversed resource
actually runs at least once, if there's a chance that it might be gone
for a while.

This patch also adds a new test harness for testing resources. It
doesn't test the "live" aspect of resources, as it doesn't run Watch,
but it was designed to ensure CheckApply works as intended, and it runs
very quickly with a simplified timeline of happenings.
2019-09-11 03:40:22 -04:00
James Shubin
7e41860b28 docs: Add missing docs on the rewatch and realize meta params
Sometimes it's hard to keep this in sync.
2019-09-11 03:40:22 -04:00
James Shubin
43ff92bbe7 engine: resources: Clean up test log message 2019-09-11 03:40:22 -04:00
James Shubin
28adc7e563 engine: resource: Refactor helper functions
Maybe we can use them in other tests too.
2019-09-11 03:40:22 -04:00
James Shubin
9788411995 engine: resources: Add another validation check
This simple check should prevent some silly mistakes and make the logic
easier for other parts of the code that won't have to worry about this
pattern.
2019-09-11 03:40:22 -04:00
James Shubin
0c9e8cc50e engine: resources: Change the default file state
The default file state should be undefined. This is important because if
a reverse scenario that doesn't specify the state gets given this
default, it will be as if it was specified explicitly, which wouldn't
necessarily be what we want. Instead, an undefined state should
implicitly cause a file to get created if there's a reason to do so,
such as if content or another attribute is specified.

Hopefully this change doesn't introduce any bugs in the CheckApply code,
if it does, then it was due to a lack of implicit file creation.
2019-09-11 03:16:57 -04:00
James Shubin
34d572c523 engine: Improve the way we make a unique res path token
This is needed in the state directory.
2019-09-11 03:16:57 -04:00
James Shubin
011b496b3f engine: resources: Ensure the Kind and Name methods work
Triple check these work after decoding, by adding a test.
2019-09-11 03:16:57 -04:00
James Shubin
12b906eac6 engine: Refactor state dir into a separate function
This lets us re-use it, and know the path is fixed.
2019-09-11 03:16:57 -04:00
James Shubin
20937d05c3 engine: resources: file: Add undefined file state and validate it
We should consider using *string instead of the empty string, but let's
keep the diff smaller for now.
2019-09-11 03:16:57 -04:00
James Shubin
4943d37ccf engine: resources: file: Use constants for state values
More robustness is yay!
2019-09-11 03:16:57 -04:00
James Shubin
3a8fd215de engine: resources: file: Add Copy method to file res
This lets us implement the CopyableRes interface.
2019-09-11 03:16:57 -04:00
James Shubin
87572e8922 test: Catch capitalized error messages in tests 2019-09-06 03:28:49 -04:00
James Shubin
f1eedc7a01 lang: Clarify error message about missing field
User probably just mistyped a field name. Make that clear.
2019-09-06 03:28:49 -04:00
Donald Bakong
b79e48dd77 docs: Fix typo on quick-start-guide.md 2019-08-25 22:53:43 -04:00
James Shubin
18872194af misc: Warn users with weird computers
A user seemed to experience a weird golang issue when they had deps from
both package managers installed. I won't block or fail their install,
but we can print a warning message so that someone sees it in their
logs.
2019-08-23 22:10:34 -04:00
James Shubin
bafd7ba282 misc: Use apt install of apt-get where possible
The future is now!
2019-08-23 22:10:12 -04:00
Donald Bakong
b186481181 pgraph: Add a test for FindEdge() function 2019-08-08 00:43:26 -04:00
James Shubin
09ca6d11ad lang: funcs: Module name should be public
For consistency with the rest of the core functions.
2019-07-29 11:17:43 -04:00
James Shubin
e68e4e786d docs: Add newly recorded talks and blog post 2019-07-26 06:52:01 -04:00
James Shubin
ee638254c3 lang: Remove the specialized info structs
Since this was an early form of the modern data struct, remove those and
pass in the correct data. This is also important in case we have
something more complex inside our string interpolation!
2019-07-26 04:20:04 -04:00
James Shubin
1e678905c4 util: Fix typo 2019-07-26 04:20:04 -04:00
James Shubin
10804c4b25 lang: Improve the gapi copying
We hit a weird bug where dirs would not get copied properly. I thought
the solution might be to add the missing dirs so they'd get a proper
mkdir, but in the end that didn't work well, so we just use `mkdirall`
and that seems to work. Let's leave it like this for now. Some of the
previous work for that is in the previous commit.
2019-07-26 04:20:04 -04:00
James Shubin
4bf9b4d41b util: Add some path helper functions
In the end, I'm not sure how useful these will be, but we'll keep them
in for now.
2019-07-26 04:20:04 -04:00
James Shubin
1161872324 etcd: fs: Errors should start with lower case 2019-07-26 04:20:04 -04:00
James Shubin
98cb570896 util: Add new mkdirall variants for the copy functions
This adds versions that recursively `mkdir` and all don't error as
easily. This works around some bugs we were having with file copying.
2019-07-26 04:20:04 -04:00
James Shubin
ed4ee3b58e lang: funcs: Add deploy package with readfile related functions
This adds a readfile function to actually access files from our deploy.
A fun side effect is that we can even access our own code! In general,
it's a good reminder that you should only run trusted code on your own
infrastructure. This also includes a fancy new test case.
2019-07-26 03:38:26 -04:00
James Shubin
066048f4de lang: Pass through the Fs and the FsURI
This should give us options as to how a function should interact with an
FS. I feel like it's cleaner to go through the World API, and passing in
the FsURI lets us do that, but I passed in the Fs at the same time in
case it's useful for some reason. I think using it is a boundary
violation, but it's just a hunch. Does anything break when we move from
one deploy to the next?
2019-07-26 03:07:08 -04:00
James Shubin
4b6b91c08b lang: Make sure to call Init for functions that arrive via import
We weren't calling Init on some functions which should have had this
done. I'm not sure whether this is the right place, or if it should be
elsewhere as part of the scope building process. Good enough for now.
2019-07-22 06:49:02 -04:00
James Shubin
2980523a5b lang: Add a new function interface to accept data
Sometimes certain internal functions might want to get some data from
the AST or from something relating to the state of the language. This
adds a method to pass in that data. For now it's a very simple method,
but we could generalize it in the future if it becomes more useful.
2019-07-22 06:46:04 -04:00
James Shubin
f2f9c043bf lang, gapi: Work around a copy bug in the deploy
It seems when we had a files/ dir that we added to our deploy, it would
get copied into /files/files/whatever instead of /files/whatever where
it should be. Hopefully this works around the issue forever.
2019-07-22 06:40:47 -04:00
James Shubin
5d59cfd2c9 util: Ensure the afero copy function is working as intended
The destination should be a dir sometimes.
2019-07-22 06:38:02 -04:00
James Shubin
f94474e24f lang: Add the world implementation to our test suite
This allows our tests to actual run the World API in them.
2019-07-22 06:36:37 -04:00
James Shubin
a63fc6d9ba util: Add a remove path suffix util function
This pairs with a similar one we already had.
2019-07-22 06:35:13 -04:00
James Shubin
076adeef80 lang: funcs: Fix a copypasta error with the not equals operator
Woops, sorry!
2019-07-22 06:08:37 -04:00
James Shubin
a0e756317c lang: Add tests for slow unification
These used to be cases where our algorithm was unusably slow.

Thanks to foxxx0 for the report!
2019-07-21 03:15:06 -04:00
James Shubin
252cb5f2f3 lang: Detect windows style CR and return a better error
If you get a sneaky \r in your code, the error just looks like
whitespace, so this way we can warn you explicitly.
2019-07-21 03:10:21 -04:00
James Shubin
64288b4914 lang, test: Inline some overly indented tests
Sometimes you're busy hacking and it's nice for future you to fix up
your code!
2019-07-21 01:19:15 -04:00
James Shubin
9ca6c6a315 test: Split up long tests into multiple sub tests again
I think we need this for non --race tests too.
2019-07-21 00:55:36 -04:00
James Shubin
3651ab5c0c lang: Add more tests for function 2019-07-20 22:27:21 -04:00
James Shubin
b3f15e1ddc lang: Add more tests for class and include 2019-07-20 01:33:42 -04:00
685 changed files with 28927 additions and 5762 deletions

2
.ackrc
View File

@@ -1,3 +1,5 @@
--ignore-dir=old/
--ignore-dir=tmp/
--ignore-dir=vendor/
--ignore-dir=releases/
--ignore-dir=rpmbuild/

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

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

@@ -0,0 +1,68 @@
# 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.19.x and tip
# minimum required and latest published go_version
- 1.18
test_block:
- basic
- shell
- race
#fail-fast: false
steps:
# Do not shallow fetch. 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

3
.gitignore vendored
View File

@@ -7,7 +7,6 @@ old/
tmp/
*WIP
*_stringer.go
bindata/*.go
mgmt
mgmt.static
# crossbuild artifacts
@@ -17,3 +16,5 @@ rpmbuild/
releases/
# vim swap files
.*.sw[op]
# prevent `echo foo 2>1` typo errors by making this file read-only
1

33
.gitmodules vendored
View File

@@ -1,33 +0,0 @@
[submodule "vendor/github.com/coreos/etcd"]
path = vendor/github.com/coreos/etcd
url = https://github.com/coreos/etcd/
[submodule "vendor/google.golang.org/grpc"]
path = vendor/google.golang.org/grpc
url = https://github.com/grpc/grpc-go
[submodule "vendor/github.com/grpc-ecosystem/grpc-gateway"]
path = vendor/github.com/grpc-ecosystem/grpc-gateway
url = https://github.com/grpc-ecosystem/grpc-gateway
[submodule "vendor/gopkg.in/fsnotify.v1"]
path = vendor/gopkg.in/fsnotify.v1
url = https://gopkg.in/fsnotify.v1
[submodule "vendor/github.com/purpleidea/go-systemd"]
path = vendor/github.com/purpleidea/go-systemd
url = https://github.com/purpleidea/go-systemd
[submodule "vendor/honnef.co/go/augeas"]
path = vendor/honnef.co/go/augeas
url = https://github.com/dominikh/go-augeas/
[submodule "vendor/github.com/grpc-ecosystem/go-grpc-prometheus"]
path = vendor/github.com/grpc-ecosystem/go-grpc-prometheus
url = https://github.com/grpc-ecosystem/go-grpc-prometheus
[submodule "vendor/github.com/ugorji/go"]
path = vendor/github.com/ugorji/go
url = https://github.com/ugorji/go
[submodule "vendor/github.com/purpleidea/docker"]
path = vendor/github.com/docker/docker
url = https://github.com/purpleidea/docker
[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

View File

@@ -24,21 +24,21 @@ install: 'make deps'
matrix:
fast_finish: false
allow_failures:
- go: 1.12.x
- go: 1.19.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.18.x
env: TEST_BLOCK=basic
- name: "shell tests"
go: 1.11.x
go: 1.18.x
env: TEST_BLOCK=shell
- name: "race tests"
go: 1.11.x
go: 1.18.x
env: TEST_BLOCK=race
- go: 1.12.x
- go: 1.19.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-2023+ 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

180
Makefile
View File

@@ -1,5 +1,5 @@
# Mgmt
# Copyright (C) 2013-2019+ James Shubin and the project contributors
# Copyright (C) 2013-2023+ 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
@@ -16,8 +16,12 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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 rpmbuild mkdirs rpm srpm spec tar upload upload-sources upload-srpms upload-rpms upload-releases copr tag release funcgen
.SILENT: clean bindata
.PHONY: all art cleanart version program lang path deps run race 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_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
# a large amount of output from this `find`, can cause `make` to be much slower!
GO_FILES := $(shell find * -name '*.go' -not -path 'old/*' -not -path 'tmp/*')
@@ -49,9 +53,26 @@ GOOSARCHES ?= linux/amd64 linux/ppc64 linux/ppc64le linux/arm64 darwin/amd64
GOHOSTOS = $(shell go env GOHOSTOS)
GOHOSTARCH = $(shell go env GOHOSTARCH)
RPM_PKG = releases/$(VERSION)/rpm/mgmt-$(VERSION)-1.x86_64.rpm
DEB_PKG = releases/$(VERSION)/deb/mgmt_$(VERSION)_amd64.deb
PACMAN_PKG = releases/$(VERSION)/pacman/mgmt-$(VERSION)-1-x86_64.pkg.tar.xz
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)
SHA256SUMS = releases/$(VERSION)/SHA256SUMS
SHA256SUMS_ASC = $(SHA256SUMS).asc
@@ -116,11 +137,6 @@ run: ## run mgmt
race:
find . -maxdepth 1 -type f -name '*.go' -not -name '*_test.go' | xargs go run -race -ldflags "-X main.program=$(PROGRAM) -X main.version=$(SVERSION)"
# generate go files from non-go source
bindata: ## generate go files from non-go sources
$(MAKE) --quiet -C bindata
$(MAKE) --quiet -C lang/funcs
generate:
go generate
@@ -132,7 +148,7 @@ lang: ## generates the lexer/parser for the language frontend
$(PROGRAM): build/mgmt-${GOHOSTOS}-${GOHOSTARCH} ## build an mgmt binary for current host os/arch
cp -a $< $@
$(PROGRAM).static: $(GO_FILES) $(MCL_FILES)
$(PROGRAM).static: $(GO_FILES) $(MCL_FILES) go.mod go.sum
@echo "Building: $(PROGRAM).static, version: $(SVERSION)..."
go generate
go build -a -installsuffix cgo -tags netgo -ldflags '-extldflags "-static" -X main.program=$(PROGRAM) -X main.version=$(SVERSION) -s -w' -o $(PROGRAM).static $(BUILD_FLAGS);
@@ -147,24 +163,21 @@ build-debug: $(PROGRAM)
# extract os and arch from target pattern
GOOS=$(firstword $(subst -, ,$*))
GOARCH=$(lastword $(subst -, ,$*))
build/mgmt-%: $(GO_FILES) $(MCL_FILES) | bindata lang funcgen
build/mgmt-%: $(GO_FILES) $(MCL_FILES) go.mod go.sum | 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 -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
# to use this you might want to run something like:
# GOOSARCHES='linux/arm64' GOTAGS='noaugeas novirt' make crossbuild
# and the output will end up in build/
crossbuild_targets = $(addprefix build/mgmt-,$(subst /,-,${GOOSARCHES}))
crossbuild: ${crossbuild_targets}
clean: ## clean things up
$(MAKE) --quiet -C bindata clean
$(MAKE) --quiet -C lang/funcs clean
$(MAKE) --quiet -C test clean
$(MAKE) --quiet -C lang clean
$(MAKE) --quiet -C misc/mkosi clean
rm -f lang/funcs/core/generated_funcs.go || true
rm -f lang/funcs/core/generated_funcs_test.go || true
[ ! -e $(PROGRAM) ] || rm $(PROGRAM)
@@ -174,6 +187,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)
@@ -343,18 +358,63 @@ copr: upload-srpms ## build in copr
tag: ## tags a new release
./misc/tag.sh
#
# 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..."
@title='$@' ; distro=$${title#'mkosi_'} ; ./misc/mkosi/make.sh $${distro} `realpath "releases/$(VERSION)/"`
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)/"`
mkosi_ubuntu-bionic: releases/$(VERSION)/.mkdir
@title='$@' ; echo "Generating: $${title#'mkosi_'} via mkosi..."
@title='$@' ; distro=$${title#'mkosi_'} ; ./misc/mkosi/make.sh $${distro} `realpath "releases/$(VERSION)/"`
mkosi_archlinux: releases/$(VERSION)/.mkdir
@title='$@' ; echo "Generating: $${title#'mkosi_'} via mkosi..."
@title='$@' ; distro=$${title#'mkosi_'} ; ./misc/mkosi/make.sh $${distro} `realpath "releases/$(VERSION)/"`
#
# release
#
release: releases/$(VERSION)/mgmt-release.url ## generates and uploads a release
releases/$(VERSION)/mgmt-release.url: $(RPM_PKG) $(DEB_PKG) $(PACMAN_PKG) $(SHA256SUMS_ASC)
releases_path:
@#Don't put any other output or dependencies in here or they'll show!
@echo "releases/$(VERSION)/"
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_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..."
hub release create \
-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 $(RPM_PKG) \
-a $(DEB_PKG) \
-a $(PACMAN_PKG) \
-a $(PKG_FEDORA-30) \
-a $(PKG_FEDORA-29) \
-a $(PKG_CENTOS-7) \
-a $(PKG_DEBIAN-10) \
-a $(PKG_UBUNTU-BIONIC) \
-a $(PKG_ARCHLINUX) \
-a $(SHA256SUMS_ASC) \
$(VERSION) \
> releases/$(VERSION)/mgmt-release.url \
@@ -362,32 +422,56 @@ releases/$(VERSION)/mgmt-release.url: $(RPM_PKG) $(DEB_PKG) $(PACMAN_PKG) $(SHA2
|| rm -f releases/$(VERSION)/mgmt-release.url
releases/$(VERSION)/.mkdir:
mkdir -p releases/$(VERSION)/{deb,rpm,pacman}/ && 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)/rpm/changelog: $(PROGRAM) releases/$(VERSION)/.mkdir
@echo "Generating: rpm changelog..."
./misc/make-rpm-changelog.sh $(VERSION)
releases/$(VERSION)/$(TOKEN_FEDORA-30)/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)
$(RPM_PKG): releases/$(VERSION)/rpm/changelog
@echo "Building: rpm package..."
./misc/fpm-pack.sh rpm $(VERSION) libvirt-devel augeas-devel
$(PKG_FEDORA-30): releases/$(VERSION)/$(TOKEN_FEDORA-30)/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-30)" libvirt-devel augeas-devel
releases/$(VERSION)/deb/changelog: $(PROGRAM) releases/$(VERSION)/.mkdir
@echo "Generating: deb changelog..."
./misc/make-deb-changelog.sh $(VERSION)
releases/$(VERSION)/$(TOKEN_FEDORA-29)/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)
$(DEB_PKG): releases/$(VERSION)/deb/changelog
@echo "Building: deb package..."
./misc/fpm-pack.sh deb $(VERSION) libvirt-dev libaugeas-dev
$(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
$(PACMAN_PKG): $(PROGRAM) releases/$(VERSION)/.mkdir
@echo "Building: pacman package..."
./misc/fpm-pack.sh pacman $(VERSION) libvirt augeas
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)
$(SHA256SUMS): $(RPM_PKG) $(DEB_PKG) $(PACMAN_PKG)
$(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)
$(PKG_DEBIAN-10): releases/$(VERSION)/$(TOKEN_DEBIAN-10)/changelog
@title='$(@D)' ; distro=$${title#'releases/$(VERSION)/'} ; echo "Building: $${distro} package..."
@title='$(@D)' ; distro=$${title#'releases/$(VERSION)/'} ; ./misc/fpm-pack.sh $${distro} $(VERSION) "$(FILE_DEBIAN-10)" libvirt-dev libaugeas-dev
releases/$(VERSION)/$(TOKEN_UBUNTU-BIONIC)/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)
$(PKG_UBUNTU-BIONIC): releases/$(VERSION)/$(TOKEN_UBUNTU-BIONIC)/changelog
@title='$(@D)' ; distro=$${title#'releases/$(VERSION)/'} ; echo "Building: $${distro} package..."
@title='$(@D)' ; distro=$${title#'releases/$(VERSION)/'} ; ./misc/fpm-pack.sh $${distro} $(VERSION) "$(FILE_UBUNTU-BIONIC)" libvirt-dev libaugeas-dev
$(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_CENTOS-7) $(PKG_DEBIAN-10) $(PKG_UBUNTU-BIONIC) $(PKG_ARCHLINUX)
@# remove the directory separator in the SHA256SUMS file
@echo "Generating: sha256 sum..."
sha256sum $(RPM_PKG) $(DEB_PKG) $(PACMAN_PKG) | 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..."
@@ -413,14 +497,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)
[![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)
[![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://godocs.io/github.com/purpleidea/mgmt)
[![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 |
@@ -99,6 +100,8 @@ Please read, enjoy and help improve our documentation!
| [prometheus guide](docs/prometheus.md) | for everyone |
| [puppet guide](docs/puppet-guide.md) | for puppet sysadmins |
| [development](docs/development.md) | for mgmt developers |
| [videos](docs/on-the-web.md) | for everyone |
| [blogs](docs/on-the-web.md) | for everyone |
## Questions:
@@ -107,12 +110,13 @@ If you have a well phrased question that might benefit others, consider asking
it by sending a patch to the [FAQ](docs/faq.md) section. I'll merge your
question, and a patch with the answer!
## Roadmap:
## Get involved:
Feel free to grab one of the straightforward [#mgmtlove](https://github.com/purpleidea/mgmt/labels/mgmtlove)
issues if you're a first time contributor to the project or if you're unsure
about what to hack on! Please get involved by working on one of these items or
by suggesting something else!
by suggesting something else! There are some lower priority issues and harder
issues available in our [TODO](TODO.md) file. Please have a look.
## Bugs:
@@ -126,6 +130,6 @@ We'd love to have your patches! Please send them by email, or as a pull request.
## On the web:
[Read what people are saying and publishing about mgmt!](docs/on-the-web.md)
[Blog posts and recorded talks about mgmt are listed here!](docs/on-the-web.md)
Happy hacking!

65
TODO.md
View File

@@ -1,10 +1,18 @@
# TODO
If you're looking for something to do, look here!
Let us know if you're working on one of the items.
If you'd like something to work on, ping @purpleidea and I'll create an issue
tailored especially for you! Just let me know your approximate golang skill
level and how many hours you'd like to spend on the patch.
Here is a TODO list of longstanding items that are either lower-priority, or
more involved in terms of time, skill-level, and/or motivation.
Please have a look, and let us know if you're working on one of the items. It's
best to open an issue to track your progress and to discuss any implementation
questions you might have.
Lastly, if you'd like something different to work on, please ping @purpleidea
and I'll create an issue tailored especially for your approximate golang skill
level and available time commitment in terms of hours you'd need to spend on the
patch.
Happy Hacking!
## Package resource
@@ -19,7 +27,7 @@ level and how many hours you'd like to spend on the patch.
## Svc resource
- [ ] base resource improvements
- [ ] refreshonly support [:heart:](https://github.com/purpleidea/mgmt/issues/464)
## Exec resource
@@ -33,33 +41,14 @@ level and how many hours you'd like to spend on the patch.
- [ ] automatic edges to file resource [:heart:](https://github.com/purpleidea/mgmt/labels/mgmtlove)
## Virt (libvirt) resource
- [ ] base resource improvements [:heart:](https://github.com/purpleidea/mgmt/labels/mgmtlove)
## Net (systemd-networkd) resource
- [ ] base resource
## Nspawn (systemd-nspawn) resource
- [ ] base resource [:heart:](https://github.com/purpleidea/mgmt/labels/mgmtlove)
## Mount (systemd-mount) resource
- [ ] base resource [:heart:](https://github.com/purpleidea/mgmt/labels/mgmtlove)
## Cron (systemd-timer) resource
- [ ] base resource [:heart:](https://github.com/purpleidea/mgmt/labels/mgmtlove)
## Http resource
- [ ] base resource [:heart:](https://github.com/purpleidea/mgmt/labels/mgmtlove)
## Etcd improvements
- [ ] fix embedded etcd master race
- [ ] fix etcd race bug that only happens during CI testing (intermittently
failing test case issue)
## Torrent/dht file transfer
@@ -69,17 +58,33 @@ level and how many hours you'd like to spend on the patch.
- [ ] base plumbing
## Resource improvements
- [ ] more reversible resources implemented
- [ ] more "cloud" resources
## Language improvements
- [ ] more core functions
- [ ] automatic language formatter, ala `gofmt`
- [ ] gedit/gnome-builder/gtksourceview syntax highlighting
- [ ] vim syntax highlighting
- [x] emacs syntax highlighting: see `misc/emacs/`
- [ ] emacs syntax highlighting: see `misc/emacs/` (needs updating)
- [ ] exposed $error variable for feedback in the language
- [ ] improve the printf function to add %[]s, %[]f ([]str, []float) and map,
struct, nested etc... %v would be nice too!
- [ ] add line/col/file annotations to AST so we can get locations of errors
that the parser finds
- [ ] add more error messages with the `%error` pattern in parser.y
- [ ] we should have helper functions or language sugar to pull a field out of a
struct, or a value out of a map, or an index out of a list, etc...
## Engine improvements
- [ ] add a "waiting for func" message in the func engine to notify the user
about slow functions...
## Other
- [ ] better error/retry handling
- [ ] deb package target in Makefile
- [ ] reproducible builds
- [ ] add your suggestions!

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

BIN
art/mgmt_poohbear_meme.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

View File

@@ -1,42 +0,0 @@
# Mgmt
# Copyright (C) 2013-2019+ 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/>.
# The bindata target generates go files from any source defined below. To use
# the files, import the generated "bindata" package and use:
# `bytes, err := bindata.Asset("FILEPATH")`
# where FILEPATH is the path of the original input file relative to `bindata/`.
# To get a list of files stored in this "bindata" package, you can use:
# `paths := bindata.AssetNames()` and `paths, err := bindata.AssetDir(name)`
# to get a list of files with a directory prefix.
.PHONY: build clean
default: build
build: bindata.go
# add more input files as dependencies at the end here...
bindata.go: ../COPYING
@echo "Generating: bindata..."
# go-bindata --pkg bindata -o <OUTPUT> <INPUT>
go-bindata --pkg bindata -o ./$@ $^
# gofmt the output file
gofmt -s -w $@
@ROOT=$$(dirname "$${BASH_SOURCE}")/.. && $$ROOT/misc/header.sh '$@'
clean:
# remove generated bindata.go
@ROOT=$$(dirname "$${BASH_SOURCE}")/.. && rm -f bindata.go

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2023+ 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-2023+ 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
@@ -15,7 +15,7 @@
// 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
//go:build !root
package converger

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-2023+ 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-2023+ 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.18
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

@@ -6,7 +6,7 @@ ENV PATH=/opt/rh/rh-ruby22/root/usr/bin:/root/gopath/bin:/usr/local/sbin:/sbin:/
ENV LD_LIBRARY_PATH=/opt/rh/rh-ruby22/root/usr/lib64${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}
ENV PKG_CONFIG_PATH=/opt/rh/rh-ruby22/root/usr/lib64/pkgconfig${PKG_CONFIG_PATH:+:${PKG_CONFIG_PATH}}
RUN yum -y install epel-release wget unzip git make which centos-release-scl gcc && sed -i "s/enabled=0/enabled=1/" /etc/yum.repos.d/epel-testing.repo && yum -y install rh-ruby22 && wget -O /opt/go1.9.1.linux-amd64.tar.gz https://storage.googleapis.com/golang/go1.9.1.linux-amd64.tar.gz && tar -C /usr/local -xzf /opt/go1.9.1.linux-amd64.tar.gz
RUN yum -y install epel-release wget unzip git make which centos-release-scl gcc && sed -i "s/enabled=0/enabled=1/" /etc/yum.repos.d/epel-testing.repo && yum -y install rh-ruby22 && wget -O /opt/go1.18.5.linux-amd64.tar.gz https://storage.googleapis.com/golang/go1.18.5.linux-amd64.tar.gz && tar -C /usr/local -xzf /opt/go1.18.5.linux-amd64.tar.gz
RUN mkdir -p $GOPATH/src/github.com/purpleidea && cd $GOPATH/src/github.com/purpleidea && git clone --recursive https://github.com/purpleidea/mgmt
RUN go get -u gopkg.in/alecthomas/gometalinter.v1 && cd $GOPATH/src/github.com/purpleidea/mgmt && make deps && make build
CMD ["/bin/bash"]

View File

@@ -1,4 +1,4 @@
FROM golang:1.11
FROM golang:1.18
MAINTAINER Michał Czeraszkiewicz <contact@czerasz.com>
@@ -27,8 +27,5 @@ WORKDIR /home/$USER_NAME/mgmt
# Install dependencies
RUN make deps
# Chown $GOPATH
RUN chown -R ${USER_ID}:${GROUP_ID} /go
# Change user
USER ${USER_NAME}

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-2023+ 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.18 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:
@@ -250,6 +254,43 @@ integer, then that value is the max size for that semaphore. Valid semaphore
id's include: `some_id`, `hello:42`, `not:smart:4` and `:13`. It is expected
that the last bare example be only used by the engine to add a global semaphore.
#### Rewatch
Boolean. Rewatch specifies whether we re-run the Watch worker during a graph
swap if it has errored. When doing a graph compare to swap the graphs, if this
is true, and this particular worker has errored, then we'll remove it and add it
back as a new vertex, thus causing it to run again. This is different from the
`Retry` metaparam which applies during the normal execution. It is only when
this is exhausted that we're in permanent worker failure, and only then can we
rely on this metaparam.
#### Realize
Boolean. Realize ensures that the resource is guaranteed to converge at least
once before a potential graph swap removes or changes it. This guarantee is
useful for fast changing graphs, to ensure that the brief creation of a resource
is seen. This guarantee does not prevent against the engine quitting normally,
and it can't guarantee it if the resource is blocked because of a failed
pre-requisite resource.
*XXX: This is currently not implemented!*
#### Reverse
Boolean. Reverse is a property that some resources can implement that specifies
that some "reverse" operation should happen when that resource "disappears". A
disappearance happens when a resource is defined in one instance of the graph,
and is gone in the subsequent one. This disappearance can happen if it was
previously in an if statement that then becomes false.
This is helpful for building robust programs with the engine. The engine adds a
"reversed" resource to that subsequent graph to accomplish the desired "reverse"
mechanics. The specifics of what this entails is a property of the particular
resource that is being "reversed".
It might be wise to combine the use of this meta parameter with the use of the
`realize` meta parameter to ensure that your reversed resource actually runs at
least once, if there's a chance that it might be gone for a while.
### Lang metadata file
Any module *must* have a metadata file in its root. It must be named
@@ -323,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
@@ -367,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
@@ -451,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-2023+ 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
@@ -215,16 +216,40 @@ requires a number of seconds as an argument.
./mgmt run lang examples/lang/hello0.mcl --converged-timeout=5
```
### When I try to build `mgmt` I see: `no Go files in $GOPATH/src/github.com/purpleidea/mgmt/bindata`.
### Why does my file resource error with `no such file or directory`?
Due to the arcane way that `golang` designed its `$GOPATH`, the main project
directory must be inside your `$GOPATH`, and at the appropriate FQDN. This is:
`$GOPATH/src/github.com/purpleidea/mgmt/`. If you have your project root outside
of that directory, then you may get this error when you try to build it. In this
case there is likely a `go get` version of the project at this location. Remove
it and replace it with your git cloned directory. In my case, I like to work on
things in `~/code/mgmt/`, so that path is a symlink that points to the long
project directory.
If you create a file resource and only specify the content like this:
```
file "/tmp/foo" {
content => "hello world\n",
}
```
Then this will attempt to set the contents of that file to the desired string,
but *only* if that file already exists. If you'd like to ensure that it also
gets created in case it is not present, then you must also specify the state:
```
file "/tmp/foo" {
state => $const.res.file.state.exists,
content => "hello world\n",
}
```
Similar logic applies for situations when you only specify the `mode` parameter.
This all turns out to be more safe and "correct", in that it would error and
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...`.
@@ -259,6 +284,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
@@ -321,6 +353,14 @@ Don't blindly use the tools that others tell you to. Learn what they do, think
for yourself, and become a power user today! That process led us to using
`git submodules`. Hopefully you'll come to the same conclusions that we did.
**UPDATE:**
After golang made it virtually impossible to build without `go.mod` stuff, we've
switched to it since golang 1.16. I still think the above approach was better,
and that the `go mod` tooling should have been a layer on top of git submodules
so that we don't grow yet another lock file format, and existing folks who are
comfortable with `git` can use those tools directly.
### Did you know that there is a band named `MGMT`?
I didn't realize this when naming the project, and it is accidental. After much
@@ -334,7 +374,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

@@ -44,3 +44,16 @@ if we missed something that you think is relevant!
| James Shubin | blog | [Mgmt Configuration Language](https://purpleidea.com/blog/2018/02/05/mgmt-configuration-language/) |
| James Shubin | video | [Recording from CfgMgmtCamp.eu 2018](https://www.youtube.com/watch?v=NxObmwZDyrI) |
| Jonathan Gold | blog | [Go Netlink and Select](https://jonathangold.ca/blog/go-netlink-and-select/) |
| James Shubin | video | [Recording from DevOpsDays Montreal 2018](https://www.youtube.com/watch?v=1i38c5cooHo) |
| James Shubin | video | [Recording from FOSDEM Minimalistic Languages Devroom 2019](https://video.fosdem.org/2019/K.4.201/mgmtconfig.webm) |
| James Shubin | video | [Recording from FOSDEM Infra Management Devroom 2019](https://video.fosdem.org/2019/UB2.252A/mgmt.webm) |
| James Shubin | video | [Recording from FOSDEM Graph Processing Devroom 2019](https://video.fosdem.org/2019/H.1308/graph_mgmt_config.webm) |
| James Shubin | video | [Recording from FOSDEM Virtualization Devroom 2019](https://video.fosdem.org/2019/H.2213/vai_real_time_virtualization_automation.webm) |
| 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) |
| James Shubin | video | [Recording from CfgMgmtCamp.eu 2023](https://www.youtube.com/watch?v=FeRGRj8w0BU) |

View File

@@ -37,13 +37,13 @@ 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.18 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)
and run: `brew install go`
* You can run `go version` to check the golang version.
* If your distro is tool old, you may need to [download](https://golang.org/dl/)
* If your distro is too old, you may need to [download](https://golang.org/dl/)
a newer golang version.
#### Setting up golang
@@ -63,13 +63,11 @@ export GOPATH=$HOME/gopath
#### Getting the mgmt code and associated dependencies
* Download the `mgmt` code into the `GOPATH`, and switch to that directory:
* Download the `mgmt` code and switch to that directory:
```shell
[ -z "$GOPATH" ] && mkdir ~/go/ || mkdir -p $GOPATH/src/github.com/purpleidea/
cd $GOPATH/src/github.com/purpleidea/ || cd ~/go/
git clone --recursive https://github.com/purpleidea/mgmt/
cd $GOPATH/src/github.com/purpleidea/mgmt/ || cd ~/go/src/github.com/purpleidea/mgmt/
git clone --recursive https://github.com/purpleidea/mgmt/ ~/mgmt/
cd ~/mgmt/
```
* Add `$GOPATH/bin` to `$PATH`

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.
@@ -69,9 +72,9 @@ identified by a trailing slash in their path name. File have no such slash.
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
* `state`: either `exists` (the default value) or `absent`
* `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
@@ -79,6 +82,16 @@ It has the following properties:
The path property specifies the file or directory that we are managing.
### State
The state property describes the action we'd like to apply for the resource. The
possible values are: `exists` and `absent`. If you do not specify either of
these, it is undefined. Without specifying this value as `exists`, another param
cannot cause a file to get implicitly created. When specifying this value as
`absent`, you should not specify any other params that would normally change the
file. For example, if you specify `content` and this param is `absent`, then you
will get an engine validation error.
### Content
The content property is a string that specifies the desired file contents.
@@ -88,10 +101,12 @@ 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.
### State
### Fragments
The state property describes the action we'd like to apply for the resource. The
possible values are: `exists` and `absent`.
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
@@ -104,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`.
@@ -206,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-2023+ 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-2023+ 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
@@ -15,7 +15,7 @@
// 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
//go:build !root
package engine

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2023+ 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-2023+ 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
@@ -152,6 +152,18 @@ func ResCmp(r1, r2 Res) error {
}
}
// compare meta params for resources with reversible traits
r1v, ok1 := r1.(ReversibleRes)
r2v, ok2 := r2.(ReversibleRes)
if ok1 != ok2 {
return fmt.Errorf("reversible differs") // they must be different (optional)
}
if ok1 && ok2 {
if r1v.ReversibleMeta().Cmp(r2v.ReversibleMeta()) != nil {
return fmt.Errorf("reversible differs")
}
}
return nil
}
@@ -280,6 +292,18 @@ func AdaptCmp(r1, r2 CompatibleRes) error {
}
}
// compare meta params for resources with reversible traits
r1v, ok1 := r1.(ReversibleRes)
r2v, ok2 := r2.(ReversibleRes)
if ok1 != ok2 {
return fmt.Errorf("reversible differs") // they must be different (optional)
}
if ok1 && ok2 {
if r1v.ReversibleMeta().Cmp(r2v.ReversibleMeta()) != nil {
return fmt.Errorf("reversible differs")
}
}
return nil
}

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2023+ 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
@@ -106,6 +106,16 @@ func ResCopy(r CopyableRes) (CopyableRes, error) {
}
}
// copy meta params for resources with reversible traits
if x, ok := r.(ReversibleRes); ok {
dst, ok := res.(ReversibleRes)
if !ok {
// programming error
panic("reversible interfaces are illogical")
}
dst.SetReversibleMeta(x.ReversibleMeta()) // no need to copy atm
}
return res, nil
}

21
engine/doc.go Normal file
View File

@@ -0,0 +1,21 @@
// Mgmt
// Copyright (C) 2013-2023+ 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 represents the implementation of the resource engine that runs
// the graph of resources in real-time. This package has the common imports that
// most consumers use directly.
package engine

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2023+ 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-2023+ 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-2023+ 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-2023+ 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-2023+ 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-2023+ 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,9 @@ func AutoEdge(graph *pgraph.Graph, debug bool, logf func(format string, v ...int
}
}
}
// 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-2023+ 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-2023+ 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,5 +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 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-2023+ 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
@@ -15,7 +15,7 @@
// 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
//go:build !root
package autogroup
@@ -595,10 +595,12 @@ func TestPgraphGrouping11(t *testing.T) {
runGraphCmp(t, g1, g2)
}
/*
// 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 +622,12 @@ func TestPgraphGrouping12(t *testing.T) {
runGraphCmp(t, g1, g2)
}
/*
// 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 +649,12 @@ func TestPgraphGrouping13(t *testing.T) {
runGraphCmp(t, g1, g2)
}
/*
// 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 +679,14 @@ func TestPgraphGrouping14(t *testing.T) {
runGraphCmp(t, g1, g2)
}
/*
// 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,6 +716,7 @@ func TestPgraphGrouping15(t *testing.T) {
runGraphCmp(t, g1, g2)
}
/*
// 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!
@@ -716,6 +725,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 +753,14 @@ func TestPgraphGrouping16(t *testing.T) {
runGraphCmp(t, g1, g2)
}
/*
// 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,6 +788,7 @@ func TestPgraphGrouping17(t *testing.T) {
runGraphCmp(t, g1, g2)
}
/*
// re-attach 3 (double)
// similar to "re-attach 1", technically there is a second possibility for this
// a2 a1 b2 a1,a2
@@ -783,6 +796,7 @@ func TestPgraphGrouping17(t *testing.T) {
// \ b1 / >>> b1,b2 (arrows point downwards)
// \ | / |
// c1 c1
*/
func TestPgraphGrouping18(t *testing.T) {
g1, _ := pgraph.NewGraph("g1") // original graph
{
@@ -813,10 +827,12 @@ func TestPgraphGrouping18(t *testing.T) {
runGraphCmp(t, g1, g2)
}
/*
// 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 +851,14 @@ func TestPgraphGroupingConnected0(t *testing.T) {
runGraphCmp(t, g1, g2)
}
/*
// 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-2023+ 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-2023+ 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-2023+ 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,13 +18,16 @@
package autogroup
import (
"fmt"
"github.com/purpleidea/mgmt/engine"
"github.com/purpleidea/mgmt/pgraph"
"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
@@ -112,8 +115,17 @@ func VertexMerge(g *pgraph.Graph, v1, v2 pgraph.Vertex, vertexMergeFn func(pgrap
// note: This branch isn't used if the vertexMergeFn
// decides to just merge logically on its own instead
// of actually returning something that we then merge.
v1 = v // TODO: ineffassign?
v1 = v // XXX: ineffassign?
//*v1 = *v
// Ensure that everything still validates. (For safety!)
r, ok := v1.(engine.Res) // TODO: v ?
if !ok {
return fmt.Errorf("not a Res")
}
if err := engine.Validate(r); err != nil {
return errwrap.Wrapf(err, "the Res did not Validate")
}
}
}
g.DeleteVertex(v2) // remove grouped vertex

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2023+ 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
@@ -15,6 +15,9 @@
// 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 graph contains the actual implementation of the resource graph engine
// that runs the graph of resources in real-time. This package has the algorithm
// that runs all the graph transitions.
package graph
import (
@@ -25,14 +28,22 @@ import (
"github.com/purpleidea/mgmt/converger"
"github.com/purpleidea/mgmt/engine"
engineUtil "github.com/purpleidea/mgmt/engine/util"
"github.com/purpleidea/mgmt/pgraph"
"github.com/purpleidea/mgmt/util/errwrap"
"github.com/purpleidea/mgmt/util/semaphore"
)
const (
// StateDir is the name of the sub directory where all the local
// resource state is stored.
StateDir = "state"
)
// Engine encapsulates a generic graph and manages its operations.
type Engine struct {
Program string
Version string
Hostname string
World engine.World
@@ -174,9 +185,9 @@ func (obj *Engine) Commit() error {
return errwrap.Wrapf(err, "the Res did not Validate")
}
// FIXME: is res.Name() sufficiently unique to use as a UID here?
pathUID := fmt.Sprintf("%s-%s", res.Kind(), res.Name())
statePrefix := fmt.Sprintf("%s/", path.Join(obj.Prefix, "state", pathUID))
pathUID := engineUtil.ResPathUID(res)
statePrefix := fmt.Sprintf("%s/", path.Join(obj.statePrefix(), pathUID))
// don't create this unless it *will* be used
//if err := os.MkdirAll(statePrefix, 0770); err != nil {
// return errwrap.Wrapf(err, "can't create state prefix")
@@ -184,10 +195,11 @@ 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,
Version: obj.Version,
Hostname: obj.Hostname,
World: obj.World,
@@ -322,14 +334,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
}
@@ -416,3 +428,8 @@ func (obj *Engine) Close() error {
func (obj *Engine) Graph() *pgraph.Graph {
return obj.graph
}
// statePrefix returns the dir where all the resource state is stored locally.
func (obj *Engine) statePrefix() string {
return fmt.Sprintf("%s/", path.Join(obj.Prefix, StateDir))
}

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2023+ 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
@@ -15,7 +15,7 @@
// 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
//go:build !root
package graph

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2023+ 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

300
engine/graph/reverse.go Normal file
View File

@@ -0,0 +1,300 @@
// Mgmt
// Copyright (C) 2013-2023+ 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 graph
import (
"fmt"
"io/ioutil"
"os"
"path"
"sort"
"github.com/purpleidea/mgmt/engine"
engineUtil "github.com/purpleidea/mgmt/engine/util"
"github.com/purpleidea/mgmt/pgraph"
"github.com/purpleidea/mgmt/util/errwrap"
)
const (
// ReverseFile is the file name in the resource state dir where any
// reversal information is stored.
ReverseFile = "reverse"
// ReversePerm is the permissions mode used to create the ReverseFile.
ReversePerm = 0600
)
// Reversals adds the reversals onto the loaded graph. This should happen last,
// and before Commit.
func (obj *Engine) Reversals() error {
if obj.nextGraph == nil {
return fmt.Errorf("there is no active graph to add reversals to")
}
// Initially get all of the reversals to seek out all possible errors.
// XXX: The engine needs to know where data might have been stored if we
// XXX: want to potentially allow alternate read/write paths, like etcd.
// XXX: In this scenario, we'd have to store a token somewhere to let us
// XXX: know to look elsewhere for the special ReversalList read method.
data, err := obj.ReversalList() // (map[string]string, error)
if err != nil {
return errwrap.Wrapf(err, "the reversals had errors")
}
if len(data) == 0 {
return nil // end early
}
resMatch := func(r1, r2 engine.Res) bool { // simple match on UID only!
if r1.Kind() != r2.Kind() {
return false
}
if r1.Name() != r2.Name() {
return false
}
return true
}
resInList := func(needle engine.Res, haystack []engine.Res) bool {
for _, res := range haystack {
if resMatch(needle, res) {
return true
}
}
return false
}
if obj.Debug {
obj.Logf("decoding %d reversals...", len(data))
}
resources := []engine.Res{}
// do this in a sorted order so that it errors deterministically
sorted := []string{}
for key := range data {
sorted = append(sorted, key)
}
sort.Strings(sorted)
for _, key := range sorted {
val := data[key]
// XXX: replace this ResToB64 method with one that stores it in
// a human readable format, in case someone wants to hack and
// edit it manually.
// XXX: we probably want this to be YAML, it works with the diff
// too...
r, err := engineUtil.B64ToRes(val)
if err != nil {
return errwrap.Wrapf(err, "error decoding res with UID: `%s`", key)
}
res, ok := r.(engine.ReversibleRes)
if !ok {
// this requirement is here to keep things simpler...
return errwrap.Wrapf(err, "decoded res with UID: `%s` was not reversible", key)
}
matchFn := func(vertex pgraph.Vertex) (bool, error) {
r, ok := vertex.(engine.Res)
if !ok {
return false, fmt.Errorf("not a Res")
}
if !resMatch(r, res) {
return false, nil
}
return true, nil
}
// FIXME: not efficient, we could build a cache-map first
vertex, err := obj.nextGraph.VertexMatchFn(matchFn) // (Vertex, error)
if err != nil {
return errwrap.Wrapf(err, "error searching graph for match")
}
if vertex != nil { // found one!
continue // it doesn't need reversing yet
}
// TODO: check for (incompatible?) duplicates instead
if resInList(res, resources) { // we've already got this one...
continue
}
// We set this in two different places to be safe. It ensures
// that we erase the reversal state file after we've used it.
res.ReversibleMeta().Reversal = true // set this for later...
resources = append(resources, res)
}
if len(resources) == 0 {
return nil // end early
}
// Now that we've passed the chance of any errors, we modify the graph.
obj.Logf("adding %d reversals...", len(resources))
for _, res := range resources {
obj.nextGraph.AddVertex(res)
}
// TODO: Do we want a way for stored reversals to add edges too?
// 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
}
// ReversalList returns all the available pending reversal data on this host. It
// can then be decoded by whatever method is appropriate for.
func (obj *Engine) ReversalList() (map[string]string, error) {
result := make(map[string]string) // some key to contents
dir := obj.statePrefix() // loop through this dir...
files, err := ioutil.ReadDir(dir)
if err != nil && !os.IsNotExist(err) {
return nil, errwrap.Wrapf(err, "error reading list of state dirs")
} else if err != nil {
return result, nil // nothing found, no state dir exists yet
}
for _, x := range files {
key := x.Name() // some uid for the resource
file := path.Join(dir, key, ReverseFile)
content, err := ioutil.ReadFile(file)
if err != nil && !os.IsNotExist(err) {
return nil, errwrap.Wrapf(err, "could not read reverse file: %s", file)
} else if err != nil {
continue // file does not exist, skip
}
// file exists!
str := string(content)
result[key] = str // save
}
return result, nil
}
// ReversalInit performs the reversal initialization steps if necessary for this
// resource.
func (obj *State) ReversalInit() error {
res, ok := obj.Vertex.(engine.ReversibleRes)
if !ok {
return nil // nothing to do
}
if res.ReversibleMeta().Disabled {
return nil // nothing to do, reversal isn't enabled
}
// If the reversal is enabled, but we are the result of a previous
// reversal, then this will overwrite that older reversal request, and
// our resource should be designed to deal with that. This happens if we
// return a reversible resource as the reverse of a resource that was
// reversed. It's probably fairly rare.
if res.ReversibleMeta().Reversal {
obj.Logf("triangle reversal") // warn!
}
r, err := res.Reversed()
if err != nil {
return errwrap.Wrapf(err, "could not reverse: %s", res.String())
}
if r == nil {
return nil // this can't be reversed, or isn't implemented here
}
// We set this in two different places to be safe. It ensures that we
// erase the reversal state file after we've used it.
r.ReversibleMeta().Reversal = true // set this for later...
// XXX: replace this ResToB64 method with one that stores it in a human
// readable format, in case someone wants to hack and edit it manually.
// XXX: we probably want this to be YAML, it works with the diff too...
str, err := engineUtil.ResToB64(r)
if err != nil {
return errwrap.Wrapf(err, "could not encode: %s", res.String())
}
// TODO: put this method on traits.Reversible as part of the interface?
return obj.ReversalWrite(str, res.ReversibleMeta().Overwrite) // Store!
}
// ReversalClose performs the reversal shutdown steps if necessary for this
// resource.
func (obj *State) ReversalClose() error {
res, ok := obj.Vertex.(engine.ReversibleRes)
if !ok {
return nil // nothing to do
}
// Don't check res.ReversibleMeta().Disabled because we're removing the
// previous one. That value only applies if we're doing a new reversal.
if !res.ReversibleMeta().Reversal {
return nil // nothing to erase, we're not a reversal resource
}
if !obj.isStateOK { // did we successfully reverse?
obj.Logf("did not complete reversal") // warn
return nil
}
// TODO: put this method on traits.Reversible as part of the interface?
return obj.ReversalDelete() // Erase our reversal instructions.
}
// ReversalWrite stores the reversal state information for this resource.
func (obj *State) ReversalWrite(str string, overwrite bool) error {
dir, err := obj.varDir("") // private version
if err != nil {
return errwrap.Wrapf(err, "could not get VarDir for reverse")
}
file := path.Join(dir, ReverseFile) // return a unique file
content, err := ioutil.ReadFile(file)
if err != nil && !os.IsNotExist(err) {
return errwrap.Wrapf(err, "could not read reverse file: %s", file)
}
// file exists and we shouldn't overwrite if different
if err == nil && !overwrite {
// compare to existing file
oldStr := string(content)
if str != oldStr {
obj.Logf("existing, pending, reversible resource exists")
//obj.Logf("diff:")
//obj.Logf("") // TODO: print the diff w/o and secret values
return fmt.Errorf("existing, pending, reversible resource exists")
}
}
return ioutil.WriteFile(file, []byte(str), ReversePerm)
}
// ReversalDelete removes the reversal state information for this resource.
func (obj *State) ReversalDelete() error {
dir, err := obj.varDir("") // private version
if err != nil {
return errwrap.Wrapf(err, "could not get VarDir for reverse")
}
file := path.Join(dir, ReverseFile) // return a unique 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-2023+ 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-2023+ 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
@@ -15,7 +15,7 @@
// 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
//go:build !root
package graph

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2023+ 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-2023+ 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.
@@ -40,6 +40,7 @@ type State struct {
Vertex pgraph.Vertex
Program string
Version string
Hostname string
World engine.World
@@ -154,6 +155,7 @@ func (obj *State) Init() error {
obj.init = &engine.Init{
Program: obj.Program,
Version: obj.Version,
Hostname: obj.Hostname,
// Watch:
@@ -169,25 +171,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, err
}
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,
@@ -203,6 +243,12 @@ func (obj *State) Init() error {
if obj.Debug {
obj.Logf("Init(%s)", res)
}
// write the reverse request to the disk...
if err := obj.ReversalInit(); err != nil {
return err // TODO: test this code path...
}
err := res.Init(obj.init)
if obj.Debug {
obj.Logf("Init(%s): Return(%+v)", res, err)
@@ -236,12 +282,23 @@ func (obj *State) Close() error {
if obj.Debug {
obj.Logf("Close(%s)", res)
}
err := res.Close()
if obj.Debug {
obj.Logf("Close(%s): Return(%+v)", res, err)
var reverr error
// clear the reverse request from the disk...
if err := obj.ReversalClose(); err != nil {
// TODO: test this code path...
// TODO: should this be an error or a warning?
reverr = err
}
return err
reterr := res.Close()
if obj.Debug {
obj.Logf("Close(%s): Return(%+v)", res, reterr)
}
reterr = errwrap.Append(reterr, reverr)
return reterr
}
// Poke sends a notification on the poke channel. This channel is used to notify

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2023+ 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-2023+ 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-2023+ 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-2023+ 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
@@ -15,7 +15,7 @@
// 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
//go:build !root
package engine

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2023+ 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-2023+ 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 == "" {
@@ -86,6 +87,9 @@ type Init struct {
// Program is the name of the program.
Program string
// Version is the version of the program.
Version string
// Hostname is the uuid for the host.
Hostname string
@@ -120,6 +124,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 +245,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-2023+ 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
@@ -15,7 +15,7 @@
// 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 !noaugeas
//go:build !noaugeas
package resources
@@ -29,8 +29,6 @@ import (
"github.com/purpleidea/mgmt/recwatch"
"github.com/purpleidea/mgmt/util/errwrap"
// FIXME: we vendor go/augeas because master requires augeas 1.6.0
// and libaugeas-dev-1.6.0 is not yet available in a PPA.
"honnef.co/go/augeas"
)
@@ -124,8 +122,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 +299,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-2023+ 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()
@@ -751,45 +755,37 @@ func (obj *AwsEc2Res) CheckApply(apply bool) (bool, error) {
// Cmp compares two resources and returns an error if they are not equivalent.
func (obj *AwsEc2Res) Cmp(r engine.Res) error {
if !obj.Compare(r) {
return fmt.Errorf("did not compare")
}
return nil
}
// Compare two resources and return if they are equivalent.
func (obj *AwsEc2Res) Compare(r engine.Res) bool {
// we can only compare AwsEc2Res to others of the same resource kind
res, ok := r.(*AwsEc2Res)
if !ok {
return false
return fmt.Errorf("not a %s", obj.Kind())
}
if obj.State != res.State {
return false
return fmt.Errorf("the State differs")
}
if obj.Region != res.Region {
return false
return fmt.Errorf("the Region differs")
}
if obj.Type != res.Type {
return false
return fmt.Errorf("the Type differs")
}
if obj.ImageID != res.ImageID {
return false
return fmt.Errorf("the ImageID differs")
}
if obj.WatchEndpoint != res.WatchEndpoint {
return false
return fmt.Errorf("the WatchEndpoint differs")
}
if obj.WatchListenAddr != res.WatchListenAddr {
return false
return fmt.Errorf("the WatchListenAddr differs")
}
if obj.ErrorOnMalformedPost != res.ErrorOnMalformedPost {
return false
return fmt.Errorf("the ErrorOnMalformedPost differs")
}
if obj.UserData != res.UserData {
return false
return fmt.Errorf("the UserData differs")
}
return true
return nil
}
func (obj *AwsEc2Res) prependName() string {
@@ -803,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()},
@@ -813,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
@@ -950,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)
@@ -1025,7 +1021,7 @@ func (obj *AwsEc2Res) snsMakeTopic() (string, error) {
}
obj.init.Logf("Created SNS Topic")
if topic.TopicArn == nil {
return "", fmt.Errorf("TopicArn is nil")
return "", fmt.Errorf("the TopicArn is nil")
}
return *topic.TopicArn, nil
}
@@ -1043,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{
@@ -1060,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{
@@ -1113,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-2023+ 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-2023+ 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-2023+ 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-2023+ 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
@@ -33,10 +33,10 @@ import (
"github.com/purpleidea/mgmt/util"
"github.com/purpleidea/mgmt/util/errwrap"
sdbus "github.com/coreos/go-systemd/dbus"
"github.com/coreos/go-systemd/unit"
systemdUtil "github.com/coreos/go-systemd/util"
"github.com/godbus/dbus"
sdbus "github.com/coreos/go-systemd/v22/dbus"
"github.com/coreos/go-systemd/v22/unit"
systemdUtil "github.com/coreos/go-systemd/v22/util"
"github.com/godbus/dbus/v5"
)
const (
@@ -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-2023+ 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
@@ -15,5 +15,5 @@
// 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 bindata stores core mcl code that is built-in at compile time.
package bindata
// Package resources contains the implementations of all the core resources.
package resources

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2023+ 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
@@ -15,7 +15,7 @@
// 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
//go:build !nodocker
package resources
@@ -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-2023+ 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
@@ -15,7 +15,7 @@
// 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
//go:build !nodocker
package resources
@@ -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-2023+ 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/>.
//go: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-2023+ 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-2023+ 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
@@ -15,7 +15,7 @@
// 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
//go:build !root
package resources
@@ -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
}
}

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-2023+ 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
@@ -15,7 +15,7 @@
// 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
//go:build !root
package resources
@@ -78,7 +78,7 @@ func TestMiscEncodeDecode1(t *testing.T) {
e := gob.NewEncoder(&b1)
err = e.Encode(&input) // pass with &
if err != nil {
t.Errorf("Gob failed to Encode: %v", err)
t.Errorf("gob failed to Encode: %v", err)
}
str := base64.StdEncoding.EncodeToString(b1.Bytes())
@@ -86,27 +86,27 @@ func TestMiscEncodeDecode1(t *testing.T) {
var output interface{}
bb, err := base64.StdEncoding.DecodeString(str)
if err != nil {
t.Errorf("Base64 failed to Decode: %v", err)
t.Errorf("base64 failed to Decode: %v", err)
}
b2 := bytes.NewBuffer(bb)
d := gob.NewDecoder(b2)
err = d.Decode(&output) // pass with &
if err != nil {
t.Errorf("Gob failed to Decode: %v", err)
t.Errorf("gob failed to Decode: %v", err)
}
res1, ok := input.(engine.Res)
if !ok {
t.Errorf("Input %v is not a Res", res1)
t.Errorf("input %v is not a Res", res1)
return
}
res2, ok := output.(engine.Res)
if !ok {
t.Errorf("Output %v is not a Res", res2)
t.Errorf("output %v is not a Res", res2)
return
}
if err := res1.Cmp(res2); err != nil {
t.Errorf("The input and output Res values do not match: %+v", err)
t.Errorf("the input and output Res values do not match: %+v", err)
}
}
@@ -116,7 +116,7 @@ func TestMiscEncodeDecode2(t *testing.T) {
// encode
input, err := engine.NewNamedResource("file", "file1")
if err != nil {
t.Errorf("Can't create: %v", err)
t.Errorf("can't create: %v", err)
return
}
// NOTE: Do not add this bit of code, because it would cause the path to
@@ -128,29 +128,29 @@ func TestMiscEncodeDecode2(t *testing.T) {
b64, err := engineUtil.ResToB64(input)
if err != nil {
t.Errorf("Can't encode: %v", err)
t.Errorf("can't encode: %v", err)
return
}
output, err := engineUtil.B64ToRes(b64)
if err != nil {
t.Errorf("Can't decode: %v", err)
t.Errorf("can't decode: %v", err)
return
}
res1, ok := input.(engine.Res)
if !ok {
t.Errorf("Input %v is not a Res", res1)
t.Errorf("input %v is not a Res", res1)
return
}
res2, ok := output.(engine.Res)
if !ok {
t.Errorf("Output %v is not a Res", res2)
t.Errorf("output %v is not a Res", res2)
return
}
// this uses the standalone file cmp function
if err := res1.Cmp(res2); err != nil {
t.Errorf("The input and output Res values do not match: %+v", err)
t.Errorf("the input and output Res values do not match: %+v", err)
}
}
@@ -160,7 +160,7 @@ func TestMiscEncodeDecode3(t *testing.T) {
// encode
input, err := engine.NewNamedResource("file", "file1")
if err != nil {
t.Errorf("Can't create: %v", err)
t.Errorf("can't create: %v", err)
return
}
fileRes := input.(*FileRes) // must not panic
@@ -169,29 +169,82 @@ func TestMiscEncodeDecode3(t *testing.T) {
b64, err := engineUtil.ResToB64(input)
if err != nil {
t.Errorf("Can't encode: %v", err)
t.Errorf("can't encode: %v", err)
return
}
output, err := engineUtil.B64ToRes(b64)
if err != nil {
t.Errorf("Can't decode: %v", err)
t.Errorf("can't decode: %v", err)
return
}
res1, ok := input.(engine.Res)
if !ok {
t.Errorf("Input %v is not a Res", res1)
t.Errorf("input %v is not a Res", res1)
return
}
res2, ok := output.(engine.Res)
if !ok {
t.Errorf("Output %v is not a Res", res2)
t.Errorf("output %v is not a Res", res2)
return
}
// this uses the more complete, engine cmp function
if err := engine.ResCmp(res1, res2); err != nil {
t.Errorf("The input and output Res values do not match: %+v", err)
t.Errorf("the input and output Res values do not match: %+v", err)
}
}
func TestMiscEncodeDecode4(t *testing.T) {
var err error
const (
Kind = "file"
Name = "file1"
)
// encode
input, err := engine.NewNamedResource(Kind, Name)
if err != nil {
t.Errorf("can't create: %v", err)
return
}
fileRes := input.(*FileRes) // must not panic
fileRes.Path = "/tmp/whatever"
// TODO: add other params/traits/etc here!
b64, err := engineUtil.ResToB64(input)
if err != nil {
t.Errorf("can't encode: %v", err)
return
}
output, err := engineUtil.B64ToRes(b64)
if err != nil {
t.Errorf("can't decode: %v", err)
return
}
res1, ok := input.(engine.Res)
if !ok {
t.Errorf("input %v is not a Res", res1)
return
}
res2, ok := output.(engine.Res)
if !ok {
t.Errorf("output %v is not a Res", res2)
return
}
// this uses the more complete, engine cmp function
if err := engine.ResCmp(res1, res2); err != nil {
t.Errorf("the input and output Res values do not match: %+v", err)
}
// ensure the kind and name are correctly decoded too!
if kind := res2.Kind(); kind != Kind {
t.Errorf("the output kind was `%s`, expected `%s`", kind, Kind)
}
if name := res2.Name(); name != Name {
t.Errorf("the output name was `%s`, expected `%s`", name, Name)
}
}

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2023+ 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
@@ -58,7 +58,7 @@ func (obj *GroupRes) Default() engine.Res {
// Validate if the params passed in are valid data.
func (obj *GroupRes) Validate() error {
if obj.State != "exists" && obj.State != "absent" {
return fmt.Errorf("State must be 'exists' or 'absent'")
return fmt.Errorf("state must be 'exists' or 'absent'")
}
return nil
}
@@ -220,32 +220,24 @@ func (obj *GroupRes) CheckApply(apply bool) (bool, error) {
// Cmp compares two resources and returns an error if they are not equivalent.
func (obj *GroupRes) Cmp(r engine.Res) error {
if !obj.Compare(r) {
return fmt.Errorf("did not compare")
}
return nil
}
// Compare two resources and return if they are equivalent.
func (obj *GroupRes) Compare(r engine.Res) bool {
// we can only compare GroupRes to others of the same resource kind
res, ok := r.(*GroupRes)
if !ok {
return false
return fmt.Errorf("not a %s", obj.Kind())
}
if obj.State != res.State {
return false
return fmt.Errorf("the State differs")
}
if (obj.GID == nil) != (res.GID == nil) {
return false
return fmt.Errorf("the GID differs")
}
if obj.GID != nil && res.GID != nil {
if *obj.GID != *res.GID {
return false
return fmt.Errorf("the GID differs")
}
}
return true
return nil
}
// GroupUID is the UID struct for GroupRes.
@@ -279,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()},
@@ -290,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

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-2023+ 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
@@ -27,7 +27,7 @@ import (
"github.com/purpleidea/mgmt/util"
"github.com/purpleidea/mgmt/util/errwrap"
"github.com/godbus/dbus"
"github.com/godbus/dbus/v5"
)
func init() {
@@ -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.
@@ -219,31 +219,23 @@ func (obj *HostnameRes) CheckApply(apply bool) (bool, error) {
// Cmp compares two resources and returns an error if they are not equivalent.
func (obj *HostnameRes) Cmp(r engine.Res) error {
if !obj.Compare(r) {
return fmt.Errorf("did not compare")
}
return nil
}
// Compare two resources and return if they are equivalent.
func (obj *HostnameRes) Compare(r engine.Res) bool {
// we can only compare HostnameRes to others of the same resource kind
res, ok := r.(*HostnameRes)
if !ok {
return false
return fmt.Errorf("not a %s", obj.Kind())
}
if obj.PrettyHostname != res.PrettyHostname {
return false
return fmt.Errorf("the PrettyHostname differs")
}
if obj.StaticHostname != res.StaticHostname {
return false
return fmt.Errorf("the StaticHostname differs")
}
if obj.TransientHostname != res.TransientHostname {
return false
return fmt.Errorf("the TransientHostname differs")
}
return true
return nil
}
// HostnameUID is the UID struct for HostnameRes.
@@ -256,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()},
@@ -269,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-2023+ 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-2023+ 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-2023+ 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
@@ -35,11 +35,11 @@ import (
"github.com/purpleidea/mgmt/util"
"github.com/purpleidea/mgmt/util/errwrap"
sdbus "github.com/coreos/go-systemd/dbus"
"github.com/coreos/go-systemd/unit"
systemdUtil "github.com/coreos/go-systemd/util"
sdbus "github.com/coreos/go-systemd/v22/dbus"
"github.com/coreos/go-systemd/v22/unit"
systemdUtil "github.com/coreos/go-systemd/v22/util"
fstab "github.com/deniswernert/go-fstab"
"github.com/godbus/dbus"
"github.com/godbus/dbus/v5"
"golang.org/x/sys/unix"
)
@@ -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-2023+ 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
@@ -15,7 +15,7 @@
// 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 !darwin
//go:build !root || !darwin
package resources

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2023+ 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
@@ -15,7 +15,7 @@
// 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
//go:build !root
package resources

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2023+ 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,7 +25,7 @@ import (
"github.com/purpleidea/mgmt/engine"
"github.com/purpleidea/mgmt/engine/traits"
"github.com/coreos/go-systemd/journal"
"github.com/coreos/go-systemd/v22/journal"
)
func init() {
@@ -200,36 +200,28 @@ func (obj *MsgRes) CheckApply(apply bool) (bool, error) {
// Cmp compares two resources and returns an error if they are not equivalent.
func (obj *MsgRes) Cmp(r engine.Res) error {
if !obj.Compare(r) {
return fmt.Errorf("did not compare")
}
return nil
}
// Compare two resources and return if they are equivalent.
func (obj *MsgRes) Compare(r engine.Res) bool {
// we can only compare MsgRes to others of the same resource kind
res, ok := r.(*MsgRes)
if !ok {
return false
return fmt.Errorf("not a %s", obj.Kind())
}
if obj.Body != res.Body {
return false
return fmt.Errorf("the Body differs")
}
if obj.Priority != res.Priority {
return false
return fmt.Errorf("the Priority differs")
}
if len(obj.Fields) != len(res.Fields) {
return false
return fmt.Errorf("the length of Fields differs")
}
for field, value := range obj.Fields {
if res.Fields[field] != value {
return false
return fmt.Errorf("the Fields differ")
}
}
return true
return nil
}
// MsgUID is a unique representation for a MsgRes object.
@@ -239,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()},
@@ -249,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-2023+ 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
@@ -15,7 +15,7 @@
// 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
//go:build !root
package resources

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2023+ 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
@@ -15,7 +15,7 @@
// 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 !darwin
//go:build !darwin
package resources
@@ -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)
@@ -506,34 +521,26 @@ func (obj *NetRes) CheckApply(apply bool) (bool, error) {
// Cmp compares two resources and returns an error if they are not equivalent.
func (obj *NetRes) Cmp(r engine.Res) error {
if !obj.Compare(r) {
return fmt.Errorf("did not compare")
}
return nil
}
// Compare two resources and return if they are equivalent.
func (obj *NetRes) Compare(r engine.Res) bool {
// we can only compare NetRes to others of the same resource kind
res, ok := r.(*NetRes)
if !ok {
return false
return fmt.Errorf("not a %s", obj.Kind())
}
if obj.State != res.State {
return false
return fmt.Errorf("the State differs")
}
if (obj.Addrs == nil) != (res.Addrs == nil) {
return false
return fmt.Errorf("the Addrs differ")
}
if err := util.SortedStrSliceCompare(obj.Addrs, res.Addrs); err != nil {
return false
return fmt.Errorf("the Addrs differ")
}
if obj.Gateway != res.Gateway {
return false
return fmt.Errorf("the Gateway differs")
}
return true
return nil
}
// NetUID is a unique resource identifier.
@@ -556,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()},
@@ -566,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
@@ -598,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)
}
@@ -633,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()
@@ -702,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-2023+ 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
@@ -15,7 +15,7 @@
// 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 !darwin
//go:build !darwin
package resources

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2023+ 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-2023+ 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
@@ -15,7 +15,7 @@
// 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
//go:build !root
package resources

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2019+ James Shubin and the project contributors
// Copyright (C) 2013-2023+ 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
@@ -30,10 +30,10 @@ import (
"github.com/purpleidea/mgmt/util"
"github.com/purpleidea/mgmt/util/errwrap"
systemdDbus "github.com/coreos/go-systemd/dbus"
machined "github.com/coreos/go-systemd/machine1"
systemdUtil "github.com/coreos/go-systemd/util"
"github.com/godbus/dbus"
systemdDbus "github.com/coreos/go-systemd/v22/dbus"
machined "github.com/coreos/go-systemd/v22/machine1"
systemdUtil "github.com/coreos/go-systemd/v22/util"
"github.com/godbus/dbus/v5"
)
const (
@@ -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.
@@ -261,35 +258,27 @@ func (obj *NspawnRes) CheckApply(apply bool) (bool, error) {
// Cmp compares two resources and returns an error if they are not equivalent.
func (obj *NspawnRes) Cmp(r engine.Res) error {
if !obj.Compare(r) {
return fmt.Errorf("did not compare")
}
return nil
}
// Compare two resources and return if they are equivalent.
func (obj *NspawnRes) Compare(r engine.Res) bool {
// we can only compare NspawnRes to others of the same resource kind
res, ok := r.(*NspawnRes)
if !ok {
return false
return fmt.Errorf("not a %s", obj.Kind())
}
if obj.State != res.State {
return false
return fmt.Errorf("the State differs")
}
// TODO: why is res.svc ever nil?
if (obj.svc == nil) != (res.svc == nil) { // xor
return false
return fmt.Errorf("the svc differs")
}
if obj.svc != nil && res.svc != nil {
if !obj.svc.Compare(res.svc) {
return false
if err := obj.svc.Cmp(res.svc); err != nil {
return errwrap.Wrapf(err, "the svc differs")
}
}
return true
return nil
}
// NspawnUID is a unique resource identifier.
@@ -312,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()},
@@ -322,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-2023+ 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,7 +29,7 @@ import (
"github.com/purpleidea/mgmt/util"
"github.com/purpleidea/mgmt/util/errwrap"
"github.com/godbus/dbus"
"github.com/godbus/dbus/v5"
)
// global tweaks of verbosity and code path
@@ -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)
@@ -352,7 +355,7 @@ loop:
// should already be broken
break loop
} else {
return []string{}, fmt.Errorf("PackageKit: Error: %v", signal.Body)
return []string{}, fmt.Errorf("error in body: %v", signal.Body)
}
}
}
@@ -363,9 +366,9 @@ loop:
func (obj *Conn) IsInstalledList(packages []string) ([]bool, error) {
var filter uint64 // initializes at the "zero" value of 0
filter += PkFilterEnumArch // always search in our arch
packageIDs, e := obj.ResolvePackages(packages, filter)
if e != nil {
return nil, fmt.Errorf("ResolvePackages error: %v", e)
packageIDs, err := obj.ResolvePackages(packages, filter)
if err != nil {
return nil, errwrap.Wrapf(err, "error resolving packages")
}
var m = make(map[string]int)
@@ -443,7 +446,7 @@ loop:
}
if signal.Name == FmtTransactionMethod("ErrorCode") {
return fmt.Errorf("PackageKit: Error: %v", signal.Body)
return fmt.Errorf("error in body: %v", signal.Body)
} else if signal.Name == FmtTransactionMethod("Package") {
// a package was installed...
// only start the timer once we're here...
@@ -454,14 +457,14 @@ loop:
} else if signal.Name == FmtTransactionMethod("Destroy") {
return nil // success
} else {
return fmt.Errorf("PackageKit: Error: %v", signal.Body)
return fmt.Errorf("error in body: %v", signal.Body)
}
case <-util.TimeAfterOrBlock(timeout):
if finished {
obj.Logf("Timeout: InstallPackages: Waiting for 'Destroy'")
return nil // got tired of waiting for Destroy
}
return fmt.Errorf("PackageKit: Timeout: InstallPackages: %s", strings.Join(packageIDs, ", "))
return fmt.Errorf("timeout installing packages: %s", strings.Join(packageIDs, ", "))
}
}
}
@@ -500,7 +503,7 @@ loop:
}
if signal.Name == FmtTransactionMethod("ErrorCode") {
return fmt.Errorf("PackageKit: Error: %v", signal.Body)
return fmt.Errorf("error in body: %v", signal.Body)
} else if signal.Name == FmtTransactionMethod("Package") {
// a package was installed...
continue loop
@@ -511,7 +514,7 @@ loop:
// should already be broken
break loop
} else {
return fmt.Errorf("PackageKit: Error: %v", signal.Body)
return fmt.Errorf("error in body: %v", signal.Body)
}
}
}
@@ -549,7 +552,7 @@ loop:
}
if signal.Name == FmtTransactionMethod("ErrorCode") {
return fmt.Errorf("PackageKit: Error: %v", signal.Body)
return fmt.Errorf("error in body: %v", signal.Body)
} else if signal.Name == FmtTransactionMethod("Package") {
} else if signal.Name == FmtTransactionMethod("Finished") {
// TODO: should we wait for the Destroy signal?
@@ -558,14 +561,15 @@ loop:
// should already be broken
break loop
} else {
return fmt.Errorf("PackageKit: Error: %v", signal.Body)
return fmt.Errorf("error in body: %v", signal.Body)
}
}
}
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
@@ -601,7 +605,7 @@ loop:
}
if signal.Name == FmtTransactionMethod("ErrorCode") {
err = fmt.Errorf("PackageKit: Error: %v", signal.Body)
err = fmt.Errorf("error in body: %v", signal.Body)
return
// one signal returned per packageID found...
@@ -626,7 +630,7 @@ loop:
// should already be broken
break loop
} else {
err = fmt.Errorf("PackageKit: Error: %v", signal.Body)
err = fmt.Errorf("error in body: %v", signal.Body)
return
}
}
@@ -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()")
@@ -669,7 +674,7 @@ loop:
}
if signal.Name == FmtTransactionMethod("ErrorCode") {
return nil, fmt.Errorf("PackageKit: Error: %v", signal.Body)
return nil, fmt.Errorf("error in body: %v", signal.Body)
} else if signal.Name == FmtTransactionMethod("Package") {
//pkg_int, ok := signal.Body[0].(int)
@@ -692,7 +697,7 @@ loop:
// should already be broken
break loop
} else {
return nil, fmt.Errorf("PackageKit: Error: %v", signal.Body)
return nil, fmt.Errorf("error in body: %v", signal.Body)
}
}
}
@@ -718,9 +723,9 @@ func (obj *Conn) PackagesToPackageIDs(packageMap map[string]string, filter uint6
if obj.Debug {
obj.Logf("PackagesToPackageIDs(): %s", strings.Join(packages, ", "))
}
resolved, e := obj.ResolvePackages(packages, filter)
if e != nil {
return nil, fmt.Errorf("Resolve error: %v", e)
resolved, err := obj.ResolvePackages(packages, filter)
if err != nil {
return nil, errwrap.Wrapf(err, "error resolving")
}
found := make([]bool, count) // default false
@@ -758,7 +763,7 @@ func (obj *Conn) PackagesToPackageIDs(packageMap map[string]string, filter uint6
}
state := packageMap[pkg] // lookup the requested state/version
if state == "" {
return nil, fmt.Errorf("Empty package state for %v", pkg)
return nil, fmt.Errorf("empty package state for: `%s`", pkg)
}
found[index] = true
stateIsVersion := (state != "installed" && state != "uninstalled" && state != "newest") // must be a ver. string
@@ -794,9 +799,9 @@ func (obj *Conn) PackagesToPackageIDs(packageMap map[string]string, filter uint6
// to be done, and if so, anything that needs updating isn't newest!
// if something isn't installed, we can't verify it with this method
// FIXME: https://github.com/hughsie/PackageKit/issues/116
updates, e := obj.GetUpdates(filter)
if e != nil {
return nil, fmt.Errorf("Updates error: %v", e)
updates, err := obj.GetUpdates(filter)
if err != nil {
return nil, errwrap.Wrapf(err, "updates error")
}
for _, packageID := range updates {
//obj.Logf("* %v", packageID)
@@ -844,9 +849,9 @@ func (obj *Conn) PackagesToPackageIDs(packageMap map[string]string, filter uint6
if obj.Debug {
obj.Logf("PackagesToPackageIDs(): Recurse: %s", strings.Join(checkPackages, ", "))
}
recursion, e = obj.PackagesToPackageIDs(filteredPackageMap, filter+PkFilterEnumNewest)
if e != nil {
return nil, fmt.Errorf("Recursion error: %v", e)
recursion, err = obj.PackagesToPackageIDs(filteredPackageMap, filter+PkFilterEnumNewest)
if err != nil {
return nil, errwrap.Wrapf(err, "recursion error")
}
}
}
@@ -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-2023+ 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
@@ -295,33 +295,25 @@ func (obj *PasswordRes) CheckApply(apply bool) (bool, error) {
// Cmp compares two resources and returns an error if they are not equivalent.
func (obj *PasswordRes) Cmp(r engine.Res) error {
if !obj.Compare(r) {
return fmt.Errorf("did not compare")
}
return nil
}
// Compare two resources and return if they are equivalent.
func (obj *PasswordRes) Compare(r engine.Res) bool {
// we can only compare PasswordRes to others of the same resource kind
res, ok := r.(*PasswordRes)
if !ok {
return false
return fmt.Errorf("not a %s", obj.Kind())
}
if obj.Length != res.Length {
return false
return fmt.Errorf("the Length differs")
}
// TODO: we *could* optimize by allowing CheckApply to move from
// saved->!saved, by removing the file, but not likely worth it!
if obj.Saved != res.Saved {
return false
return fmt.Errorf("the Saved differs")
}
if obj.CheckRecovery != res.CheckRecovery {
return false
return fmt.Errorf("the CheckRecovery differs")
}
return true
return nil
}
// PasswordUID is the UID struct for PasswordRes.
@@ -330,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()},
@@ -355,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-2023+ 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-2023+ 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/>.
//go: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")
}
}

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