239 Commits

Author SHA1 Message Date
James Shubin
e40819d617 modules: Add a small stub for CUPS
This is definitely not perfect, but it's a simple stub which we can
expand on.
2025-01-31 00:52:24 -05:00
James Shubin
7331d3a7ee util: pprof: Improve pprof for ease of use
This makes it slightly cleaner.
2025-01-31 00:00:05 -05:00
James Shubin
95f353c6a4 modules: virtualization: Add some simpler helpers
You don't need much to build a vm host. Here's the start.
2025-01-30 23:04:12 -05:00
James Shubin
5044ef4e8a engine: resources: Add a virt builder password selector
This is very helpful for debugging, particularly with broken Debian
installs.
2025-01-30 22:56:24 -05:00
James Shubin
3c61d088ab test: Make sure to test the docs
I can't believe I forgot to enable this earlier.
2025-01-29 12:18:55 -05:00
James Shubin
315a493565 lang: Add a few more tests 2025-01-26 19:48:17 -05:00
James Shubin
6268b61a7d lang: core: Lookup function (with default) can be more precise 2025-01-26 19:12:13 -05:00
James Shubin
3f202c6a7a lang: core: Fix struct lookup corner case
We forgot to reject this corner case which could lead to a runtime error
since the expected type from the incoming struct would not match what
we're handling.
2025-01-26 19:12:13 -05:00
James Shubin
d46c43df5a lang: core: Let lookup function specialize earlier
If we happen to know some information, we can specialize early and help
type unification solve things.
2025-01-26 18:21:54 -05:00
James Shubin
1538befc93 lang: ast, parser: Allow calling anonymous functions
I forgot to plumb this in through the parser. Pretty easy to add,
hopefully I didn't forget any weird corner scope cases here.
2025-01-26 17:21:11 -05:00
James Shubin
1af334f2ce misc: Update to golang 1.23
The 1.22 version has some issues that makes it more difficult to
workaround, so we'll move right away to 1.23
2025-01-26 17:17:02 -05:00
James Shubin
d30ea571f1 misc: Update to golang 1.22
Ran:

go get -u ./...
go mod tidy

We also got rid of travis and simplified things a bit.
2025-01-26 17:16:40 -05:00
James Shubin
d30ff6cfae legal: Remove year
Instead of constantly making these updates, let's just remove the year
since things are stored in git anyways, and this is not an actual modern
legal risk anymore.
2025-01-26 16:24:51 -05:00
James Shubin
1d3f2dbe3c util: Add a pprof helper package
As it turns out, there are other helper packages out there. I wrote this
before I realized that, and it didn't take me that long anyways.
2025-01-24 03:06:31 -05:00
James Shubin
ca6e7ad432 lang: types, lang: core: strings: Add a new join_nonempty function
Along with a helper function for when we need a []string from a Value.
2025-01-24 00:07:23 -05:00
James Shubin
f92afe9ae4 modules: meta: Add a router meta module
Useful for setting up simple routers. Some auto-provisioning polish
would definitely help, but this is pretty useful already.
2025-01-18 01:22:33 -05:00
James Shubin
483cc22c32 lang: core: embedded: provisioner: Add IPXE support
This lets you boot from ipxe. You can run the ipxe shell from their
stock image or the netboot.xyz one. For the latter, press "m", then type
"dhcp" (machine is now pingable!) then type "route" to check the ip.

To boot type:

chain http://192.168.42.1:4280/menu.ipxe

and you're off!

Thanks to frebib for finding the workaround to the VFS bug. The answer
is you need to run the imgfree command to unblock the initrd.
2025-01-18 01:07:19 -05:00
James Shubin
2f3bd72491 lang: core: embedded: provisioner: Fix regression in Fedora 41
Unfortunately with Fedora 41 and DNF5, this breaks what our host machine
expects. So you now need dnf5+ to make this work, or you can revert this
commit.
2025-01-18 01:02:17 -05:00
James Shubin
6499fcb1e0 modules: dhcp: Support the authoritative setting
Quite useful if we plumb it all the way through!
2025-01-18 00:30:46 -05:00
James Shubin
12a0600d38 engine: resources: Improve hostname change message
Make it clearer where empty strings are, and that it already happened.
2025-01-17 23:58:26 -05:00
James Shubin
cace2bacb8 lang: core: embedded: provisioner: Add support for module path
This is needed for bigger code bases. Remember: this is consulted at the
deploy stage, and the deploy contains the entire tree (including
modules) of everything it needs to run. This is why you don't need to
add a --module-path arg when running mgmt in systemd with the "empty"
frontend.
2025-01-17 23:54:30 -05:00
James Shubin
05d440114a lang: core: embedded: provisioner: Remove stale comment 2025-01-17 23:49:21 -05:00
James Shubin
b392285e1d gapi, lang: gapi: Make dir to avoid errors
It seems we sometimes need to make the intermediate dir.
2025-01-17 23:32:13 -05:00
James Shubin
a713c08585 engine: resources: Make net and firewalld resources quieter
They are too noisy!
2025-01-17 23:32:13 -05:00
James Shubin
8e8e831e73 modules: misc: Switch type to list of strings
The DNS entry should be a list of strings. We would have caught this
earlier but this helped us find a type unification issue.
2025-01-17 23:28:05 -05:00
James Shubin
86b95b2c0b modules: shorewall: Fix type on stoppedrules file 2025-01-17 22:12:55 -05:00
James Shubin
4a578ca40c lang: core: embedded: provisioner: Handoff a hostname
This makes it easier to run config tools if the hostname is already set.
2025-01-17 19:48:23 -05:00
James Shubin
a60148f370 lang: core: net: Add more ip utilities 2025-01-17 18:58:21 -05:00
James Shubin
00366de67b modules: purpleidea: Update my personal packages 2025-01-17 18:57:18 -05:00
James Shubin
a08ba0b0e9 engine: resources: Tar now accepts dirs without a trailing slash
If these are found, then the dir path itself is copied in as well.
2025-01-17 18:15:45 -05:00
James Shubin
81b102ed7f lang: ast: Allow multiple star imports
If more than one star import is present in the same scope, allow it. If
one star import could overwrite something, ordering is not guaranteed.
We allow this for now, but we might create a compiler fix to stop it.
This adds a test to notice both of these behaviours.
2025-01-17 14:03:48 -05:00
James Shubin
c8f911ec5d lang: core: embedded: provisioner: Fix panic
The conditions changed for this one, make it stricter.
2025-01-17 12:35:30 -05:00
James Shubin
7694da4241 lang: core: embedded: provisioner: Skip bmc if empty 2025-01-17 12:34:27 -05:00
James Shubin
a0d500a602 lang: interfaces: Remove dangerous init method
This can cause the source to get overwritten and changed and is usually
unnecessary.
2025-01-04 20:56:11 -05:00
James Shubin
553172992f lang: parser: Typo fix 2025-01-04 20:55:41 -05:00
James Shubin
e6d614f4dd engine: resources: Add a bmc resource
This resource manages bmc devices in servers or elsewhere. This also
integrates with the provisioner code.
2025-01-03 18:19:31 -05:00
James Shubin
3107dfbd08 modules: shorewall: Small fixups to improve the module
These are some common fixes and improvements for normal shorewall usage.
As we shake out more uses of this, we find small issues. This lets us
have long rules, and a better default config.
2025-01-02 15:43:27 -05:00
James Shubin
802823dcb0 modules: misc: Support VIP's in network config 2025-01-02 15:42:40 -05:00
James Shubin
5858c8b501 pgraph: Panic if vertex is nil
These should be caught early.
2024-12-18 13:49:13 -05:00
James Shubin
2561dba8f5 lang: Increase debugging timeout 2024-12-15 10:39:46 -05:00
James Shubin
f5806e0617 lang: funcs: structs: Add Call and CallStruct methods for composite
It might turn out the CallStruct is the API we want. This will depend on
the future iterations of the function engine.
2024-12-08 17:43:41 -05:00
James Shubin
e9dbb7b86c lang: funcs: structs: Add Call method for const 2024-12-08 16:48:04 -05:00
James Shubin
28f5b8331a lang: funcs: structs: Improve naming
These could print nicer for debugging.
2024-12-08 16:24:42 -05:00
James Shubin
5ff4f0456a lang: Add mode to overwrite tests
This is useful if we need to reformat a bunch of previously passing
tests.
2024-12-08 15:34:28 -05:00
James Shubin
82c614f2d9 engine: resources: Workaround broken debian package when building images 2024-12-06 15:59:39 -05:00
James Shubin
50265d2303 engine: resources: Pull out a distro function for guest 2024-12-06 15:58:51 -05:00
James Shubin
ecee84aa28 docs: New blog post about modules and imports 2024-12-03 03:00:21 -05:00
James Shubin
2e146e8c8e engine: resources: Fix up some issues with cron
This really needs looking at again, since it's so old and buggy or
broken.
2024-12-03 01:52:23 -05:00
James Shubin
097efdd66a engine: graph: autoedge: Clean up redundant logs
They repeat themselves, this is cleaner.
2024-12-03 00:56:22 -05:00
James Shubin
5764c977f1 modules: misc: Don't ignore the router setting
This mistake caused us to ignore the router setting when we wanted it!
Woops =D
2024-12-03 00:43:32 -05:00
James Shubin
4d30772b3b lang: Add test for unused include statements
Even if they have no side effect, they aren't legal, since it would be
surprising if suddenly something new got added in one which then broke
the imports.
2024-11-28 19:17:33 -05:00
James Shubin
8472b1ebf2 lang: gapi: Convenience the user by allowing relative dirs in cli 2024-11-28 19:17:17 -05:00
James Shubin
e1070d3e13 lang: ast, download: Improve error messages 2024-11-28 19:17:17 -05:00
James Shubin
98d7f294eb lang: download: Improve git reliability
Fix a small panic that could happen if we had a bad clone, and make
visibility into this operation better. We also make room for future
context cancellation since the library now supports this.
2024-11-28 16:38:33 -05:00
James Shubin
517fc1e05b lang: gapi: Remap the module path correctly
If we've set the --module-path arg, then we expect to rebase that path
out of the deploy, and instead it should show up as /modules/ which is
the standard. Handle this scenario.
2024-11-28 16:29:33 -05:00
James Shubin
c2f75d64a6 util: arch: The Any value should be the same everywhere
In 80e8c9cadc when this was ported, the
"Any" value diverged accidentally. This would cause some packages to not
be found, since they didn't match any arch.

Thanks to karpfen to digging into the issue.
2024-11-28 14:55:30 -05:00
James Shubin
380004b1dc readme, docs: New docs available 2024-11-23 01:25:40 -05:00
James Shubin
28a443d11d docs: Add a hack for golang functions 2024-11-22 14:20:24 -05:00
James Shubin
a600e11100 cli, docs: Add a docs command for doc generation
This took a lot longer than it looks to get right. It's not perfect, but
it now reliably generates documentation which we can put into gohugo.
2024-11-22 14:20:16 -05:00
James Shubin
7b45f94bb0 lang: core: Remove the unnecessary func suffix
We don't really need these, it's clear what things are.
2024-11-22 01:18:19 -05:00
James Shubin
acdd6476f2 test: Remove empty variable
Copy-pasta bug!
2024-11-21 23:49:32 -05:00
James Shubin
018d3efc90 lang: funcs: Move standalone functions into core
Everything should be all together.
2024-11-21 22:56:17 -05:00
James Shubin
b40d10a366 util: Add a generic map key and value swapping function
Fun little utility function which is useful.
2024-11-21 02:54:10 -05:00
James Shubin
a88034ab06 modules: misc: Add standard header 2024-11-20 23:47:17 -05:00
James Shubin
907d2ad1a1 modules: dhcp: Add an mgmt module for managing dhcpd
This is not perfect, but it's a good start, and it shows how a module
might be structured.
2024-11-18 15:12:01 -05:00
James Shubin
3bd6986fde modules: shorewall: Add an mgmt module for managing shorewall
This is not perfect, but it's a good start, and it shows how a module
might be structured.
2024-11-18 15:11:31 -05:00
James Shubin
43bd847bad modules: misc: Improvements on ip address setting 2024-11-08 14:12:02 -05:00
James Shubin
0c0583adc8 modules: misc: Add network manipulation helpers
This is common functionality which we might want to use on new machines.
2024-11-06 22:13:31 -05:00
James Shubin
c642b5eeae lang: core: net: Add new function to get cidr prefix 2024-11-06 21:10:04 -05:00
James Shubin
69e84fbbed engine: resources: cron: Add ctx where possible.
Lots of the API's here now support this. Here's an example, work on the
others too.
2024-11-06 21:09:50 -05:00
James Shubin
f8b06f32ec engine: resources: Remove unused wait group 2024-11-06 21:09:50 -05:00
James Shubin
59a20f53eb lang: core: sys, engine: resource: Update hostname functionality
We didn't have a solid resource and sys.hostname() didn't have events!
2024-11-06 21:09:50 -05:00
James Shubin
83fd8b7e54 engine: util: Add more cmp utility functions 2024-11-06 20:02:10 -05:00
James Shubin
098ab20ec9 lang: gapi: Duplicates are possible if we have a diamond dag
Allow this, just remove them...
2024-11-06 20:02:10 -05:00
James Shubin
a2ce9e890d lang: core: net: Add a way to get the machine mac addresses 2024-11-05 14:55:58 -05:00
James Shubin
be7a5399e3 lang: core: util: Add hostname mapper function
This adds a new util package with some useful functionality which could
be implemented as pure mcl, but instead we add it here as a good place
to help with code reuse.
2024-11-05 14:55:58 -05:00
James Shubin
3fb492f6aa util: Add a TLS helper
Make it easier to build TLS stuff in pure golang.
2024-11-01 19:41:35 -04:00
James Shubin
e4f062b006 engine: resources: Parse distro properly
I regressed here when patching this. Here's the fix. Would be beautiful
to have hardware to run end-to-end testing on. If you want to sponsor
this, please let me know!
2024-10-30 16:04:01 -04:00
James Shubin
422719c345 lang: core: map: Add functions to extract keys and values
Simple stuff, but now it's done!
2024-10-30 00:58:10 -04:00
James Shubin
71a1efde99 examples: tls: Add a simple TLS example
Was useful for testing things...
2024-10-30 00:41:26 -04:00
James Shubin
ed84c5460c lang: core: embedded: provisioner: Workaround bad mirrors
With the release of Fedora 41, I was getting lots of mirror errors.
Hopefully this helps make it more robust. It was failing repeatedly
while trying to download packages, and I kept having to restart things,
but once I added this option things worked. Hopefully they're related.
2024-10-29 18:53:58 -04:00
James Shubin
0222a682fc lang: core: embedded: provisioner: Not sure we need this package
Seems to cause some issues. Remove it for now.
2024-10-29 18:53:36 -04:00
James Shubin
1cd4af5838 lang: core: embedded: provisioner: Keep this message separate
Don't autogroup it with others.
2024-10-29 16:58:06 -04:00
James Shubin
d1aaf6e82b lang: core: embedded: provisioner: Handle spurious failures
Not sure why this happen, I think it's just random network blips. Simple
retry should be used for now.
2024-10-29 16:58:06 -04:00
James Shubin
52a71f9515 lang: core: embedded: provisioner: Add unused repo generation
I was playing around with generating repo's, but they didn't turn out to
be needed at this time. Committing this anyways for future reference.
2024-10-29 16:58:06 -04:00
James Shubin
3c665174cc lang: core: embedded: provisioner: Implement handoff
Here's a good first way to implement handoff. What's particularly
elegant about handoff here, is that this is the first form of it I know,
where handoff happens between a provisioning tool and a configuration
management tool and those are the same tool! As a result, this can allow
for some really elegant integration, and the end-user never has to deal
with the combinatorial explosion of the N * M scenario of gluing each
provisioning tool to each different configuration management tool.

We'll have other forms of handoff in the future, but this simple
approach is useful already.
2024-10-29 16:58:06 -04:00
James Shubin
93eb8b2b76 lang: core: embedded: provisioner: Host an available deploy
This makes the current deploy available. This is likely not useful when
this is used from the embedded provisioner cli tool, since it would
contain cli functions that won't run, but it is useful when it's used as
a library.
2024-10-29 16:42:15 -04:00
James Shubin
1692235498 lang: core: embedded: provisioner: Log output of post
This is useful for debugging and for knowing what we really did to the
machine.
2024-10-29 16:42:15 -04:00
James Shubin
a6bcd4b92b lang: core: embedded: provisioner: Reduce amount of log noise
We don't need to refresh the leases so often.
2024-10-29 16:42:15 -04:00
James Shubin
d065cddf5e lang: core: embedded: provisioner: Remove old package
Doesn't seem to be used for anything at the moment.
2024-10-29 16:42:15 -04:00
James Shubin
20d4809e8e engine: resources: Print netmask nicely for our DHCP resource
Makes it easier to see what's going on.
2024-10-29 16:42:15 -04:00
James Shubin
b074386c26 cli: Add setup and firstboot commands
This adds two new top-level commands: setup and firstboot.

Firstboot is pure-golang implementation of a service that runs some
commands once when a system first boots. You need to install this
service, and put the scripts to run in a special directory. This is
inspired by the virt-builder --firstboot mechanism.

Setup is a general purpose command that makes it easy to setup certain
facilities on a new machine. These include the mgmt package dependencies
it might need, a service to run it from, and the necessary service to
use the mgmt firstboot service as well.

All of this has been built to facilitate handoff between provisioning a
new machine and running configuration management on it.
2024-10-29 16:42:15 -04:00
James Shubin
b140b2dfeb util: Move executable path finding into a helper function 2024-10-29 16:41:37 -04:00
James Shubin
8e3d959500 util: We prefer to append rather than truncate
This makes this utility function more useful.
2024-10-29 16:41:37 -04:00
James Shubin
8c886bbe7c util: Nil input to our simple cmd helper should be allowed
Don't panic here!
2024-10-29 16:41:37 -04:00
James Shubin
7d204dfb74 util: Add a simple append file write function
Similar to the golang os lib version except we append.
2024-10-29 16:39:46 -04:00
James Shubin
583f90dc7b util: distro: Rename functions to avoid golang stutter warning 2024-10-25 02:56:12 -04:00
James Shubin
85e1d6c0e8 engine: resources: Make sure to set the netmask
Some clients would DECLINE if this was not set. This was reproduced my
using the stock coredhcp DHCPv4 server and disabling the netmask plugin.
One of the clients that would DECLINE is a Lenovo ThinkCentre m90n doing
a UEFI (PXE) netboot.

This was found in 1327752725 and is
hopefully now completely fixed!
2024-10-25 00:57:50 -04:00
James Shubin
2c967e3897 util: Add a simple template system for systemd unit files
Just the basics for what we need, nothing more. Not intended as a
general-purpose library for use elsewhere.
2024-10-24 17:29:00 -04:00
James Shubin
202a8e1fba util: Add a small helper to exec commands simply 2024-10-18 10:02:29 -04:00
James Shubin
e6085d77ff util: Add an flock utility for lock file type things
Useful for ensuring only one binary runs at the same time.
2024-10-18 10:02:29 -04:00
James Shubin
10f82c6566 lang: core: list: Add a concat function
It works with arbitrary numbers of arguments too!
2024-10-18 10:02:29 -04:00
James Shubin
3d11b2caaf lang: core: deploy: Add a function to help in obtaining bootstrap deps
This should make it easier to implement handoff.
2024-10-15 20:54:33 -04:00
James Shubin
f8037a1f99 lang: types: Add a small helper function for common type conversions 2024-10-15 20:53:39 -04:00
James Shubin
067eef9007 util: distro, engine: resources: Update virt-builder res
A little easier to maintain if we support more distros eventually.
2024-10-15 20:36:51 -04:00
James Shubin
e45d9be065 util: distro: Parse the os-release file 2024-10-15 20:36:51 -04:00
James Shubin
d24149518c util: distro: Refactor family and distro code
I hate writing abstraction code like this, but I'm hoping it will be
useful.
2024-10-15 20:36:50 -04:00
James Shubin
d403f18b2a util: distro: Put distro specific data in this util package
Try and see if we can put all our distro specific stuff in here...
2024-10-15 19:04:17 -04:00
James Shubin
1f12150d8f engine: resources, examples: lang: Expand on future deploy ideas
This is one idea for reference. But I doubt this is needed anytime soon
since we have a good working solution in the examples.
2024-10-13 20:47:47 -04:00
James Shubin
d3a7cefcc6 engine: resources: Add an archiving, deploy resource
This makes a bundle out of the code in the current deploy. Hopefully
this is useful for handoff!
2024-10-13 16:44:23 -04:00
James Shubin
a8c8f09aa3 gapi: Plumb through a URI mechanism
This is at least a stop-gap until we redo the whole filesystem API mess.
I think golang is partly to blame because they don't have proper API's
merged yet.
2024-10-13 16:40:50 -04:00
James Shubin
b03fdeccae lang: interfaces: Nil input means no args 2024-10-07 00:00:19 -04:00
James Shubin
6c12e8a29b lang: gapi: The module path needs separate rebasing
If we're using a module path which doesn't share the same root, rebase
it separately.
2024-10-07 00:00:19 -04:00
James Shubin
310452542b lang: gapi: Rebase to the common path prefix
When the modules dir is not within the main base, we don't correctly
choose the common base path. As a result, we should choose something
common for our internal path representation.
2024-10-05 01:02:03 -04:00
James Shubin
b514022713 util: Add some path manipulation algorithms
These could use some optimization by an algorithmist! Not urgent right
now since they're not currently in any fast paths in the code.
2024-10-05 01:02:03 -04:00
James Shubin
c937280664 engine: local, lang: core: local: Add a pool function
This adds a new local API for pool allocation, and with it a
corresponding function in the "local" import.
2024-10-04 22:37:39 -04:00
James Shubin
898b58e3e7 lang: core: strings: Add a substring function named substr 2024-10-03 13:49:08 -04:00
James Shubin
74119a0a53 lang: core: strings, util: Add left and right padding functions
Golang didn't want these in the standard library, but they are useful.
2024-10-03 13:49:08 -04:00
James Shubin
d6914d3437 lang: core: net: Improve formatting of the mac functions 2024-10-03 13:49:08 -04:00
James Shubin
fdfa03685c lang: core: net: Add is_mac function 2024-10-02 14:31:20 -04:00
James Shubin
149a85fcde modules: misc: Ensure the ssh key has a folder 2024-09-29 00:53:11 -04:00
James Shubin
65f26769ae lang: core: net: Add a cidr to mask function
And tidy things up slightly to remove buggy tests.
2024-09-28 22:54:06 -04:00
James Shubin
6397c8f930 lang: Clean up import logs
We get a lot of useless noise here, make it neater.
2024-09-28 21:56:53 -04:00
James Shubin
761030b5b8 modules: misc: Make the ssh keygen module more useful 2024-09-26 12:52:27 -04:00
James Shubin
9a752da13d modules: misc: Add a small helper module
Maybe I'll collect enough small snippets that I can keep them in here
until they get split out elsewhere to more appropriate places.
2024-09-26 12:43:26 -04:00
James Shubin
13fc711657 util: Expanding home directory should preserve trailing slash
Because we consider slashes as a directory identifier when needed.
2024-09-26 12:29:43 -04:00
James Shubin
6419f931ee engine: resources: Add a virt-builder resource
This wraps the excellent virt-builder utility which makes setting up new
virtual machines a breeze.
2024-09-26 11:47:40 -04:00
James Shubin
562138cb74 lang: core: os: Add expand_home function
Simple helper that works with ~/foo/ or ~james/foo/ type patterns.
2024-09-25 20:31:18 -04:00
James Shubin
8aac770bcb engine: resources: pkg: Add a small helper for simple installs
Add this utility function for doing single package installs.
2024-09-25 16:26:32 -04:00
James Shubin
80e8c9cadc util: arch: Use small arch util library
Put all of the arch stuff that we can into this library.
2024-09-25 16:25:55 -04:00
James Shubin
87b3dda867 readme: Fixup typo
Reported in GH#775.
2024-09-19 13:22:00 -04:00
James Shubin
b9e093cd6b engine: resources: svc: Reduce unnecessary logging 2024-09-18 21:47:29 -04:00
James Shubin
06a023ca66 engine: resources: sysctl: Be more careful about dir path
More validation is not bad!
2024-09-18 21:39:33 -04:00
James Shubin
ccb4c6244d engine: resources: exec: Improve the docs for a common scenario
I knew this, and now you know it too!
2024-09-18 21:39:03 -04:00
James Shubin
4489e5ce6e engine: graph: autoedge: Quiet down the useless logs
These are so useful, let's silence them.
2024-09-18 21:38:29 -04:00
James Shubin
8df82f0301 docs: Add a new faq entry about deploy.readfile
This may be a common thing people forget.
2024-09-18 21:38:08 -04:00
James Shubin
57b4a7efce lib, engine: graph: Let children directories be readable
We want to be able to put useful scripts in $vardir type places, but if
the perms at the higher levels block this, then that can't work. The
top-level should always be more permissive, and then it grows more
restricted as we descend.
2024-09-18 21:03:58 -04:00
James Shubin
fd508fbc0d engine: resources: Fix typo in svc
It's a typo, right?
2024-09-17 18:10:13 -04:00
James Shubin
a4f368fc9f engine: resources: Add a sysctl resource
Very useful since these are easy to forget!
2024-09-15 23:07:03 -04:00
James Shubin
e7b57a32fd engine: resources: Add a tar resource
This makes tar archives from a list of files and/or directories. It
correctly includes empty directories as well. The code structure is
similar to the gzip resource. While this resource is arguably more
useful than gzip, it was invaluable to write the gzip resource first
since that made writing this one much easier.
2024-09-13 20:04:53 -04:00
James Shubin
06cc63fcb6 util: recwatch: Add a helper function for merging these
I should really rework the recwatch package and API, but I wasn't in the
mood to touch this code today, so this will have to do for now.
2024-09-13 19:48:00 -04:00
James Shubin
e34212a10b engine: resources: gzip: Check unhandled error
This is probably inconsequential, but let's do it since it's not in a
defer.
2024-09-13 19:48:00 -04:00
James Shubin
5f6e07b5e8 engine: resources: gzip: Fix typo 2024-09-13 16:24:01 -04:00
James Shubin
1465c5cdc9 engine: resource: gzip: Remove unneeded waitgroup
I think this was a copy+pasta mistake.
2024-09-13 16:13:03 -04:00
James Shubin
29eebd0d07 lang: core: Move template to golang namespace
I don't think this template function should be in any way authoritative,
so let's namespace it.
2024-09-13 15:51:24 -04:00
James Shubin
5bbc06d8bc engine: resources: Add new gzip resource
This may have lots of uses, particularly for bootstrapping and handoff
if we want to compress payloads. It is also a good model resource for
how to implement such a resource to avoid re-computing the result on
every CheckApply call. Of course if the computation is cheaper than the
hashing of the data this isn't the optimal approach.
2024-09-13 03:32:10 -04:00
James Shubin
9a5f6a5bd3 lang: core, funcs: Use the correct zero type
I wasn't using the correct contained type here.
2024-09-10 23:23:00 -04:00
James Shubin
2e774215e4 lang: core: deploy: Add a function to get the binary path
Useful for bootstrapping new machines.
2024-09-10 23:22:53 -04:00
James Shubin
1327752725 engine: resources: Special log message for unhandled decline
Not sure why a PXE client is sending these... Not sure if it's buggy
firmware or my inability to handle a DHCP corner case.
2024-09-10 23:22:47 -04:00
James Shubin
118f266211 lang: core: local: Add a new vardir function
This gives us a function to return a created vardir folder. It is not
locally namespaced, and a future function will have to namespace one to
each scope.
2024-09-09 18:04:18 -04:00
James Shubin
87a2dfc8f9 engine: local: Add a vardir API to our local API collection 2024-09-09 17:41:12 -04:00
James Shubin
b88ac4603f lang: interfaces Add CallableFunc interface
Add a new interface for callable functions. This will likely be useful
for future versions of the function engine and for the "timeless" work.
2024-09-09 15:49:57 -04:00
James Shubin
28e81bcca3 modules: Add a modules directory for mcl code
Details in the README file.
2024-09-09 15:14:33 -04:00
James Shubin
3d0660559e examples: lang: Add a join example
Good reminder that a lot of the golang stdlib functions are available.
2024-09-09 14:54:45 -04:00
James Shubin
48dc9ad099 test: shell: Disable another flaky test
We need to fix these all eventually, but that day is not today.
2024-09-06 16:24:28 -04:00
James Shubin
fd3a2a1f0f engine: resources: Make consul optional
Licensing has made this non-free. Let's put that behind a build tag for
now, and remove it entirely if no suitable libre replacement is found.
2024-09-03 20:26:38 -04:00
James Shubin
c6e9175e3f engine: resources: Add missing build tag 2024-09-03 20:21:31 -04:00
James Shubin
1a39472734 lang: core: embedded: provisioner: Sometimes this is used
We need better overview of all the PXE/netboot stuff, probably we should
read a spec, but until an expert comes along, we'll have to proceed
incrementally.
2024-09-03 14:48:38 -04:00
James Shubin
bfa88e9b1c engine: resources: Workaround regression in wget2
Apparently wget2 has a serious regression that the HTTP 102 header
throws it off... So let's not send this for now... I'm pretty unhappy
about this, wget used to always be rock solid. Maybe curl deserves a
chance? (This works fine with curl btw.)
2024-08-30 20:33:41 -04:00
James Shubin
a0972c0752 lang, engine: Add a metaparam for catching accidental dollar signs
Let's make our life easier for users!
2024-08-22 20:41:48 -04:00
James Shubin
8dc0d44513 lang: Add an extra fail scenario to our test suite
Let's us write tests for Validate failures.
2024-08-22 20:12:59 -04:00
James Shubin
8594b6e2a9 lang: funcs: Hint the struct_lookup functions better
If we have static information, use it to help unification.
2024-08-21 19:00:51 -04:00
James Shubin
82cac572ca lang: core: fmt: Allow type unification variables for format
This allows some simple cases.
2024-08-21 19:00:50 -04:00
James Shubin
da4f69cd87 lang: ast, core: fmt: Allow unification variables for fmt
This lets us pass through unification variables into the fmt function. I
hope this doesn't break anything, but it's worth trying for now.
2024-08-21 18:52:24 -04:00
James Shubin
e6cb776eb6 lang: ast, core: fmt: Catch invalid nil signatures
We accidentally had a bad error triggered.
2024-08-21 18:50:11 -04:00
James Shubin
7557114b4e lang: ast: Don't send empty ord names for partials
We would accidentally send some empty partials, woops! This reinforces
my belief that we should never pre-allocate list size unless we notice a
performance issue.
2024-08-21 18:00:44 -04:00
James Shubin
001e1a5da0 lang: Remove some error wrapping
Makes errors cleaner to read. The extra context wasn't very helpful.
2024-08-18 19:07:27 -04:00
James Shubin
6f3c3c318b lang: core: Shorten functions with wrapper
This demonstrates how to write a function with the wrapper. Note that
you must not include Init if you're not calling the nested wrapper
function.
2024-08-18 18:29:01 -04:00
James Shubin
654e376be7 lang: core: Add list and map packages
Put the common functionality for those types in there.
2024-08-18 18:28:26 -04:00
James Shubin
211121cdca lang: funcs: Use correct constant 2024-08-18 17:31:58 -04:00
James Shubin
f2d4cac92d docs: Add a short contributing guide
I think this is all common sense, but I thought it might be helpful for
anyone that might not be well-versed with how such projects run.
2024-08-16 23:57:38 -04:00
James Shubin
c5dc9c7650 docs: Add a guide for writing API services
Hopefully this is useful to companies who want to design their services
properly to support modern tooling.
2024-08-16 23:38:27 -04:00
James Shubin
7596f5b572 lang: core: os: Add family functions and variables
Make it easier to do os-specific stuff.
2024-08-07 17:30:15 -04:00
James Shubin
8e9c3b6c1e lang: funcs: vars: Include system package variables in the scope 2024-08-07 17:30:13 -04:00
James Shubin
a93c98402a lang: ast: Add better logging about scope issues
This may help out programmers who aren't sure what's going on.
2024-08-07 17:17:57 -04:00
James Shubin
b04ee4ba22 lang: ast: Pass through the data field for vars 2024-08-07 17:17:57 -04:00
James Shubin
65b104ea55 lang: ast: Split off helpers into util file 2024-08-07 17:17:57 -04:00
James Shubin
562eb643fc engine: resources: Display bytes copied when making a file 2024-08-06 15:24:45 -04:00
James Shubin
80178422db engine: graph, resources: Clean up log messages
The idea is to have a better user experience in the terminal.
2024-08-06 15:12:10 -04:00
James Shubin
e94f39bf2c engine: resources: Cleanups to the svc resource
Some new API's exist that take a context now too!
2024-08-06 14:31:39 -04:00
James Shubin
6c1a33066a engine: resources: The svc resource should reload on notification
Missing feature that is finally landing. I wish this wasn't needed, but
we need fancier plumbing to avoid it.
2024-08-06 14:22:15 -04:00
James Shubin
beca0c3ae6 engine: resources: Plumb through the context and constants
Basically a small cleanup.
2024-08-06 14:13:39 -04:00
James Shubin
7517c83953 engine: resources: Add the systemd service constants
Basically a cleanup to avoid duplicate strings everywhere. This makes it
easier to follow the code too.
2024-08-06 14:02:17 -04:00
James Shubin
0354082f89 engine: resources: Allow symbolic modes for missing files
In 83a747794e a bug was introduced with
the implementation of symbolic modes, that would prevent a file resource
from passing the Validate step if you were using a symbolic mode, and
the file didn't already exist. If you didn't use symbolic modes and
those files weren't absent, then you wouldn't have noticed.

It might be worth looking into the API for symbolic parsing as well.
2024-08-06 13:24:25 -04:00
James Shubin
4abcd9cf01 lang: core: Quiet down the template function by default
We don't need to know this most of the time.
2024-08-01 20:54:55 -04:00
James Shubin
c974820c56 engine: resources: Add log messages for chmod and chown 2024-08-01 20:32:56 -04:00
James Shubin
88670ae7a1 engine: resources: Improve output of log messages
I don't remember ever having this display a pointer address, but it is
now, so let's make this cleaner.
2024-08-01 18:44:44 -04:00
James Shubin
d0ed004b24 examples: lang: Test that each of the mcl examples compiles
We let these rot, so fixup the issues and test them!
2024-07-31 17:29:42 -04:00
James Shubin
6de7d8b254 lang: funcs: Catch non-specific type build error
If you had ambiguous code, and specified an invalid type, this could
sneak through and become a runtime error, instead of a compile-time
error. We fix this and add a test.
2024-07-31 17:29:42 -04:00
James Shubin
bfb5d983c1 lang: types, unification: Don't recurse into private fields
We forgot to omit looking deeper into private struct fields. I don't
know why we didn't catch this earlier, I can only assume some subtlety
changed, since we've previously used many of the resources this would
fail on. Maybe golang broke some API that they didn't consider stable?

This also adds a new test for this, and ensures each resource can be
inspected too!
2024-07-31 17:29:42 -04:00
James Shubin
0a183dfff9 lang: funcs: txn, util: Fix typos 2024-07-25 12:44:58 -04:00
James Shubin
8b54306eb9 examples: lib: Fix these rotted tests
I think the mgmt lib approach is a good idea, even though I'm not
putting much energy into keeping these up to date. Let's at least
re-enable the tests for now, after a few fixups.
2024-07-25 12:39:43 -04:00
James Shubin
fd86b35ce3 docs: Improve the FAQ 2024-07-23 17:26:57 -04:00
James Shubin
d9f8dd53c1 test: Add comment explaining the line length rule issue better 2024-07-23 17:26:18 -04:00
Omar Al-Shuha
ccb0e55d5a examples: lang: Fix env0 example
Change function calls to the correct
one, remove extra argument in getenv
call, and fix typo.
2024-07-08 02:22:57 +02:00
James Shubin
74f747e80b util: password: Fix suspicious dep issue
It seems that without warning, the author of this dep has nuked the old
version, and reorganized the source tree significantly. I'm not an
expert and cryptography routines, but this doesn't make me feel warm
inside. I hope more expert researchers could look into this so that we
avoid supply chain attacks.
2024-07-07 12:47:14 -04:00
James Shubin
aa03b5ce2f lang: core: iter: Add filter iterator function
This was fun to write and adds a new core iterator function.
2024-07-03 21:25:19 -04:00
James Shubin
e747e12002 examples: lang: Fixup a few examples
We might change unification to allow naked single strings with fancier
unification, but let's leave it as is for now and see how often it comes
up.
2024-07-02 23:50:24 -04:00
James Shubin
d1753c592a lang: core: iter: Misc formatting fixes
Also fix up the examples.
2024-07-02 23:49:56 -04:00
James Shubin
7a35bef7ac test: Improve comment parser to skip code blocks
It might be nicer to have some code blocks all by themselves on a single
line.
2024-07-02 13:24:55 -04:00
James Shubin
e10e92596f lang: types: Add stringer information manually
This lets us get the more correct lowercase versions of type kinds in
error messages. (These match what the user would type.)
2024-07-01 18:35:20 -04:00
James Shubin
28253c4bd2 lang: Move stateful test objects into a per-test mode
Was this causing failures? Does this make things much slower?
2024-07-01 18:34:42 -04:00
James Shubin
f2976deb02 pgraph, lang: ast: Fix failing tests due to non-deterministic topo sort
This causes inconsistent type unification when running our tests. It's a
bad user experience too.
2024-07-01 18:34:24 -04:00
James Shubin
14577a0c46 lang: Add modern type unification implementation
This adds a modern type unification algorithm, which drastically
improves performance, particularly for bigger programs.

This required a change to the AST to add TypeCheck methods (for Stmt)
and Infer/Check methods (for Expr). This also changed how the functions
express their invariants, and as a result this was changed as well.

This greatly improves the way we express these invariants, and as a
result it makes adding new polymorphic functions significantly easier.

This also makes error output for the user a lot better in pretty much
all scenarios.

The one downside of this patch is that a good chunk of it is merged in
this giant single commit since it was hard to do it step-wise. That's
not the end of the world.

This couldn't be done without the guidance of Sam who helped me in
explaining, debugging, and writing all the sneaky algorithmic parts and
much more. Thanks again Sam!

Co-authored-by: Samuel Gélineau <gelisam@gmail.com>
2024-07-01 18:33:47 -04:00
James Shubin
4e18c9c67a lang: Plumb through the unified state facility 2024-07-01 16:07:14 -04:00
James Shubin
d326917432 lang: interfaces: Add some unification basics
This includes the GenericCheck helper which we'll use everywhere, and
the standard single invariant which we use throughout.
2024-07-01 16:07:14 -04:00
James Shubin
ad4eb86262 lang: unification: util: Add the core unification helpers
This adds the core unification helper functions that do the core work of
solving the invariants. This includes the actual Unify, OccursCheck, and
Extract which is sometimes known as "zonk".

A few other small functions are also included.

Co-authored-by: Samuel Gélineau <gelisam@gmail.com>
2024-07-01 16:06:58 -04:00
James Shubin
5c73e7c582 lang: types: Add a facility for printing consistent unification vars
When we look at unification variables from two different places, the
default printer will always start numbering them from ?1 and therefore
if we look at two unrelated systems, they might both print as ?1 when
they are in fact different pointers.

We don't collect them all by default since it's usually not necessary
except for debugging, but in those situations, we want a consistent
unification store which we can pass around to get sensible debug output.
2024-07-01 14:16:11 -04:00
James Shubin
dc33d9aab7 lang: types: Add a comparable helper to our types library 2024-07-01 14:14:20 -04:00
James Shubin
cdc2439f89 lang: types: Add an iterator helper to our types lib 2024-07-01 14:13:41 -04:00
James Shubin
318ee0d002 lang: types: Fixup small log messages 2024-07-01 14:12:22 -04:00
James Shubin
653299a88f lang: types: Plumb in a unification variable into our type
This is used for representing a unification variable in our type during
type unification. For example, this allows us to have a [?1] or a
map{?1:[?2]} and so on...
2024-07-01 14:11:03 -04:00
James Shubin
6066cbf075 util: Add disjoint package to implement a union find datastructure
This is a fascinating, and incredibly simple data structure. I hope I
can end up using it for more than just type unification!

Thanks to Sam who taught me about its existence.
2024-07-01 14:05:48 -04:00
James Shubin
2b3a41fefa pgraph: Fix rare panic if Sprint is badly used
Be nicer.
2024-07-01 14:05:48 -04:00
James Shubin
5ca9f7fa38 test: Skip link check by default
We should run this periodically or put it in a separate job, as it's
causing the tests to fail all the time. I expect those sites may be
blocking github as they see it as a DOS.
2024-07-01 13:40:14 -04:00
xlai89
201cf091d5 test: Add a links checker and fix some links 2024-06-17 14:17:39 -04:00
Cian Yong Leow
09e53bfd3f engine: resources: file: Remove Validate owner/group Checks
The owner/group of a file should not be validated on the host until runtime. This removes the checks in Validate() that were happening before the execution of the resource graph (and therefore bound to fail if the system was being bootstrapped).
2024-06-17 13:52:04 +01:00
James Shubin
3c661ab674 lang: core: embedded: provisioner: Version flag needs a unique name
Conflicts with the stock --version.
2024-05-23 07:57:30 -04:00
James Shubin
415e22abe2 lang: core, funcs, types: Add ctx to simple func
Plumb through the standard context.Context so that a function can be
cancelled if someone requests this. It makes it less awkward to write
simple functions that might depend on io or network access.
2024-05-09 19:25:46 -04:00
James Shubin
3b754d5324 docs: Fix markdown failing
It fails locally, but not in CI, and I don't know why.
2024-05-05 15:34:04 -04:00
James Shubin
7a568627e9 docs: Update dead links 2024-05-05 15:34:00 -04:00
James Shubin
328360eea8 docs: Add addition to style guide for pointer receivers 2024-04-28 16:14:05 -04:00
James Shubin
7ae3ba4483 mod: Run go mod commands
This was done with go get -u ./... followed by go mod tidy.
2024-04-27 13:31:32 -04:00
Joe Groocock
351a61c0cd engine: resources: docker: Update docker
Several types were renamed and moved

Signed-off-by: Joe Groocock <me@frebib.net>
2024-04-27 10:52:29 +00:00
James Shubin
c12452b3ce misc: Move to golang 1.21
Unfortunately, this also breaks go-mod-upgrade with:

upgrade failed error=Error running go command to discover modules: exit
status 1 stderr=go: loading module retractions for
golang.org/x/mod@v0.16.0: version "v0.17.0" invalid: resolves to version
v0.17.1-0.20240315155916-aa51b25a4485 (v0.17.0 is not a tag) go: loading
module retractions for golang.org/x/sync@v0.6.0: version "v0.7.0"
invalid: resolves to version v0.7.1-0.20240304172602-14be23e5b48b
(v0.7.0 is not a tag)
2024-04-25 13:01:41 -04:00
James Shubin
0e92d190cc make: Add easy error message for common issue
This can happen if the golang tools are angry. Make it easier for the
user to debug and fix.
2024-04-25 12:38:50 -04:00
James Shubin
453cd4409e lang: ast: Remove unnecessary metaparam exclusive
Originally, I considered having more than one way to express the meta
param. After thinking about it for longer, it probably makes sense to
have a second meta param if necessary, and to avoid the exclusive.
2024-04-18 00:44:34 -04:00
James Shubin
51cf1e2921 lang: ast: The res and edge names should not use exclusives
This removes the exclusive from the res names and edge names. We now
require that the names should be lists of strings, however they can
still be single strings if that can be determined statically.
Programmers should explicitly wrap their variables in a string by
interpolation to force this, or in square brackets to force a list. The
former is generally preferable because it generates a small function
graph since it doesn't need to build a list.
2024-04-18 00:07:53 -04:00
James Shubin
dc45c90ccd lang: Add common type to global variables
We use the list of strings so often, we might as well give it a global
variable.
2024-04-16 14:31:03 -04:00
James Shubin
6782d65577 test: shell, lang: core: embedded: provisioner: Check it compiles
Add a test to guarantee we continue to keep compiling, in case something
in the language changes.
2024-04-16 14:30:56 -04:00
James Shubin
68ee163eb1 entry, lang: core: embedded: provisioner: Allow more than one entry
This changes the entry API slightly to allow for more than one entry
registered, which makes building, testing and user tooling easier.
2024-04-16 14:11:34 -04:00
James Shubin
bc4b5d96b0 lang: core: embedded: provisioner: Better name for firewall entries 2024-04-15 15:03:27 -04:00
James Shubin
909dbb531d lang: ast: Fix small typos 2024-04-06 15:32:31 -04:00
Felix Frank
a2654bdc69 test: Send error messages to stderr, where they belong
When error messages are written to stdout, they will be considered as
output in case we want to fail from inside $( ) or backticks, and then
the error does not end up on the terminal.
2024-04-02 21:32:05 -04:00
Felix Frank
edcb04d1a9 misc, test: Some quality of life improvements
Add a fold in github actions output around the ragel build.

Run the commit-message test locally, so that error can be detected
before pushing to CI. We also now accept two-letter topics.

Some minor improvement in the testing scripts.
2024-04-02 21:15:51 -04:00
Felix Frank
29ec867ac7 gapi: Bring back puppet and langpuppet
This reverts commit e767655ede.

In addition, it applies required changes to function with the new CLI backend.
2024-04-02 21:07:02 -04:00
Julian Rüth
22873b3c3f docs: Fix docker build instructions
fixes #752
2024-04-03 01:01:47 +03:00
Julian Rüth
ede5db18d7 docs: Binaries are not outdated currently 2024-04-03 01:01:37 +03:00
James Shubin
964b1dc58a docs: Add release notes for 0.0.26
I send these out by email and then archive a copy here. Sign up to the
mgmt partner program for early access. Ping me for details.
2024-03-30 19:03:31 -04:00
854 changed files with 28911 additions and 16078 deletions

View File

@@ -27,9 +27,9 @@ jobs:
# macos tests are currently failing in CI
#- macos-latest
golang_version:
# TODO: add 1.21.x and tip
# TODO: add 1.24.x and tip
# minimum required and latest published go_version
- "1.20"
- "1.23"
test_block:
- basic
- shell

3
.lycheeignore Normal file
View File

@@ -0,0 +1,3 @@
# list URLs that should be excluded for lychee link checher
https://roidelapluie.be
https://github.com/purpleidea/mgmt/commit

View File

@@ -1,63 +0,0 @@
language: go
os:
- linux
go_import_path: github.com/purpleidea/mgmt
sudo: true
dist: xenial
# travis requires that you update manually, and provides this key to trigger it
apt:
update: true
before_install:
# print some debug information to help catch the constant travis regressions
- if [ -e /etc/apt/sources.list.d/ ]; then sudo ls -l /etc/apt/sources.list.d/; fi
# workaround broken travis NO_PUBKEY errors
- if [ -e /etc/apt/sources.list.d/rabbitmq_rabbitmq-server.list ]; then sudo rm -f /etc/apt/sources.list.d/rabbitmq_rabbitmq-server.list; fi
- if [ -e /etc/apt/sources.list.d/github_git-lfs.list ]; then sudo rm -f /etc/apt/sources.list.d/github_git-lfs.list; fi
# as per a number of comments online, this might mitigate some flaky fails...
- if [[ "$TRAVIS_OS_NAME" != "osx" ]]; then sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 0C49F3730359A14518585931BC711F9BA15703C6; fi
# apt update tends to be flaky in travis, retry up to 3 times on failure
# https://docs.travis-ci.com/user/common-build-problems/#travis_retry
- if [[ "$TRAVIS_OS_NAME" != "osx" ]]; then travis_retry travis_retry sudo apt update; fi
- git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*"
- git fetch --unshallow
install: 'make deps'
matrix:
fast_finish: false
allow_failures:
- go: 1.21.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.20.x
env: TEST_BLOCK=basic
- name: "shell tests"
go: 1.20.x
env: TEST_BLOCK=shell
- name: "race tests"
go: 1.20.x
env: TEST_BLOCK=race
- go: 1.21.x
- go: tip
- os: osx
script: 'TEST_BLOCK="$TEST_BLOCK" make test'
# the "secure" channel value is the result of running: ./misc/travis-encrypt.sh
# 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=
template:
- "%{repository} (%{commit}: %{author}): %{message}"
- "More info : %{build_url}"
on_success: always
on_failure: always
use_notice: false
skip_join: false
email:
recipients:
- secure: qNkgP6QLl6VXpFQIxas2wggxvIiOmm1/hGRXm4BXsSFzHsJPvMamA3E1HEC7H+luiWTny1jtGSGgTJPV9CX1LtQV0g0S4ThaAvWuKvk3rXO8IVd++iA/Lh1s1H6JdKM0dJtLqFICawjeci4tOQzSvrM2eCBWqT0UYsrQsGHB6AF31GNAH0Acqd5cYeL+ZpbCN+hQEznAZQ7546N25TwqieI8Lg7nisA+lwYYwsaC2+f5RIeyvvKjQv3wzEdBAQ9CI9WQiTOUBnUnyYxMrdomQ/XGF66QnZy9vq5nEP83IFtuhPvSamL7ceT+yJW0jDyBi8sYEV7On7eXzjyHbiYpF4YHcJrFnf5RyV4kQGd6/SC8iZwK4Is4eyeAjDFTC+JafLajw9R9x9bK43BwlRAWOZxjFKe0cU/BVAjmlz87vHgUho2P41+0a5XfajfU6VhA5QFPK6rNH7W1CnA7D/0LmS0yaqJM1OCrm6LfoZEMhe0DxTJ9uWJbr0x1sYao6q8H4xYk+fyRgoBAr2TxYU7kXx8ThiRdzuQ8izdbojlzTYLe8liZMIsjL0axLsLK7YBWrjJUcDFDjR/DqmVxPrvbVFbCi9ChmBw0WmbJvDY0FV8T8dO8wCjg9JEmprAmWPyq0g/F87LFK4tAZqQFJGjP1qwsR9jdwdNTKeCdY656f/Y=
on_failure: change
on_success: change

View File

@@ -1,5 +1,5 @@
Mgmt
Copyright (C) 2013-2024+ James Shubin and the project contributors
Copyright (C) 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-2024+ James Shubin and the project contributors
# Copyright (C) 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
@@ -225,6 +225,8 @@ build-debug: $(PROGRAM)
GOOS=$(firstword $(subst -, ,$*))
GOARCH=$(lastword $(subst -, ,$*))
build/mgmt-%: $(GO_FILES) $(MCL_FILES) go.mod go.sum | lang funcgen
@# If you need to run `go mod tidy` then this can trigger.
@if [ "$(PKGNAME)" = "" ]; then echo "\$$(PKGNAME) is empty, test with: go list ."; exit 42; fi
@echo "Building: $(PROGRAM), os/arch: $*, version: $(SVERSION)..."
time env GOOS=${GOOS} GOARCH=${GOARCH} go build $(TRIMPATH) -ldflags=$(PKGNAME)="-X main.program=$(PROGRAM) -X main.version=$(SVERSION) ${LDFLAGS}" -o $@ $(BUILD_FLAGS)

View File

@@ -10,13 +10,19 @@
[![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)
> [!TIP]
> [Resource reference guide now available!](https://mgmtconfig.com/docs/resources/)
> [!TIP]
> [Function reference guide now available!](https://mgmtconfig.com/docs/functions/)
## About:
`Mgmt` is a real-time automation tool. It is familiar to existing configuration
management software, but is drastically more powerful as it can allow you to
build real-time, closed-loop feedback systems, in a very safe way, and with a
surprisingly small amout of our `mcl` code. For example, the following code will
ensure that your file server is set to read-only when it's friday.
surprisingly small amount of our `mcl` code. For example, the following code
will ensure that your file server is set to read-only when it's friday.
```mcl
import "datetime"
@@ -92,12 +98,17 @@ Please read, enjoy and help improve our documentation!
| [quick start guide](docs/quick-start-guide.md) | for everyone |
| [frequently asked questions](docs/faq.md) | for everyone |
| [general documentation](docs/documentation.md) | for everyone |
| [resource reference](https://mgmtconfig.com/docs/resources/) | for everyone |
| [function reference](https://mgmtconfig.com/docs/functions/) | for everyone |
| [language guide](docs/language-guide.md) | for everyone |
| [function guide](docs/function-guide.md) | for mgmt developers |
| [resource guide](docs/resource-guide.md) | for mgmt developers |
| [style guide](docs/style-guide.md) | for mgmt developers |
| [contributing guide](docs/contributing.md) | for mgmt contributors |
| [service API guide](docs/service-guide.md) | for external developers |
| [godoc API reference](https://godoc.org/github.com/purpleidea/mgmt) | for mgmt developers |
| [prometheus guide](docs/prometheus.md) | for everyone |
| [puppet guide](docs/puppet-guide.md) | for puppet sysadmins |
| [development](docs/development.md) | for mgmt developers |
| [videos](docs/on-the-web.md) | for everyone |
| [blogs](docs/on-the-web.md) | for everyone |

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2024+ James Shubin and the project contributors
// Copyright (C) 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
@@ -119,6 +119,12 @@ type Args struct {
DeployCmd *DeployArgs `arg:"subcommand:deploy" help:"deploy code into a cluster"`
SetupCmd *SetupArgs `arg:"subcommand:setup" help:"setup some bootstrapping tasks"`
FirstbootCmd *FirstbootArgs `arg:"subcommand:firstboot" help:"run some tasks on first boot"`
DocsCmd *DocsGenerateArgs `arg:"subcommand:docs" help:"generate documentation"`
// This never runs, it gets preempted in the real main() function.
// XXX: Can we do it nicely with the new arg parser? can it ignore all args?
EtcdCmd *EtcdArgs `arg:"subcommand:etcd" help:"run standalone etcd"`
@@ -155,6 +161,18 @@ func (obj *Args) Run(ctx context.Context, data *cliUtil.Data) (bool, error) {
return cmd.Run(ctx, data)
}
if cmd := obj.SetupCmd; cmd != nil {
return cmd.Run(ctx, data)
}
if cmd := obj.FirstbootCmd; cmd != nil {
return cmd.Run(ctx, data)
}
if cmd := obj.DocsCmd; cmd != nil {
return cmd.Run(ctx, data)
}
// NOTE: we could return true, fmt.Errorf("...") if more than one did
return false, nil // nobody activated
}

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2024+ James Shubin and the project contributors
// Copyright (C) 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
@@ -61,6 +61,8 @@ type DeployArgs struct {
DeployEmpty *cliUtil.EmptyArgs `arg:"subcommand:empty" help:"deploy empty payload"`
DeployLang *cliUtil.LangArgs `arg:"subcommand:lang" help:"deploy lang (mcl) payload"`
DeployYaml *cliUtil.YamlArgs `arg:"subcommand:yaml" help:"deploy yaml graph payload"`
DeployPuppet *cliUtil.PuppetArgs `arg:"subcommand:puppet" help:"deploy puppet graph payload"`
DeployLangPuppet *cliUtil.LangPuppetArgs `arg:"subcommand:langpuppet" help:"deploy langpuppet graph payload"`
}
// Run executes the correct subcommand. It errors if there's ever an error. It
@@ -87,6 +89,14 @@ func (obj *DeployArgs) Run(ctx context.Context, data *cliUtil.Data) (bool, error
name = cliUtil.LookupSubcommand(obj, cmd) // "yaml"
args = cmd
}
if cmd := obj.DeployPuppet; cmd != nil {
name = cliUtil.LookupSubcommand(obj, cmd) // "puppet"
args = cmd
}
if cmd := obj.DeployLangPuppet; cmd != nil {
name = cliUtil.LookupSubcommand(obj, cmd) // "langpuppet"
args = cmd
}
// XXX: workaround https://github.com/alexflint/go-arg/issues/239
gapiNames := gapi.Names() // list of registered names

150
cli/docs.go Normal file
View File

@@ -0,0 +1,150 @@
// Mgmt
// Copyright (C) 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 <https://www.gnu.org/licenses/>.
//
// Additional permission under GNU GPL version 3 section 7
//
// If you modify this program, or any covered work, by linking or combining it
// with embedded mcl code and modules (and that the embedded mcl code and
// modules which link with this program, contain a copy of their source code in
// the authoritative form) containing parts covered by the terms of any other
// license, the licensors of this program grant you additional permission to
// convey the resulting work. Furthermore, the licensors of this program grant
// the original author, James Shubin, additional permission to update this
// additional permission if he deems it necessary to achieve the goals of this
// additional permission.
package cli
import (
"context"
"os"
"os/signal"
"sync"
"syscall"
cliUtil "github.com/purpleidea/mgmt/cli/util"
"github.com/purpleidea/mgmt/docs"
)
// DocsGenerateArgs is the CLI parsing structure and type of the parsed result.
// This particular one contains all the common flags for the `docs generate`
// subcommand.
type DocsGenerateArgs struct {
docs.Config // embedded config (can't be a pointer) https://github.com/alexflint/go-arg/issues/240
DocsGenerate *cliUtil.DocsGenerateArgs `arg:"subcommand:generate" help:"generate documentation"`
}
// Run executes the correct subcommand. It errors if there's ever an error. It
// returns true if we did activate one of the subcommands. It returns false if
// we did not. This information is used so that the top-level parser can return
// usage or help information if no subcommand activates. This particular Run is
// the run for the main `docs` subcommand.
func (obj *DocsGenerateArgs) Run(ctx context.Context, data *cliUtil.Data) (bool, error) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
var name string
var args interface{}
if cmd := obj.DocsGenerate; cmd != nil {
name = cliUtil.LookupSubcommand(obj, cmd) // "generate"
args = cmd
}
_ = name
Logf := func(format string, v ...interface{}) {
// Don't block this globally...
//if !data.Flags.Debug {
// return
//}
data.Flags.Logf("main: "+format, v...)
}
var api docs.API
if cmd := obj.DocsGenerate; cmd != nil {
api = &docs.Generate{
DocsGenerateArgs: args.(*cliUtil.DocsGenerateArgs),
Config: obj.Config,
Program: data.Program,
Version: data.Version,
Debug: data.Flags.Debug,
Logf: Logf,
}
}
if api == nil {
return false, nil // nothing found (display help!)
}
// We don't use these for the setup command in normal operation.
if data.Flags.Debug {
cliUtil.Hello(data.Program, data.Version, data.Flags) // say hello!
defer Logf("goodbye!")
}
// install the exit signal handler
wg := &sync.WaitGroup{}
defer wg.Wait()
exit := make(chan struct{})
defer close(exit)
wg.Add(1)
go func() {
defer cancel()
defer wg.Done()
// must have buffer for max number of signals
signals := make(chan os.Signal, 3+1) // 3 * ^C + 1 * SIGTERM
signal.Notify(signals, os.Interrupt) // catch ^C
//signal.Notify(signals, os.Kill) // catch signals
signal.Notify(signals, syscall.SIGTERM)
var count uint8
for {
select {
case sig := <-signals: // any signal will do
if sig != os.Interrupt {
data.Flags.Logf("interrupted by signal")
return
}
switch count {
case 0:
data.Flags.Logf("interrupted by ^C")
cancel()
case 1:
data.Flags.Logf("interrupted by ^C (fast pause)")
cancel()
case 2:
data.Flags.Logf("interrupted by ^C (hard interrupt)")
cancel()
}
count++
case <-exit:
return
}
}
}()
if err := api.Main(ctx); err != nil {
if data.Flags.Debug {
data.Flags.Logf("main: %+v", err)
}
return false, err
}
return true, nil
}

151
cli/firstboot.go Normal file
View File

@@ -0,0 +1,151 @@
// Mgmt
// Copyright (C) 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 <https://www.gnu.org/licenses/>.
//
// Additional permission under GNU GPL version 3 section 7
//
// If you modify this program, or any covered work, by linking or combining it
// with embedded mcl code and modules (and that the embedded mcl code and
// modules which link with this program, contain a copy of their source code in
// the authoritative form) containing parts covered by the terms of any other
// license, the licensors of this program grant you additional permission to
// convey the resulting work. Furthermore, the licensors of this program grant
// the original author, James Shubin, additional permission to update this
// additional permission if he deems it necessary to achieve the goals of this
// additional permission.
package cli
import (
"context"
"os"
"os/signal"
"sync"
"syscall"
cliUtil "github.com/purpleidea/mgmt/cli/util"
"github.com/purpleidea/mgmt/firstboot"
)
// FirstbootArgs is the CLI parsing structure and type of the parsed result.
// This particular one contains all the common flags for the `firstboot`
// subcommand.
type FirstbootArgs struct {
firstboot.Config // embedded config (can't be a pointer) https://github.com/alexflint/go-arg/issues/240
FirstbootStart *cliUtil.FirstbootStartArgs `arg:"subcommand:start" help:"start firstboot service"`
}
// Run executes the correct subcommand. It errors if there's ever an error. It
// returns true if we did activate one of the subcommands. It returns false if
// we did not. This information is used so that the top-level parser can return
// usage or help information if no subcommand activates. This particular Run is
// the run for the main `firstboot` subcommand. The firstboot command as a
// service that lets you run commands once on the first boot of a system.
func (obj *FirstbootArgs) Run(ctx context.Context, data *cliUtil.Data) (bool, error) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
var name string
var args interface{}
if cmd := obj.FirstbootStart; cmd != nil {
name = cliUtil.LookupSubcommand(obj, cmd) // "pkg"
args = cmd
}
_ = name
Logf := func(format string, v ...interface{}) {
// Don't block this globally...
//if !data.Flags.Debug {
// return
//}
data.Flags.Logf("main: "+format, v...)
}
var api firstboot.API
if cmd := obj.FirstbootStart; cmd != nil {
api = &firstboot.Start{
FirstbootStartArgs: args.(*cliUtil.FirstbootStartArgs),
Config: obj.Config,
Program: data.Program,
Version: data.Version,
Debug: data.Flags.Debug,
Logf: Logf,
}
}
if api == nil {
return false, nil // nothing found (display help!)
}
// We don't use these for the setup command in normal operation.
if data.Flags.Debug {
cliUtil.Hello(data.Program, data.Version, data.Flags) // say hello!
defer Logf("goodbye!")
}
// install the exit signal handler
wg := &sync.WaitGroup{}
defer wg.Wait()
exit := make(chan struct{})
defer close(exit)
wg.Add(1)
go func() {
defer cancel()
defer wg.Done()
// must have buffer for max number of signals
signals := make(chan os.Signal, 3+1) // 3 * ^C + 1 * SIGTERM
signal.Notify(signals, os.Interrupt) // catch ^C
//signal.Notify(signals, os.Kill) // catch signals
signal.Notify(signals, syscall.SIGTERM)
var count uint8
for {
select {
case sig := <-signals: // any signal will do
if sig != os.Interrupt {
data.Flags.Logf("interrupted by signal")
return
}
switch count {
case 0:
data.Flags.Logf("interrupted by ^C")
cancel()
case 1:
data.Flags.Logf("interrupted by ^C (fast pause)")
cancel()
case 2:
data.Flags.Logf("interrupted by ^C (hard interrupt)")
cancel()
}
count++
case <-exit:
return
}
}
}()
if err := api.Main(ctx); err != nil {
if data.Flags.Debug {
data.Flags.Logf("main: %+v", err)
}
return false, err
}
return true, nil
}

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2024+ James Shubin and the project contributors
// Copyright (C) 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
@@ -55,6 +55,8 @@ type RunArgs struct {
RunEmpty *cliUtil.EmptyArgs `arg:"subcommand:empty" help:"run empty payload"`
RunLang *cliUtil.LangArgs `arg:"subcommand:lang" help:"run lang (mcl) payload"`
RunYaml *cliUtil.YamlArgs `arg:"subcommand:yaml" help:"run yaml graph payload"`
RunPuppet *cliUtil.PuppetArgs `arg:"subcommand:puppet" help:"run puppet graph payload"`
RunLangPuppet *cliUtil.LangPuppetArgs `arg:"subcommand:langpuppet" help:"run a combined lang/puppet graph payload"`
}
// Run executes the correct subcommand. It errors if there's ever an error. It
@@ -81,6 +83,14 @@ func (obj *RunArgs) Run(ctx context.Context, data *cliUtil.Data) (bool, error) {
name = cliUtil.LookupSubcommand(obj, cmd) // "yaml"
args = cmd
}
if cmd := obj.RunPuppet; cmd != nil {
name = cliUtil.LookupSubcommand(obj, cmd) // "puppet"
args = cmd
}
if cmd := obj.RunLangPuppet; cmd != nil {
name = cliUtil.LookupSubcommand(obj, cmd) // "langpuppet"
args = cmd
}
// XXX: workaround https://github.com/alexflint/go-arg/issues/239
lists := [][]string{

180
cli/setup.go Normal file
View File

@@ -0,0 +1,180 @@
// Mgmt
// Copyright (C) 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 <https://www.gnu.org/licenses/>.
//
// Additional permission under GNU GPL version 3 section 7
//
// If you modify this program, or any covered work, by linking or combining it
// with embedded mcl code and modules (and that the embedded mcl code and
// modules which link with this program, contain a copy of their source code in
// the authoritative form) containing parts covered by the terms of any other
// license, the licensors of this program grant you additional permission to
// convey the resulting work. Furthermore, the licensors of this program grant
// the original author, James Shubin, additional permission to update this
// additional permission if he deems it necessary to achieve the goals of this
// additional permission.
package cli
import (
"context"
"os"
"os/signal"
"sync"
"syscall"
cliUtil "github.com/purpleidea/mgmt/cli/util"
"github.com/purpleidea/mgmt/setup"
)
// SetupArgs is the CLI parsing structure and type of the parsed result. This
// particular one contains all the common flags for the `setup` subcommand.
type SetupArgs struct {
setup.Config // embedded config (can't be a pointer) https://github.com/alexflint/go-arg/issues/240
SetupPkg *cliUtil.SetupPkgArgs `arg:"subcommand:pkg" help:"setup packages"`
SetupSvc *cliUtil.SetupSvcArgs `arg:"subcommand:svc" help:"setup services"`
SetupFirstboot *cliUtil.SetupFirstbootArgs `arg:"subcommand:firstboot" help:"setup firstboot"`
}
// Run executes the correct subcommand. It errors if there's ever an error. It
// returns true if we did activate one of the subcommands. It returns false if
// we did not. This information is used so that the top-level parser can return
// usage or help information if no subcommand activates. This particular Run is
// the run for the main `setup` subcommand. The setup command does some
// bootstrap work to help get things going.
func (obj *SetupArgs) Run(ctx context.Context, data *cliUtil.Data) (bool, error) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
var name string
var args interface{}
if cmd := obj.SetupPkg; cmd != nil {
name = cliUtil.LookupSubcommand(obj, cmd) // "pkg"
args = cmd
}
if cmd := obj.SetupSvc; cmd != nil {
name = cliUtil.LookupSubcommand(obj, cmd) // "svc"
args = cmd
}
if cmd := obj.SetupFirstboot; cmd != nil {
name = cliUtil.LookupSubcommand(obj, cmd) // "firstboot"
args = cmd
}
_ = name
Logf := func(format string, v ...interface{}) {
// Don't block this globally...
//if !data.Flags.Debug {
// return
//}
data.Flags.Logf("main: "+format, v...)
}
var api setup.API
if cmd := obj.SetupPkg; cmd != nil {
api = &setup.Pkg{
SetupPkgArgs: args.(*cliUtil.SetupPkgArgs),
Config: obj.Config,
Program: data.Program,
Version: data.Version,
Debug: data.Flags.Debug,
Logf: Logf,
}
}
if cmd := obj.SetupSvc; cmd != nil {
api = &setup.Svc{
SetupSvcArgs: args.(*cliUtil.SetupSvcArgs),
Config: obj.Config,
Program: data.Program,
Version: data.Version,
Debug: data.Flags.Debug,
Logf: Logf,
}
}
if cmd := obj.SetupFirstboot; cmd != nil {
api = &setup.Firstboot{
SetupFirstbootArgs: args.(*cliUtil.SetupFirstbootArgs),
Config: obj.Config,
Program: data.Program,
Version: data.Version,
Debug: data.Flags.Debug,
Logf: Logf,
}
}
if api == nil {
return false, nil // nothing found (display help!)
}
// We don't use these for the setup command in normal operation.
if data.Flags.Debug {
cliUtil.Hello(data.Program, data.Version, data.Flags) // say hello!
defer Logf("goodbye!")
}
// install the exit signal handler
wg := &sync.WaitGroup{}
defer wg.Wait()
exit := make(chan struct{})
defer close(exit)
wg.Add(1)
go func() {
defer cancel()
defer wg.Done()
// must have buffer for max number of signals
signals := make(chan os.Signal, 3+1) // 3 * ^C + 1 * SIGTERM
signal.Notify(signals, os.Interrupt) // catch ^C
//signal.Notify(signals, os.Kill) // catch signals
signal.Notify(signals, syscall.SIGTERM)
var count uint8
for {
select {
case sig := <-signals: // any signal will do
if sig != os.Interrupt {
data.Flags.Logf("interrupted by signal")
return
}
switch count {
case 0:
data.Flags.Logf("interrupted by ^C")
cancel()
case 1:
data.Flags.Logf("interrupted by ^C (fast pause)")
cancel()
case 2:
data.Flags.Logf("interrupted by ^C (hard interrupt)")
cancel()
}
count++
case <-exit:
return
}
}
}()
if err := api.Main(ctx); err != nil {
if data.Flags.Debug {
data.Flags.Logf("main: %+v", err)
}
return false, err
}
return true, nil
}

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2024+ James Shubin and the project contributors
// Copyright (C) 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
@@ -102,3 +102,97 @@ type YamlArgs struct {
// Input is the input yaml code or file path or any input specification.
Input string `arg:"positional,required"`
}
// PuppetArgs is the puppet CLI parsing structure and type of the parsed result.
type PuppetArgs struct {
// Input is the input puppet code or file path or just "agent".
Input string `arg:"positional,required"`
// PuppetConf is the optional path to a puppet.conf config file.
PuppetConf string `arg:"--puppet-conf" help:"full path to the puppet.conf file to use"`
}
// LangPuppetArgs is the langpuppet CLI parsing structure and type of the parsed
// result.
type LangPuppetArgs struct {
// LangInput is the input mcl code or file path or any input specification.
LangInput string `arg:"--lang,required" help:"the input parameter for the lang module"`
// PuppetInput is the input puppet code or file path or just "agent".
PuppetInput string `arg:"--puppet,required" help:"the input parameter for the puppet module"`
// copy-pasted from PuppetArgs
// PuppetConf is the optional path to a puppet.conf config file.
PuppetConf string `arg:"--puppet-conf" help:"full path to the puppet.conf file to use"`
// end PuppetArgs
// copy-pasted from LangArgs
// TODO: removed (temporarily?)
//Stdin bool `arg:"--stdin" help:"use passthrough stdin"`
Download bool `arg:"--download" help:"download any missing imports"`
OnlyDownload bool `arg:"--only-download" help:"stop after downloading any missing imports"`
Update bool `arg:"--update" help:"update all dependencies to the latest versions"`
OnlyUnify bool `arg:"--only-unify" help:"stop after type unification"`
SkipUnify bool `arg:"--skip-unify" help:"skip type unification"`
Depth int `arg:"--depth" default:"-1" help:"max recursion depth limit (-1 is unlimited)"`
// The default of 0 means any error is a failure by default.
Retry int `arg:"--depth" help:"max number of retries (-1 is unlimited)"`
ModulePath string `arg:"--module-path,env:MGMT_MODULE_PATH" help:"choose the modules path (absolute)"`
// end LangArgs
}
// SetupPkgArgs is the setup service CLI parsing structure and type of the
// parsed result.
type SetupPkgArgs struct {
Distro string `arg:"--distro" help:"build for this distro"`
Sudo bool `arg:"--sudo" help:"include sudo in the command"`
Exec bool `arg:"--exec" help:"actually run these commands"`
}
// SetupSvcArgs is the setup service CLI parsing structure and type of the
// parsed result.
type SetupSvcArgs struct {
BinaryPath string `arg:"--binary-path" help:"path to the binary"`
Install bool `arg:"--install" help:"install the systemd mgmt service"`
Start bool `arg:"--start" help:"start the mgmt service"`
Enable bool `arg:"--enable" help:"enable the mgmt service"`
}
// SetupFirstbootArgs is the setup service CLI parsing structure and type of the
// parsed result.
type SetupFirstbootArgs struct {
BinaryPath string `arg:"--binary-path" help:"path to the binary"`
Mkdir bool `arg:"--mkdir" help:"make the necessary firstboot dirs"`
Install bool `arg:"--install" help:"install the systemd firstboot service"`
Start bool `arg:"--start" help:"start the firstboot service (typically not used)"`
Enable bool `arg:"--enable" help:"enable the firstboot service"`
FirstbootStartArgs // Include these options if we want to specify them.
}
// FirstbootStartArgs is the firstboot service CLI parsing structure and type of
// the parsed result.
type FirstbootStartArgs struct {
LockFilePath string `arg:"--lock-file-path" help:"path to the lock file"`
ScriptsDir string `arg:"--scripts-dir" help:"path to the scripts dir"`
DoneDir string `arg:"--done-dir" help:"dir to move done scripts to"`
LoggingDir string `arg:"--logging-dir" help:"directory to store logs in"`
}
// DocsGenerateArgs is the docgen utility CLI parsing structure and type of the
// parsed result.
type DocsGenerateArgs struct {
Output string `arg:"--output" help:"output path to write to"`
RootDir string `arg:"--root-dir" help:"path to mgmt source dir"`
NoResources bool `arg:"--no-resources" help:"skip resource doc generation"`
NoFunctions bool `arg:"--no-functions" help:"skip function doc generation"`
}

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2024+ James Shubin and the project contributors
// Copyright (C) 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
@@ -41,7 +41,7 @@ func Hello(program, version string, flags Flags) {
program = "<unknown>"
}
fmt.Println(fmt.Sprintf("This is: %s, version: %s", program, version))
fmt.Println("Copyright (C) 2013-2024+ James Shubin and the project contributors")
fmt.Println("Copyright (C) James Shubin and the project contributors")
fmt.Println("Written by James Shubin <james@shubin.ca> and the project contributors")
flags.Logf("main: start: %v", start)
}

View File

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

2
debian/copyright vendored
View File

@@ -3,7 +3,7 @@ Upstream-Name: mgmt
Source: <https://github.com/purpleidea/mgmt>
Files: *
Copyright: Copyright (C) 2013-2024+ James Shubin and the project contributors
Copyright: Copyright (C) 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-2024+ James Shubin and the project contributors
// Copyright (C) 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,4 +1,4 @@
FROM golang:1.20
FROM golang:1.23
MAINTAINER Michał Czeraszkiewicz <contact@czerasz.com>

View File

@@ -1,4 +1,4 @@
FROM fedora:38
FROM fedora:41
LABEL org.opencontainers.image.authors="laurent.indermuehle@pm.me"
ENV GOPATH=/root/gopath

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.20.11.linux-amd64.tar.gz https://storage.googleapis.com/golang/go1.20.11.linux-amd64.tar.gz && tar -C /usr/local -xzf /opt/go1.20.11.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.23.5.linux-amd64.tar.gz https://storage.googleapis.com/golang/go1.23.5.linux-amd64.tar.gz && tar -C /usr/local -xzf /opt/go1.23.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.20
FROM golang:1.23
MAINTAINER Michał Czeraszkiewicz <contact@czerasz.com>

View File

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

96
docs/contributing.md Normal file
View File

@@ -0,0 +1,96 @@
# Contributing
What follows is a short guide with information for participants who wish to
contribute to the project. It hopes to set both some expectations and boundaries
so that we both benefit.
## Small patches
If you have a small patch which you believe is straightforward, should be easy
to merge, and isn't overly onerous on your time to write, please feel free to
send it our way without asking first. Bug fixes are excellent examples of small
patches. Please make sure to familiarize yourself with the rough coding style of
the project first, and read through the [style guide](style-guide.md).
## Making an excellent small patch
As a special case: We'd like to avoid minimal effort, one-off, drive-by patches
by bots and contributors looking to increase their "activity" numbers. As an
example: a patch which fixes a small linting issue isn't rousing, but a patch
that adds a linter test _and_ fixes a small linting issue is, because it shows
you put in more effort.
## Medium patches
Medium sized patches are especially welcome. Good examples of these patches
can include writing a new `mgmt` resource or function. You'll generally need
some knowledge of golang interfaces and concurrency to write these patches.
Before writing one of these, please make sure you understand some basics about
the project and how the tool works. After this, it is recommended that you join
our discussion channel to suggest the idea, and ideally include the actual API
you'd like to propose before writing the code and sending a patch.
## Making an excellent medium patch proposal
The "API" of a resource is the type signature of the resource struct, and the
"API" of a function is the type signature or signatures that it supports. (Since
functions can be polymorphic, more than one signature can be possible!) A good
proposal would likely also comment on the mechanisms the resources or functions
would use to watch for events, to check state, and to apply changes. If these
mechanisms need new dependencies, a brief survey of which dependencies are
available and why you recommend a particular one is encouraged.
## Large patches or structural and core patches
Please do not send us large, core or structurally significant patches without
first getting our approval and without getting some medium patches in first.
These patches take a lot of effort to review, and we don't want to skimp on our
commitment to that if we can't muster it. Instead grow our relationship with you
on the medium-sized patches first. (A core patch might refer to something that
touches either the function engine, resource engine, compiler internals, or
something that is part of one of the internal API's.)
## Expectations and boundaries
When interacting with the project and soliciting feedback (either for design or
during a code review) please keep in mind that the project (unfortunately!) has
time constraints and so must prioritize how it handles workloads. If you are
someone who has successfully sent in small patches, we will be more willing to
spend time mentoring your medium sized patches and so on. Think of it this way:
as you show that you're contributing to the project, we'll contribute more to
you. Put another way: we can't afford to spend large amounts of time discussing
potential patches with you, just to end up nowhere. Build up your reputation
with us, and we hope to help grow our symbiosis with you all the while as you
grow too!
## Energy output
The same goes for users and issue creators. There are times when we simply don't
have the cycles to discuss or litigate an issue with you. We wish we did have
more time, but it is finite, and running a project is not free. Therefore,
please keep in mind that you don't automatically qualify for free support or
attention.
## Attention seeking behaviours
Some folks spend too much time starting discussions, commenting on issues,
"planning" and otherwise displaying attention seeking behaviours. Please avoid
doing this as much as possible, especially if you are not already a major
contributor to the project. While it may be well intentioned, if it is
indistinguishable to us from intentional interference, then it's not welcome
behaviour. Remember that Free Software is not free to write. If you require more
attention, then either contribute more to the project, or consider paying for a
[support contract](https://mgmtconfig.com/).
## Consulting
Having said all that, there are some folks who want to do some longer-term
planning to decide if our core design and architecture is right for them to
invest in. If that's the case, and you aren't already a well-known project
contributor, please [contact](https://mgmtconfig.com/) us for a consulting
quote. We have packages available for both individuals and businesses.
## Respect
Please be mindful and respectful of others when interacting with the project and
its contributors. If you cannot abide by that, you may no longer be welcome.

View File

@@ -16,7 +16,7 @@ be working properly.
## Using Docker
Alternatively, you can check out the [docker-guide](docker-guide.md) in order to
Alternatively, you can check out the [docker folder](../docker/) in order to
develop or deploy using docker. This method is not endorsed or supported, so use
at your own risk, as it might not be working properly.
@@ -28,8 +28,9 @@ required for running the _test_ suite.
### Build
* `golang` 1.20 or higher (required, available in some distros and distributed
as a binary officially by [golang.org](https://golang.org/dl/))
* A modern `golang` version. The version available in the current Fedora
releases is usually supported. This is also distributed as a binary officially
by [golang.org](https://golang.org/dl/).
### Runtime

50
docs/docs.go Normal file
View File

@@ -0,0 +1,50 @@
// Mgmt
// Copyright (C) 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 <https://www.gnu.org/licenses/>.
//
// Additional permission under GNU GPL version 3 section 7
//
// If you modify this program, or any covered work, by linking or combining it
// with embedded mcl code and modules (and that the embedded mcl code and
// modules which link with this program, contain a copy of their source code in
// the authoritative form) containing parts covered by the terms of any other
// license, the licensors of this program grant you additional permission to
// convey the resulting work. Furthermore, the licensors of this program grant
// the original author, James Shubin, additional permission to update this
// additional permission if he deems it necessary to achieve the goals of this
// additional permission.
// Package docs provides a tool that generates documentation from the source.
//
// ./mgmt docs generate --output /tmp/docs.json && cat /tmp/docs.json | jq
package docs
import (
"context"
)
// API is the simple interface we expect for any setup items.
type API interface {
// Main runs everything for this setup item.
Main(context.Context) error
}
// Config is a struct of all the configuration values which are shared by all of
// the setup utilities. By including this as a separate struct, it can be used
// as part of the API if we want.
type Config struct {
//Foo string `arg:"--foo,env:MGMT_DOCGEN_FOO" help:"Foo..."` // TODO: foo
}

View File

@@ -131,6 +131,33 @@ execute via a `remote` resource.
You can read the introductory blog post about this topic here:
[https://purpleidea.com/blog/2016/10/07/remote-execution-in-mgmt/](https://purpleidea.com/blog/2016/10/07/remote-execution-in-mgmt/)
### Puppet support
You can supply a puppet manifest instead of creating the (YAML) graph manually.
Puppet must be installed and in `mgmt`'s search path. You also need the
[ffrank-mgmtgraph puppet module](https://forge.puppet.com/ffrank/mgmtgraph).
Invoke `mgmt` with the `--puppet` switch, which supports 3 variants:
1. Request the configuration from the puppet server (like `puppet agent` does)
`mgmt run puppet --puppet agent`
2. Compile a local manifest file (like `puppet apply`)
`mgmt run puppet --puppet /path/to/my/manifest.pp`
3. Compile an ad hoc manifest from the commandline (like `puppet apply -e`)
`mgmt run puppet --puppet 'file { "/etc/ntp.conf": ensure => file }'`
For more details and caveats see [puppet-guide.md](puppet-guide.md).
#### Blog post
An introductory post on the puppet support is on
[Felix's blog](http://ffrank.github.io/features/2016/06/19/puppet-powered-mgmt/).
## Reference
Please note that there are a number of undocumented options. For more
@@ -247,6 +274,29 @@ and it can't guarantee it if the resource is blocked because of a failed
pre-requisite resource.
*XXX: This is currently not implemented!*
#### Dollar
Boolean. Dollar allows you to have a resource name that starts with a `$` sign.
This is false by default. This helps you catch cases when you write code like:
```mcl
$foo = "/tmp/file1"
file "$foo" {} # incorrect!
```
The above code would ignore the `$foo` variable and attempt to make a file named
`$foo` which would obviously not work. To correctly interpolate a variable, you
need to surround the name with curly braces.
```mcl
$foo = "/tmp/file1"
file "${foo}" {} # correct!
```
This meta param is a safety measure to make your life easier. It works for all
resources. If someone comes up with a resource which would routinely start with
a dollar sign, then we can revisit the default for this resource kind.
#### Reverse
Boolean. Reverse is a property that some resources can implement that specifies
@@ -335,7 +385,7 @@ size of 42, you can expect a semaphore if named: `:42`. It is expected that
consumers of the semaphore metaparameter always include a prefix to avoid a
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`.
management tools such as `puppet` can be obtained with `--sema 1`.
#### `--ssh-priv-id-rsa`
@@ -410,7 +460,7 @@ directory in the git source repository. It is available from:
### Systemd:
See [`misc/mgmt.service`](misc/mgmt.service) for a sample systemd unit file.
See [`misc/mgmt.service`](../misc/mgmt.service) for a sample systemd unit file.
This unit file is part of the RPM.
To specify your custom options for `mgmt` on a systemd distro:
@@ -443,7 +493,7 @@ To report any bugs, please file a ticket at: [https://github.com/purpleidea/mgmt
## Authors
Copyright (C) 2013-2024+ James Shubin and the project contributors
Copyright (C) James Shubin and the project contributors
Please see the
[AUTHORS](https://github.com/purpleidea/mgmt/tree/master/AUTHORS) file

View File

@@ -280,9 +280,76 @@ 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 reversible file resource.
### Package resources error with: "The name is not activatable", what's wrong?
You may see an error like:
`main: error running auto edges: The name is not activatable`
This can happen because the mgmt `pkg` resource uses a library and daemon called
`PackageKit` to install packages. If it is not installed, then it cannot do its
work. On Fedora system you may wish to run `dnf install /usr/bin/pkcon` or on a
Debian system you may wish to run `apt install packagekit-tools`.
PackageKit is excellent because it provides both an API and an event system to
watch the package database for changes, and it abstracts away the differences
between the various package managers. If you'd prefer to not need to install
this tool, then you can contribute a native `pkg:rpm` and `pkg:deb` resource to
mgmt!
### When running mgmt, it says: "module path error: can't find a module path".
You might get an error along the lines of:
```
could not set scope: import scope `git://github.com/purpleidea/mgmt/modules/some_module_name/` failed: module path error: can't find a module path
```
This usually means that you haven't specified the directory that mgmt should use
when looking for modules. This could happen when using mgmt interactively or
when it's being run as a service. In such cases you may want the main invocation
to look something like:
```
mgmt run lang --module-path '/etc/mgmt/modules/' /etc/mgmt/main.mcl
```
### I get an error: "cannot open shared object file: No such file or directory".
Mgmt currently uses two libraries that depend on `.so` files being installed on
the host. Those are for `augeas` and `libvirt`. If those dependencies are not
present, then mgmt will not run. The complete error might look like:
```
mgmt: error while loading shared libraries: libvirt-lxc.so.0: cannot open shared object file: No such file or directory
```
or:
```
mgmt: error while loading shared libraries: libaugeas.so.0: cannot open shared object file: No such file or directory
```
or something similar. There are two solutions to this:
1. Use a build that doesn't include one or both of those features. You can build
that like: `GOTAGS="noaugeas novirt nodocker" make build`.
2. Install those dependencies. On a Fedora machine you might want to run:
```
dnf install libvirt-devel augeas-devel
```
On a Debian machine you might want to run:
```
apt install libvirt-dev libaugeas-dev
```
### Why do function names inside of templates include underscores?
The golang template library which we use to implement the template() function
The golang template library which we use to implement the golang.template() func
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.
@@ -320,7 +387,7 @@ 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.
### Type unification error: "could not unify types: 2 unconsumed generators".
### Type unification error with string interpolation.
Look carefully at the following code:
@@ -343,8 +410,13 @@ print "hello" {
}
```
Yes we know the compiler gives horrible error messages, and yes we would
absolutely love your help improving this.
The first example will usually error with something along the lines of:
`unify error with: topLevel(func() { <built-in:concat> }): type error: int != str`
Now you know why this specific case doesn't work! We may reconsider allowing
other types to be pulled into interpolation in the future. If you have a good
case for this, then let us know.
### The run and deploy commands don't parse correctly when used with `--seeds`.

View File

@@ -41,7 +41,7 @@ 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/core/`](https://github.com/purpleidea/mgmt/tree/master/lang/core/). The
function should be implemented as a `FuncValue` in our type system. It is then
function should be implemented as a `simple.Scaffold` in our API. It is then
registered with the engine during `init()`. An example explains it best:
### Example
@@ -50,6 +50,7 @@ registered with the engine during `init()`. An example explains it best:
package simple
import (
"context"
"fmt"
"github.com/purpleidea/mgmt/lang/funcs/simple"
@@ -59,9 +60,10 @@ import (
// 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.
simple.ModuleRegister(ModuleName, "talkingsquare", &types.FuncValue{
simple.ModuleRegister(ModuleName, "talkingsquare", &simple.Scaffold{
T: types.NewType("func(int) str"), // declare the signature
V: func(input []types.Value) (types.Value, error) {
F: func(ctx context.Context, input []types.Value) (types.Value, error) {
i := input[0].Int() // get first arg as an int64
// must return the above specified value
return &types.StrValue{
@@ -87,109 +89,41 @@ mgmt engine to shutdown. It should be seen as the equivalent to calling a
Ideally, your functions should never need to error. You should never cause a
real `panic()`, since this could have negative consequences to the system.
## Simple Polymorphic Function API
Most functions should be implemented using the simple function API. If they need
to have multiple polymorphic forms under the same name, then you can use this
API. This is useful for situations when it would be unhelpful to name the
functions differently, or when the number of possible signatures for the
function would be infinite.
The canonical example of this is the `len` function which returns the number of
elements in either a `list` or a `map`. Since lists and maps are two different
types, you can see that polymorphism is more convenient than requiring a
`listlen` and `maplen` function. Nevertheless, it is also required because a
`list of int` is a different type than a `list of str`, which is a different
type than a `list of list of str` and so on. As you can see the number of
possible input types for such a `len` function is infinite.
Another downside to implementing your functions with this API is that they will
*not* be made available for use inside templates. This is a limitation of the
`golang` template library. In the future if this limitation proves to be
significantly annoying, we might consider writing our own template library.
As with the simple, non-polymorphic API, you can only implement [pure](https://en.wikipedia.org/wiki/Pure_function)
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 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/core/`](https://github.com/purpleidea/mgmt/tree/master/lang/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. 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:
### Example
```golang
package simple
import (
"context"
"fmt"
"github.com/purpleidea/mgmt/lang/funcs/simplepoly"
"github.com/purpleidea/mgmt/lang/funcs/simple"
"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"),
V: Len,
},
{
T: types.NewType("func({variant: variant}) int"),
V: Len,
},
// This is the actual definition of the `len` function.
simple.Register("len", &simple.Scaffold{
T: types.NewType("func(?1) int"), // contains a unification var
C: simple.TypeMatch([]string{ // match on any of these sigs
"func(str) int",
"func([]?1) int",
"func(map{?1: ?2}) int",
}),
// The implementation is left as an exercise for the reader.
F: Len,
})
}
// Len returns the number of elements in a list or the number of key pairs in a
// map. It can operate on either of these types.
func Len(input []types.Value) (types.Value, error) {
var length int
switch k := input[0].Type().Kind; k {
case types.KindList:
length = len(input[0].List())
case types.KindMap:
length = len(input[0].Map())
default:
return nil, fmt.Errorf("unsupported kind: %+v", k)
}
return &types.IntValue{
V: int64(length),
}, nil
}
```
This simple polymorphic function can accept an infinite number of signatures, of
which there are two basic forms. Both forms return an `int` as is seen above.
The first form takes a `[]variant` which means a `list` of `variant`'s, which
means that it can be a list of any type, since `variant` itself is not a
concrete type. The second form accepts a `{variant: variant}`, which means that
it accepts any form of `map` as input.
## Simple Polymorphic Function API
The implementation for both of these forms is the same: it is handled by the
same `Len` function which is clever enough to be able to deal with any of the
type signatures possible from those two patterns.
At compile time, if your `mcl` code type checks correctly, a concrete type will
be known for each and every usage of the `len` function, and specific values
will be passed in for this code to compute the length of. As usual, make sure to
only write safe code that will not panic! A panic is a bug. If you really cannot
continue, then you must return an error.
Most functions should be implemented using the simple function API. If they need
to have multiple polymorphic forms under the same name, with each resultant type
match needing to be paired to a different implementation, then you can use this
API. This is useful for situations when the functions differ in output type
only.
## Function API
@@ -358,23 +292,6 @@ We don't expect this functionality to be particularly useful or common, as it's
probably easier and preferable to simply import common golang library code into
multiple different functions instead.
## Polymorphic Function API
The polymorphic function API is an API that lets you implement functions which
do not necessarily have a single static function signature. After compile time,
all functions must have a static function signature. We also know that there
might be different ways you would want to call `printf`, such as:
`printf("the %s is %d", "answer", 42)` or `printf("3 * 2 = %d", 3 * 2)`. Since
you couldn't implement the infinite number of possible signatures, this API lets
you write code which can be coerced into different forms. This makes
implementing what would appear to be generic or polymorphic, instead of
something that is actually static and that still has the static type safety
properties that were guaranteed by the mgmt language.
Since this is an advanced topic, it is not described in full at this time. For
more information please have a look at the source code comments, some of the
existing implementations, and ask around in the community.
## Frequently asked questions
(Send your questions as a patch to this FAQ! I'll review it, merge it, and

795
docs/generate.go Normal file
View File

@@ -0,0 +1,795 @@
// Mgmt
// Copyright (C) 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 <https://www.gnu.org/licenses/>.
//
// Additional permission under GNU GPL version 3 section 7
//
// If you modify this program, or any covered work, by linking or combining it
// with embedded mcl code and modules (and that the embedded mcl code and
// modules which link with this program, contain a copy of their source code in
// the authoritative form) containing parts covered by the terms of any other
// license, the licensors of this program grant you additional permission to
// convey the resulting work. Furthermore, the licensors of this program grant
// the original author, James Shubin, additional permission to update this
// additional permission if he deems it necessary to achieve the goals of this
// additional permission.
package docs
import (
"context"
"encoding/json"
"fmt"
"go/ast"
"go/parser"
"go/token"
"os"
"path/filepath"
"sort"
"strings"
cliUtil "github.com/purpleidea/mgmt/cli/util"
docsUtil "github.com/purpleidea/mgmt/docs/util"
"github.com/purpleidea/mgmt/engine"
engineUtil "github.com/purpleidea/mgmt/engine/util"
"github.com/purpleidea/mgmt/lang/funcs"
"github.com/purpleidea/mgmt/lang/interfaces"
"github.com/purpleidea/mgmt/util"
)
const (
// JSONSuffix is the output extension for the generated documentation.
JSONSuffix = ".json"
)
// Generate is the main entrypoint for this command. It generates everything.
type Generate struct {
*cliUtil.DocsGenerateArgs // embedded config
Config // embedded Config
// Program is the name of this program, usually set at compile time.
Program string
// Version is the version of this program, usually set at compile time.
Version string
// Debug represents if we're running in debug mode or not.
Debug bool
// Logf is a logger which should be used.
Logf func(format string, v ...interface{})
}
// Main runs everything for this setup item.
func (obj *Generate) Main(ctx context.Context) error {
if err := obj.Validate(); err != nil {
return err
}
if err := obj.Run(ctx); err != nil {
return err
}
return nil
}
// Validate verifies that the structure has acceptable data stored within.
func (obj *Generate) Validate() error {
if obj == nil {
return fmt.Errorf("data is nil")
}
if obj.Program == "" {
return fmt.Errorf("program is empty")
}
if obj.Version == "" {
return fmt.Errorf("version is empty")
}
return nil
}
// Run performs the desired actions to generate the documentation.
func (obj *Generate) Run(ctx context.Context) error {
outputFile := obj.DocsGenerateArgs.Output
if outputFile == "" || !strings.HasSuffix(outputFile, JSONSuffix) {
return fmt.Errorf("must specify output")
}
// support relative paths too!
if !strings.HasPrefix(outputFile, "/") {
wd, err := os.Getwd()
if err != nil {
return err
}
outputFile = filepath.Join(wd, outputFile)
}
if obj.Debug {
obj.Logf("output: %s", outputFile)
}
// Ensure the directory exists.
//d := filepath.Dir(outputFile)
//if err := os.MkdirAll(d, 0750); err != nil {
// return fmt.Errorf("could not make output dir at: %s", d)
//}
resources, err := obj.genResources()
if err != nil {
return err
}
functions, err := obj.genFunctions()
if err != nil {
return err
}
data := &Output{
Version: safeVersion(obj.Version),
Resources: resources,
Functions: functions,
}
b, err := json.Marshal(data)
if err != nil {
return err
}
b = append(b, '\n') // needs a trailing newline
if err := os.WriteFile(outputFile, b, 0600); err != nil {
return err
}
obj.Logf("wrote: %s", outputFile)
return nil
}
func (obj *Generate) getResourceInfo(kind, filename, structName string) (*ResourceInfo, error) {
rootDir := obj.DocsGenerateArgs.RootDir
if rootDir == "" {
wd, err := os.Getwd()
if err != nil {
return nil, err
}
rootDir = wd + "/" // add a trailing slash
}
if !strings.HasPrefix(rootDir, "/") || !strings.HasSuffix(rootDir, "/") {
return nil, fmt.Errorf("bad root dir: %s", rootDir)
}
// filename might be "noop.go" for example
p := filepath.Join(rootDir, engine.ResourcesRelDir, filename)
fset := token.NewFileSet()
// f is a: https://golang.org/pkg/go/ast/#File
f, err := parser.ParseFile(fset, p, nil, parser.ParseComments)
if err != nil {
return nil, err
}
// mcl field name to golang field name
mapping, err := engineUtil.LangFieldNameToStructFieldName(kind)
if err != nil {
return nil, err
}
// golang field name to mcl field name
nameMap, err := util.MapSwap(mapping)
if err != nil {
return nil, err
}
// mcl field name to mcl type
typMap, err := engineUtil.LangFieldNameToStructType(kind)
if err != nil {
return nil, err
}
ri := &ResourceInfo{}
// Populate the fields, even if they don't have a comment.
ri.Name = structName // golang name
ri.Kind = kind // duplicate data
ri.File = filename
ri.Fields = make(map[string]*ResourceFieldInfo)
for mclFieldName, fieldName := range mapping {
typ, exists := typMap[mclFieldName]
if !exists {
continue
}
ri.Fields[mclFieldName] = &ResourceFieldInfo{
Name: fieldName,
Type: typ.String(),
Desc: "", // empty for now
}
}
var previousComment *ast.CommentGroup
// Walk through the AST...
ast.Inspect(f, func(node ast.Node) bool {
// Comments above the struct appear as a node right _before_ we
// find the struct, so if we see one, save it for later...
if cg, ok := node.(*ast.CommentGroup); ok {
previousComment = cg
return true
}
typeSpec, ok := node.(*ast.TypeSpec)
if !ok {
return true
}
name := typeSpec.Name.Name // name is now known!
// If the struct isn't what we're expecting, then move on...
if name != structName {
return true
}
// Check if the TypeSpec is a named struct type that we want...
st, ok := typeSpec.Type.(*ast.StructType)
if !ok {
return true
}
// At this point, we have the struct we want...
var comment *ast.CommentGroup
if typeSpec.Doc != nil {
// I don't know how to even get here...
comment = typeSpec.Doc // found!
} else if previousComment != nil {
comment = previousComment // found!
previousComment = nil
}
ri.Desc = commentCleaner(comment)
// Iterate over the fields of the struct
for _, field := range st.Fields.List {
// Check if the field has a comment associated with it
if field.Doc == nil {
continue
}
if len(field.Names) < 1 { // XXX: why does this happen?
continue
}
fieldName := field.Names[0].Name
if fieldName == "" { // Can this happen?
continue
}
if isPrivate(fieldName) {
continue
}
mclFieldName, exists := nameMap[fieldName]
if !exists {
continue
}
ri.Fields[mclFieldName].Desc = commentCleaner(field.Doc)
}
return true
})
return ri, nil
}
func (obj *Generate) genResources() (map[string]*ResourceInfo, error) {
resources := make(map[string]*ResourceInfo)
if obj.DocsGenerateArgs.NoResources {
return resources, nil
}
r := engine.RegisteredResourcesNames()
sort.Strings(r)
for _, kind := range r {
metadata, err := docsUtil.LookupResource(kind)
if err != nil {
return nil, err
}
if strings.HasPrefix(kind, "_") {
// TODO: Should we display these somehow?
// built-in resource
continue
}
ri, err := obj.getResourceInfo(kind, metadata.Filename, metadata.Typename)
if err != nil {
return nil, err
}
if ri.Name == "" {
return nil, fmt.Errorf("empty resource name: %s", kind)
}
if ri.File == "" {
return nil, fmt.Errorf("empty resource file: %s", kind)
}
if ri.Desc == "" {
obj.Logf("empty resource desc: %s", kind)
}
fields := []string{}
for field := range ri.Fields {
fields = append(fields, field)
}
sort.Strings(fields)
for _, field := range fields {
if ri.Fields[field].Desc == "" {
obj.Logf("empty resource (%s) field desc: %s", kind, field)
}
}
resources[kind] = ri
}
return resources, nil
}
func (obj *Generate) getFunctionInfo(pkg, name string, metadata *docsUtil.Metadata) (*FunctionInfo, error) {
rootDir := obj.DocsGenerateArgs.RootDir
if rootDir == "" {
wd, err := os.Getwd()
if err != nil {
return nil, err
}
rootDir = wd + "/" // add a trailing slash
}
if !strings.HasPrefix(rootDir, "/") || !strings.HasSuffix(rootDir, "/") {
return nil, fmt.Errorf("bad root dir: %s", rootDir)
}
if metadata.Filename == "" {
return nil, fmt.Errorf("empty filename for: %s.%s", pkg, name)
}
// filename might be "pow.go" for example and contain a rel dir
p := filepath.Join(rootDir, funcs.FunctionsRelDir, metadata.Filename)
fset := token.NewFileSet()
// f is a: https://golang.org/pkg/go/ast/#File
f, err := parser.ParseFile(fset, p, nil, parser.ParseComments)
if err != nil {
return nil, err
}
fi := &FunctionInfo{}
fi.Name = metadata.Typename
fi.File = metadata.Filename
var previousComment *ast.CommentGroup
found := false
rawFunc := func(node ast.Node) (*ast.CommentGroup, string) {
fd, ok := node.(*ast.FuncDecl)
if !ok {
return nil, ""
}
return fd.Doc, fd.Name.Name // name is now known!
}
rawStruct := func(node ast.Node) (*ast.CommentGroup, string) {
typeSpec, ok := node.(*ast.TypeSpec)
if !ok {
return nil, ""
}
// Check if the TypeSpec is a named struct type that we want...
if _, ok := typeSpec.Type.(*ast.StructType); !ok {
return nil, ""
}
return typeSpec.Doc, typeSpec.Name.Name // name is now known!
}
// Walk through the AST...
ast.Inspect(f, func(node ast.Node) bool {
// Comments above the struct appear as a node right _before_ we
// find the struct, so if we see one, save it for later...
if cg, ok := node.(*ast.CommentGroup); ok {
previousComment = cg
return true
}
doc, name := rawFunc(node) // First see if it's a raw func.
if name == "" {
doc, name = rawStruct(node) // Otherwise it's a struct.
}
// If the func isn't what we're expecting, then move on...
if name != metadata.Typename {
return true
}
var comment *ast.CommentGroup
if doc != nil {
// I don't know how to even get here...
comment = doc // found!
} else if previousComment != nil {
comment = previousComment // found!
previousComment = nil
}
fi.Desc = commentCleaner(comment)
found = true
return true
})
if !found {
//return nil, nil
}
return fi, nil
}
func (obj *Generate) genFunctions() (map[string]*FunctionInfo, error) {
functions := make(map[string]*FunctionInfo)
if obj.DocsGenerateArgs.NoFunctions {
return functions, nil
}
m := funcs.Map() // map[string]func() interfaces.Func
names := []string{}
for name := range m {
names = append(names, name)
}
sort.Slice(names, func(i, j int) bool {
a := names[i]
b := names[j]
// TODO: do a sorted-by-package order.
return a < b
})
for _, name := range names {
//v := m[name]
//fn := v()
fn := m[name]()
// eg: golang/strings.has_suffix
sp := strings.Split(name, ".")
if len(sp) == 0 {
return nil, fmt.Errorf("unexpected empty function")
}
if len(sp) > 2 {
return nil, fmt.Errorf("unexpected function name: %s", name)
}
n := sp[0]
p := sp[0]
if len(sp) == 1 { // built-in
p = "" // no package!
}
if len(sp) == 2 { // normal import
n = sp[1]
}
if strings.HasPrefix(n, "_") {
// TODO: Should we display these somehow?
// built-in function
continue
}
var sig *string
//iface := ""
if x := fn.Info().Sig; x != nil {
s := x.String()
sig = &s
//iface = "simple"
}
metadata := &docsUtil.Metadata{}
// XXX: maybe we need a better way to get this?
mdFunc, ok := fn.(interfaces.MetadataFunc)
if !ok {
// Function doesn't tell us what the data is, let's try
// to get it automatically...
metadata.Typename = funcs.GetFunctionName(fn) // works!
metadata.Filename = "" // XXX: How can we get this?
// XXX: We only need this back-channel metadata store
// because we don't know how to get the filename without
// manually writing code in each function. Alternatively
// we could add a New() method to each struct and then
// we could modify the struct instead of having it be
// behind a copy which is needed to get new copies!
var err error
metadata, err = docsUtil.LookupFunction(name)
if err != nil {
return nil, err
}
} else if mdFunc == nil {
// programming error
return nil, fmt.Errorf("unexpected empty metadata for function: %s", name)
} else {
metadata = mdFunc.GetMetadata()
}
if metadata == nil {
return nil, fmt.Errorf("unexpected nil metadata for function: %s", name)
}
// This may be an empty func name if the function did not know
// how to get it. (This is normal for automatic regular funcs.)
if metadata.Typename == "" {
metadata.Typename = funcs.GetFunctionName(fn) // works!
}
fi, err := obj.getFunctionInfo(p, n, metadata)
if err != nil {
return nil, err
}
// We may not get any fields added if we can't find anything...
fi.Name = metadata.Typename
fi.Package = p
fi.Func = n
fi.File = metadata.Filename
//fi.Desc = desc
fi.Signature = sig
// Hack for golang generated functions!
if strings.HasPrefix(fi.Package, "golang/") && fi.File == "generated_funcs.go" {
pkg := fi.Package[len("golang/"):]
frag := strings.TrimPrefix(fi.Name, strings.Title(strings.Join(strings.Split(pkg, "/"), ""))) // yuck
fi.File = fmt.Sprintf("https://pkg.go.dev/%s#%s", pkg, frag)
}
if fi.Func == "" {
return nil, fmt.Errorf("empty function name: %s", name)
}
if fi.File == "" {
return nil, fmt.Errorf("empty function file: %s", name)
}
if fi.Desc == "" {
obj.Logf("empty function desc: %s", name)
}
if fi.Signature == nil {
obj.Logf("empty function sig: %s", name)
}
functions[name] = fi
}
return functions, nil
}
// Output is the type of the final data that will be for the json output.
type Output struct {
// Version is the sha1 or ref name of this specific version. This is
// used if we want to generate documentation with links matching the
// correct version. If unspecified then this assumes git master.
Version string `json:"version"`
// Resources contains the collection of every available resource!
// FIXME: should this be a list instead?
Resources map[string]*ResourceInfo `json:"resources"`
// Functions contains the collection of every available function!
// FIXME: should this be a list instead?
Functions map[string]*FunctionInfo `json:"functions"`
}
// ResourceInfo stores some information about each resource.
type ResourceInfo struct {
// Name is the golang name of this resource.
Name string `json:"name"`
// Kind is the kind of this resource.
Kind string `json:"kind"`
// File is the file name where this resource exists.
File string `json:"file"`
// Desc explains what this resource does.
Desc string `json:"description"`
// Fields is a collection of each resource field and corresponding info.
Fields map[string]*ResourceFieldInfo `json:"fields"`
}
// ResourceFieldInfo stores some information about each field in each resource.
type ResourceFieldInfo struct {
// Name is what this field is called in golang format.
Name string `json:"name"`
// Type is the mcl type for this field.
Type string `json:"type"`
// Desc explains what this field does.
Desc string `json:"description"`
}
// FunctionInfo stores some information about each function.
type FunctionInfo struct {
// Name is the golang name of this function. This may be an actual
// function if used by the simple API, or the name of a struct.
Name string `json:"name"`
// Package is the import name to use to get to this function.
Package string `json:"package"`
// Func is the name of the function in that package.
Func string `json:"func"`
// File is the file name where this function exists.
File string `json:"file"`
// Desc explains what this function does.
Desc string `json:"description"`
// Signature is the type signature of this function. If empty then the
// signature is not known statically and it may be polymorphic.
Signature *string `json:"signature,omitempty"`
}
// commentCleaner takes a comment group and returns it as a clean string. It
// removes the spurious newlines and programmer-focused comments. If there are
// blank lines, it replaces them with a single newline. The idea is that the
// webpage formatter would replace the newline with a <br /> or similar. This
// code is a modified alternative of the ast.CommentGroup.Text() function.
func commentCleaner(g *ast.CommentGroup) string {
if g == nil {
return ""
}
comments := make([]string, len(g.List))
for i, c := range g.List {
comments[i] = c.Text
}
lines := make([]string, 0, 10) // most comments are less than 10 lines
for _, c := range comments {
// Remove comment markers.
// The parser has given us exactly the comment text.
switch c[1] {
case '/':
//-style comment (no newline at the end)
c = c[2:]
if len(c) == 0 {
// empty line
break
}
if isDevComment(c[1:]) { // get rid of one space
continue
}
if c[0] == ' ' {
// strip first space - required for Example tests
c = c[1:]
break
}
//if isDirective(c) {
// // Ignore //go:noinline, //line, and so on.
// continue
//}
case '*':
/*-style comment */
c = c[2 : len(c)-2]
}
// Split on newlines.
cl := strings.Split(c, "\n")
// Walk lines, stripping trailing white space and adding to list.
for _, l := range cl {
lines = append(lines, stripTrailingWhitespace(l))
}
}
// Remove leading blank lines; convert runs of interior blank lines to a
// single blank line.
n := 0
for _, line := range lines {
if line != "" || n > 0 && lines[n-1] != "" {
lines[n] = line
n++
}
}
lines = lines[0:n]
// Concatenate all of these together. Blank lines should be a newline.
s := ""
for i, line := range lines {
if line == "" {
continue
}
s += line
if i < len(lines)-1 { // Is there another line?
if lines[i+1] == "" {
s += "\n" // Will eventually be a line break.
} else {
s += " "
}
}
}
return s
}
// TODO: should we use unicode.IsSpace instead?
func isWhitespace(ch byte) bool { return ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r' }
// TODO: should we replace with a strings package stdlib function?
func stripTrailingWhitespace(s string) string {
i := len(s)
for i > 0 && isWhitespace(s[i-1]) {
i--
}
return s[0:i]
}
// isPrivate specifies if a field name is "private" or not.
func isPrivate(fieldName string) bool {
if fieldName == "" {
panic("invalid field name")
}
x := fieldName[0:1]
if strings.ToLower(x) == x {
return true // it was already private
}
return false
}
// isDevComment tells us that the comment is for developers only!
func isDevComment(comment string) bool {
if strings.HasPrefix(comment, "TODO:") {
return true
}
if strings.HasPrefix(comment, "FIXME:") {
return true
}
if strings.HasPrefix(comment, "XXX:") {
return true
}
return false
}
// safeVersion parses the main version string and returns a short hash for us.
// For example, we might get a string of 0.0.26-176-gabcdef012-dirty as input,
// and we'd want to return abcdef012.
func safeVersion(version string) string {
const dirty = "-dirty"
s := version
if strings.HasSuffix(s, dirty) { // helpful dirty remover
s = s[0 : len(s)-len(dirty)]
}
ix := strings.LastIndex(s, "-")
if ix == -1 { // assume we have a standalone version (future proofing?)
return s
}
s = s[ix+1:]
// From the `git describe` man page: The "g" prefix stands for "git" and
// is used to allow describing the version of a software depending on
// the SCM the software is managed with. This is useful in an
// environment where people may use different SCMs.
const g = "g"
if strings.HasPrefix(s, g) {
s = s[len(g):]
}
return s
}

View File

@@ -14,3 +14,4 @@ Welcome to mgmt's documentation!
quick-start-guide
resource-guide
prometheus
puppet-guide

View File

@@ -283,6 +283,14 @@ one of many ways you can perform iterative tasks that you might have
traditionally used a `for` loop for instead. This is preferred, because flow
control is error-prone and can make for less readable code.
The single `str` variation, may only be used when it is possible for the
compiler to determine statically that the value is of that type. Otherwise, it
will assume it to be a list of strings. Programmers should explicitly wrap their
variables in a string by interpolation to force this static `str` determination,
or in square brackets to force a list. The former is generally preferable
because it generates a smaller function graph since it doesn't need to build a
list.
##### Internal edges
Resources may also declare edges internally. The edges may point to or from
@@ -337,6 +345,28 @@ to express a relationship between three resources. The first character in the
resource kind must be capitalized so that the parser can't ascertain
unambiguously that we are referring to a dependency relationship.
##### Edge naming
Each edge must have a unique name of type `str` that is used to uniquely
identify that edge, and can be used in the functioning of the edge at its
discretion.
Alternatively, the name value may be a list of strings `[]str` to build a list
of edges, each with a name from that list.
Using this construct is a veiled form of looping (iteration). This technique is
one of many ways you can perform iterative tasks that you might have
traditionally used a `for` loop for instead. This is preferred, because flow
control is error-prone and can make for less readable code.
The single `str` variation, may only be used when it is possible for the
compiler to determine statically that the value is of that type. Otherwise, it
will assume it to be a list of strings. Programmers should explicitly wrap their
variables in a string by interpolation to force this static `str` determination,
or in square brackets to force a list. The former is generally preferable
because it generates a smaller function graph since it doesn't need to build a
list.
#### Class
A class is a grouping structure that bind's a list of statements to a name in
@@ -561,7 +591,7 @@ Lexing is done using [nex](https://github.com/blynn/nex). It is a pure-golang
implementation which is similar to _Lex_ or _Flex_, but which produces golang
code instead of C. It integrates reasonably well with golang's _yacc_ which is
used for parsing. The token definitions are in:
[lang/lexer.nex](https://github.com/purpleidea/mgmt/tree/master/lang/lexer.nex).
[lang/lexer.nex](https://github.com/purpleidea/mgmt/tree/master/lang/parser/lexer.nex).
Lexing and parsing run together by calling the `LexParse` method.
#### Parsing
@@ -573,7 +603,7 @@ and trial and error. One small advantage yacc has over standard yacc is that it
can produce error messages from examples. The best documentation is to examine
the source. There is a short write up available [here](https://research.swtch.com/yyerror).
The yacc file exists at:
[lang/parser.y](https://github.com/purpleidea/mgmt/tree/master/lang/parser.y).
[lang/parser.y](https://github.com/purpleidea/mgmt/tree/master/lang/parser/parser.y).
Lexing and parsing run together by calling the `LexParse` method.
#### Interpolation
@@ -609,23 +639,27 @@ so that each `Expr` node in the AST knows what to expect. Type annotation is
allowed in situations when you want to explicitly specify a type, or when the
compiler cannot deduce it, however, most of it can usually be inferred.
For type inferrence to work, each node in the AST implements a `Unify` method
which is able to return a list of invariants that must hold true. This starts at
the top most AST node, and gets called through to it's children to assemble a
giant list of invariants. The invariants can take different forms. They can
specify that a particular expression must have a particular type, or they can
specify that two expressions must have the same types. More complex invariants
allow you to specify relationships between different types and expressions.
Furthermore, invariants can allow you to specify that only one invariant out of
a set must hold true.
For type inference to work, each `Stmt` node in the AST implements a `TypeCheck`
method which is able to return a list of invariants that must hold true. This
starts at the top most AST node, and gets called through to it's children to
assemble a giant list of invariants. The invariants all have the same form. They
specify that a particular expression corresponds to two particular types which
may both contain unification variables.
Each `Expr` node in the AST implements an `Infer` and `Check` method. The
`Infer` method returns the type of that node along with a list of invariants as
described above. Unification variables can of course be used throughout. The
`Check` method always uses a generic check implementation and generally doesn't
need to be implemented by the user.
Once the list of invariants has been collected, they are run through an
invariant solver. The solver can return either return successfully or with an
error. If the solver returns successfully, it means that it has found a trivial
error. If the solver returns successfully, it means that it has found a single
mapping between every expression and it's corresponding type. At this point it
is a simple task to run `SetType` on every expression so that the types are
known. If the solver returns in error, it is usually due to one of two
possibilities:
known. During this stage, each SetType method verifies that it's a compatible
type that it can use. If either that method or if the solver returns in error,
it is usually due to one of two possibilities:
1. Ambiguity
@@ -645,8 +679,8 @@ possibilities:
always happens if the user has made a type error in their program.
Only one solver currently exists, but it is possible to easily plug in an
alternate implementation if someone more skilled in the art of solver design
would like to propose a more logical or performant variant.
alternate implementation if someone wants to experiment with the art of solver
design and would like to propose a more logical or performant variant.
#### Function graph generation
@@ -687,8 +721,9 @@ If you'd like to create a built-in, core function, you'll need to implement the
function API interface named `Func`. It can be found in
[lang/interfaces/func.go](https://github.com/purpleidea/mgmt/tree/master/lang/interfaces/func.go).
Your function must have a specific type. For example, a simple math function
might have a signature of `func(x int, y int) int`. As you can see, all the
types are known _before_ compile time.
might have a signature of `func(x int, y int) int`. The simple functions have
their types known _before_ compile time. You may also include unification
variables in the function signature as long as the top-level type is a function.
A separate discussion on this matter can be found in the [function guide](function-guide.md).
@@ -716,6 +751,12 @@ added in the future. This method is usually called before any other, and should
not depend on any other method being called first. Other methods must not depend
on this method being called first.
If you use any unification variables in the function signature, then your
function will *not* be made available for use inside templates. This is a
limitation of the `golang` templating library. In the future if this limitation
proves to be significantly annoying, we might consider writing our own template
library.
#### Example
```golang
@@ -726,6 +767,18 @@ func (obj *FooFunc) Info() *interfaces.Info {
}
```
#### Example
This example contains unification variables.
```golang
func (obj *FooFunc) Info() *interfaces.Info {
return &interfaces.Info{
Sig: types.NewType("func(a ?1, b ?2, foo [?3]) ?1"),
}
}
```
### Init
```golang
@@ -788,49 +841,67 @@ Please see the example functions in
[lang/core/](https://github.com/purpleidea/mgmt/tree/master/lang/core/).
```
### Polymorphic Function API
### BuildableFunc Function API
For some functions, it might be helpful to be able to implement a function once,
but to have multiple polymorphic variants that can be chosen at compile time.
For this more advanced topic, you will need to use the
[Polymorphic Function API](#polymorphic-function-api). This will help with code
reuse when you have a small, finite number of possible type signatures, and also
for more complicated cases where you might have an infinite number of possible
type signatures. (eg: `[]str`, or `[][]str`, or `[][][]str`, etc...)
For some functions, it might be helpful to have a function which needs a "build"
step which is run after type unification. This step can be used to build the
function using the determined type, but it may also just be used for checking
that unification picked a valid solution.
Suppose you want to implement a function which can assume different type
signatures. The mgmt language does not support polymorphic types-- you must use
static types throughout the language, however, it is legal to implement a
function which can take different specific type signatures based on how it is
used. For example, you might wish to add a math function which could take the
form of `func(x int, x int) int` or `func(x float, x float) float` depending on
the input values. You might also want to implement a function which takes an
arbitrary number of input arguments (the number must be statically fixed at the
compile time of your program though) and which returns a string.
form of `func(x int, y int) int` or `func(x float, y float) float` depending on
the input values. For this case you could use a signature containing unification
variables, eg: `func(x ?1, y ?1) ?1`. At the end the buildable function would
need to check that it received a `?1` type of either `int` or `float`, since
this function might not support doing math on strings. Remember that type
unification can only return zero or one solutions, it's not possible to return
more than one, which is why this secondary validation step is a brilliant way to
filter out invalid solutions without needing to encode them as algebraic
conditions during the solver state, which would otherwise make it exponential.
The `PolyFunc` interface adds additional methods which you must implement to
satisfy such a function implementation. If you'd like to implement such a
function, then please notify the project authors, and they will expand this
section with a longer description of the process.
### InferableFunc Function API
#### Examples
You might also want to implement a function which takes an arbitrary number of
input arguments (the number must be statically fixed at the compile time of your
program though) and which returns a string or something else.
What follows are a few examples that might help you understand some of the
language details.
The `InferableFunc` interface adds ad additional `FuncInfer` method which you
must implement to satisfy such a function implementation. This lets you
dynamically generate a type signature (including unification variables) and a
list of invariants before running the type unification solver. It takes as input
a list of the statically known input types and input values (if any) and as well
the number of input arguments specified. This is usually enough information to
generate a fixed type signature of a fixed size.
##### Example Foo
TODO: please add an example here!
##### Example Bar
TODO: please add an example here!
Using this API should generally be pretty rare, but it is how certain special
functions such as `fmt.printf` are built. If you'd like to implement such a
function, then please notify the project authors as we're curious about your
use case.
## Frequently asked questions
(Send your questions as a patch to this FAQ! I'll review it, merge it, and
respond by commit with the answer.)
### Why am I getting a deploy.readfile error when the file actually exists?
You may be seeing an error like:
`readfile`: open /*/files/foo: file does not exist can't read file `/files/foo`?
If you look, the `foo` file is indeed in the `files/` directory. The problem is
that the `files/` directory won't be seen if you didn't specify to include it as
part of your deploy. To do so, chances are that all you need to do is add a
`metadata.yaml` file into the parent directory to that files folder. This will
be used as the entrypoint instead of the naked `main.mcl` file that you have
there, and with that metadata entrypoint, you get a default `files/` directory
added. You can of course change the `files/` path by setting a key in the
`metadata.yaml` file, but we recommend you leave it as the default.
### What is the difference between `ExprIf` and `StmtIf`?
The language contains both an `if` expression, and and `if` statement. An `if`

View File

@@ -21,15 +21,15 @@ if we missed something that you think is relevant!
| Felix Frank | blog | [Puppet Powered Mgmt (puppet to mgmt tl;dr)](https://ffrank.github.io/features/2016/06/19/puppet-powered-mgmt/) |
| James Shubin | blog | [Automatic clustering in mgmt](https://purpleidea.com/blog/2016/06/20/automatic-clustering-in-mgmt/) |
| James Shubin | video | [Recording from CoreOSFest 2016](https://www.youtube.com/watch?v=KVmDCUA42wc&html5=1) |
| James Shubin | video | [Recording from DebConf16](http://meetings-archive.debian.net/pub/debian-meetings/2016/debconf16/Next_Generation_Config_Mgmt.webm) ([Slides](https://annex.debconf.org//debconf-share/debconf16/slides/15-next-generation-config-mgmt.pdf)) |
| James Shubin | video | [Recording from DebConf16](http://meetings-archive.debian.net/pub/debian-meetings/2016/debconf16/Next_Generation_Config_Mgmt.webm) |
| Felix Frank | blog | [Edging It All In (puppet and mgmt edges)](https://ffrank.github.io/features/2016/07/12/edging-it-all-in/) |
| Felix Frank | blog | [Translating All The Things (puppet to mgmt translation warnings)](https://ffrank.github.io/features/2016/08/19/translating-all-the-things/) |
| James Shubin | video | [Recording from systemd.conf 2016](https://www.youtube.com/watch?v=jB992Zb3nH0&html5=1) |
| James Shubin | video | [Recording from systemd.conf 2016](https://www.youtube.com/watch?v=_TowsFAWWRA) |
| James Shubin | blog | [Remote execution in mgmt](https://purpleidea.com/blog/2016/10/07/remote-execution-in-mgmt/) |
| James Shubin | video | [Recording from High Load Strategy 2016](https://vimeo.com/191493409) |
| James Shubin | video | [Recording from NLUUG 2016](https://www.youtube.com/watch?v=MmpwOQAb_SE&html5=1) |
| James Shubin | video | [Recording from High Load Strategy 2016](https://www.youtube.com/watch?v=-4g14KUVPVk) |
| James Shubin | video | [Recording from NLUUG 2016](https://www.youtube.com/watch?v=0vO93ni1zos) |
| James Shubin | blog | [Send/Recv in mgmt](https://purpleidea.com/blog/2016/12/07/sendrecv-in-mgmt/) |
| Julien Pivotto | blog | [Augeas resource for mgmt](https://roidelapluie.be/blog/2017/02/14/mgmt-augeas/) |
| Julien Pivotto | blog | [Augeas resource for mgmt](https://purpleidea.com/cached/mgmt-augeas.html) (Cached from: https://roidelapluie.be/blog/2017/02/14/mgmt-augeas/) |
| James Shubin | blog | [Metaparameters in mgmt](https://purpleidea.com/blog/2017/03/01/metaparameters-in-mgmt/) |
| James Shubin | video | [Recording from Incontro DevOps 2017](https://vimeo.com/212241877) |
| Yves Brissaud | blog | [mgmt aux HumanTalks Grenoble (french)](http://log.winsos.net/2017/04/12/mgmt-aux-human-talks-grenoble.html) |
@@ -59,3 +59,5 @@ if we missed something that you think is relevant!
| James Shubin | video | [Recording from CfgMgmtCamp.eu 2023](https://www.youtube.com/watch?v=FeRGRj8w0BU) |
| James Shubin | video | [Recording from FOSDEM 2024, Golang Devroom](https://video.fosdem.org/2024/ud2218a/fosdem-2024-2575-single-binary-full-stack-provisioning.mp4) |
| James Shubin | video | [Recording from CfgMgmtCamp.eu 2024](https://www.youtube.com/watch?v=vBt9lpGD4bc) |
| James Shubin | blog | [Mgmt Configuration Language: Functions](https://purpleidea.com/blog/2024/11/22/functions-in-mgmt/) |
| James Shubin | blog | [Modules and imports in mgmt](https://purpleidea.com/blog/2024/12/03/modules-and-imports-in-mgmt/) |

316
docs/puppet-guide.md Normal file
View File

@@ -0,0 +1,316 @@
# Puppet guide
`mgmt` can use Puppet as its source for the configuration graph.
This document goes into detail on how this works, and lists
some pitfalls and limitations.
For basic instructions on how to use the Puppet support, see
the [main documentation](documentation.md#puppet-support).
## Prerequisites
You need Puppet installed in your system. It is not important how you
get it. On the most common Linux distributions, you can use packages
from the OS maintainer, or upstream Puppet repositories. An alternative
that will also work on OSX is the `puppet` Ruby gem. It also has the
advantage that you can install any desired version in your home directory
or any other location.
Any release of Puppet's 3.x and 4.x series should be suitable for use with
`mgmt`. Most importantly, make sure to install the `ffrank-mgmtgraph` Puppet
module (referred to below as "the translator module").
```
puppet module install ffrank-mgmtgraph
```
Please note that the module is not required on your Puppet master (if you
use a master/agent setup). It's needed on the machine that runs `mgmt`.
You can install the module on the master anyway, so that it gets distributed
to your agents through Puppet's `pluginsync` mechanism.
### Testing the Puppet side
The following command should run successfully and print a YAML hash on your
terminal:
```puppet
puppet mgmtgraph print --code 'file { "/tmp/mgmt-test": ensure => present }'
```
You can use this CLI to test any manifests before handing them straight
to `mgmt`.
## Writing a suitable manifest
### Unsupported attributes
`mgmt` inherited its resource module from Puppet, so by and large, it's quite
possible to express `mgmt` graphs in terms of Puppet manifests. However,
there isn't (and likely never will be) full feature parity between the
respective resource types. In consequence, a manifest can have semantics that
cannot be transferred to `mgmt`.
For example, at the time of writing this, the `file` type in `mgmt` had no
notion of permissions (the file `mode`) yet. This lead to the following
warning (among others that will be discussed below):
```
$ puppet mgmtgraph print --code 'file { "/tmp/foo": mode => "0600" }'
Warning: cannot translate: File[/tmp/foo] { mode => "600" } (attribute is ignored)
```
This is a heads-up for the user, because the resulting `mgmt` graph will
in fact not pass this information to the `/tmp/foo` file resource, and
`mgmt` will ignore this file's permissions. Including such attributes in
manifests that are written expressly for `mgmt` is not sensible and should
be avoided.
### Unsupported resources
Puppet has a fairly large number of
[built-in types](https://www.puppet.com/docs/puppet/8/cheatsheet_core_types.html),
and countless more are available through
[modules](https://forge.puppet.com/). It's unlikely that all of them will
eventually receive native counterparts in `mgmt`.
When encountering an unknown resource, the translator module will replace
it with an `exec` resource in its output. This resource will run the equivalent
of a `puppet resource` command to make Puppet apply the original resource
itself. This has quite abysmal performance, because processing such a
resource requires the forking of at least one Puppet process (two if it
is found to be out of sync). This comes with considerable overhead.
On most systems, starting up any Puppet command takes several seconds.
Compared to the split second that the actual work usually takes,
this overhead can amount to several orders of magnitude.
Avoid Puppet types that `mgmt` does not implement (yet).
### Avoiding common warnings
Many resource parameters in Puppet take default values. For the most part,
the translator module just ignores them. However, there are cases in which
Puppet will default to convenient behavior that `mgmt` cannot quite replicate.
For example, translating a plain `file` resource will lead to a warning message:
```
$ puppet mgmtgraph print --code 'file { "/tmp/mgmt-test": }'
Warning: File[/tmp/mgmt-test] uses the 'puppet' file bucket, which mgmt cannot
do. There will be no backup copies!
```
The reason is that per default, Puppet assumes the following parameter value
(among others)
```puppet
file { "/tmp/mgmt-test":
backup => 'puppet',
}
```
To avoid this, specify the parameter explicitly:
```bash
puppet mgmtgraph print --code 'file { "/tmp/mgmt-test": backup => false }'
```
This is tedious in a more complex manifest. A good simplification is the
following [resource default](https://www.puppet.com/docs/puppet/8/lang_defaults)
anywhere on the top scope of your manifest:
```puppet
File { backup => false }
```
If you encounter similar warnings from other types and/or parameters,
use the same approach to silence them if possible.
## Configuring Puppet
Since `mgmt` uses an actual Puppet CLI behind the scenes, you might
need to tweak some of Puppet's runtime options in order to make it
do what you want. Reasons for this could be among the following:
* You use the `--puppet agent` variant and need to configure
`servername`, `certname` and other master/agent-related options.
* You don't want runtime information to end up in the `vardir`
that is used by your regular `puppet agent`.
* You install specific Puppet modules for `mgmt` in a non-standard
location.
`mgmt` exposes only one Puppet option in order to allow you to
control all of them, through its `--puppet-conf` option. It allows
you to specify which `puppet.conf` file should be used during
translation.
```
mgmt run puppet --puppet /opt/my-manifest.pp --puppet-conf /etc/mgmt/puppet.conf
```
Within this file, you can just specify any needed options in the
`[main]` section:
```
[main]
server=mgmt-master.example.net
vardir=/var/lib/mgmt/puppet
```
## Caveats
Please see the [README](https://github.com/ffrank/puppet-mgmtgraph/blob/master/README.md)
of the translator module for the current state of supported and unsupported
language features.
You should probably make sure to always use the latest release of
both `ffrank-mgmtgraph` and `ffrank-yamlresource` (the latter is
getting pulled in as a dependency of the former).
## Using Puppet in conjunction with the mcl lang
The graph that Puppet generates for `mgmt` can be united with a graph
that is created from native `mgmt` code in its mcl language. This is
useful when you are in the process of replacing Puppet with mgmt. You
can translate your custom modules into mgmt's language one by one,
and let mgmt run the current mix.
Instead of the usual `--puppet-conf` flag and argv for `puppet` and `mcl` input,
you need to use alternative flags to make this work:
* `--lp-lang` to specify the mcl input
* `--lp-puppet` to specify the puppet input
* `--lp-puppet-conf` to point to the optional puppet.conf file
`mgmt` will derive a graph that contains all edges and vertices from
both inputs. You essentially get two unrelated subgraphs that run in
parallel. To form edges between these subgraphs, you have to define
special vertices that will be merged. This works through a hard-coded
naming scheme.
### Mixed graph example 1 - No merges
```mcl
# lang
file "/tmp/mgmt_dir/" { state => "present" }
file "/tmp/mgmt_dir/a" { state => "present" }
```
```puppet
# puppet
file { "/tmp/puppet_dir": ensure => "directory" }
file { "/tmp/puppet_dir/a": ensure => "file" }
```
These very simple inputs (including implicit edges from directory to
respective file) result in two subgraphs that do not relate.
```
File[/tmp/mgmt_dir/] -> File[/tmp/mgmt_dir/a]
File[/tmp/puppet_dir] -> File[/tmp/puppet_dir/a]
```
### Mixed graph example 2 - Merged vertex
In order to have merged vertices in the resulting graph, you will
need to include special resources and classes in the respective
input code.
* On the lang side, add `noop` resources with names starting in `puppet_`.
* On the Puppet side, add **empty** classes with names starting in `mgmt_`.
```mcl
# lang
noop "puppet_handover_to_mgmt" {}
file "/tmp/mgmt_dir/" { state => "present" }
file "/tmp/mgmt_dir/a" { state => "present" }
Noop["puppet_handover_to_mgmt"] -> File["/tmp/mgmt_dir/"]
```
```puppet
# puppet
class mgmt_handover_to_mgmt {}
include mgmt_handover_to_mgmt
file { "/tmp/puppet_dir": ensure => "directory" }
file { "/tmp/puppet_dir/a": ensure => "file" }
File["/tmp/puppet_dir/a"] -> Class["mgmt_handover_to_mgmt"]
```
The new `noop` resource is merged with the new class, resulting in
the following graph:
```
File[/tmp/puppet_dir] -> File[/tmp/puppet_dir/a]
|
V
Noop[handover_to_mgmt]
|
V
File[/tmp/mgmt_dir/] -> File[/tmp/mgmt_dir/a]
```
You put all your ducks in a row, and the resources from the Puppet input
run before those from the mcl input.
**Note:** The names of the `noop` and the class must be identical after the
respective prefix. The common part (here, `handover_to_mgmt`) becomes the name
of the merged resource.
## Mixed graph example 3 - Multiple merges
In most scenarios, it will not be possible to define a single handover
point like in the previous example. For example, if some Puppet resources
need to run in between two stages of native resources, you need at least
two merged vertices:
```mcl
# lang
noop "puppet_handover" {}
noop "puppet_handback" {}
file "/tmp/mgmt_dir/" { state => "present" }
file "/tmp/mgmt_dir/a" { state => "present" }
file "/tmp/mgmt_dir/puppet_subtree/state-file" { state => "present" }
File["/tmp/mgmt_dir/"] -> Noop["puppet_handover"]
Noop["puppet_handback"] -> File["/tmp/mgmt_dir/puppet_subtree/state-file"]
```
```puppet
# puppet
class mgmt_handover {}
class mgmt_handback {}
include mgmt_handover, mgmt_handback
class important_stuff {
file { "/tmp/mgmt_dir/puppet_subtree":
ensure => "directory"
}
# ...
}
Class["mgmt_handover"] -> Class["important_stuff"] -> Class["mgmt_handback"]
```
The resulting graph looks roughly like this:
```
File[/tmp/mgmt_dir/] -> File[/tmp/mgmt_dir/a]
|
V
Noop[handover] -> ( class important_stuff resources )
|
V
Noop[handback]
|
V
File[/tmp/mgmt_dir/puppet_subtree/state-file]
```
You can add arbitrary numbers of merge pairs to your code bases,
with relationships as needed. From our limited experience, code
readability suffers quite a lot from these, however. We advise
to keep these structures simple.

View File

@@ -21,8 +21,6 @@ to build your own.
### Downloading a pre-built release:
This method is not recommended because those packages are now very old.
The latest releases can be found [here](https://github.com/purpleidea/mgmt/releases/).
An alternate mirror is available [here](https://dl.fedoraproject.org/pub/alt/purpleidea/mgmt/releases/).
@@ -39,7 +37,7 @@ You'll need some dependencies, including `golang`, and some associated tools.
#### Installing golang
* You need golang version 1.20 or greater installed.
* You need a modern golang version 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)
@@ -103,13 +101,14 @@ This method avoids polluting your workstation with the dependencies for the
build. Here is an example using Fedora, Podman and Buildah:
```shell
git clone --recursive https://github.com/purpleidea/mgmt/ ~/mgmt/
cd ~/mgmt/docker
buildah build -f Dockerfile-fedora.build -t mgmt_build
podman run -d -it --name mgmt_build localhost/mgmt_build
podman cp mgmt_build:/src/github.com/purpleidea/mgmt/mgmt /tmp/mgmt
sudo mv /tmp/mgmt /usr/local/bin # be sure this is in your $PATH
sudo chown root:root /usr/local/bin/mgmt
git clone --recursive https://github.com/purpleidea/mgmt/
cd mgmt
docker build -t mgmt -f docker/Dockerfile .
docker run --rm --entrypoint cat mgmt mgmt > mgmt
chmod +x mgmt
./mgmt --version
# you could now copy the mgmt binary somewhere into your $PATH
# e.g., /usr/local/bin/ to make it accessible from anywhere
```
## Running mgmt

142
docs/release-notes/0.0.26 Normal file
View File

@@ -0,0 +1,142 @@
I've just released version 0.0.26 of mgmt!
> 16 files changed, 869 insertions(+), 181 deletions(-)
Hot off the heels of the recent large release (0.0.25) I've just
released an incremental update...
See more here:
https://purpleidea.com/blog/2024/03/27/a-new-provisioning-tool/
With that, here are a few highlights from the release:
* We have a new mgmt partner program. Please sign-up for early access
to these release notes, along with other special privileges. Details
at: https://bit.ly/mgmt-partner-program
* Type unification for the provisioning tool is about 40x faster.
* We fix a small bug related to the upcoming fedora 40 release.
And much more...
DOWNLOAD
Prebuilt binaries are available here for this release:
https://github.com/purpleidea/mgmt/releases/tag/0.0.26
They can also be found on the Fedora mirror:
https://dl.fedoraproject.org/pub/alt/purpleidea/mgmt/releases/0.0.26/
NEWS
* Added old release notes into git
* We now skip over unreleased Fedora versions (like "40 Beta") when
trying to automatically determine the latest stable release.
* Type unification was structurally refactored to make way for a bunch
of future improvements and generally to modernize the code.
* Added some unification optimizations and a unification flag
optimizations system to allow solvers to support special flags. One of
these new flags was used for the provisioner code with a substantial
improvement in type unification time by about 40x.
* New cli args are also available for using these flags.
* We're looking for help writing Amazon, Google, DigitalOcean, Hetzner,
etc, resources if anyone is interested, reach out to us. Particularly
if there is support from those organizations as well.
* Many other bug fixes, changes, etc...
* See the git log for more NEWS, and for anything notable I left out!
BUGS/TODO
* Function values getting _passed_ to resources doesn't work yet, but
it's not a blocker, but it would definitely be useful. We're looking
into it.
* Function graphs are unnecessarily dynamic. We might make them more
static so that we don't need as many transactions. This is really a
compiler optimization and not a bug, but it's something important we'd
like to have.
* Running two Txn's during the same pause would be really helpful. I'm
not sure how much of a performance improvement we'd get from this, but
it would sure be interesting to build. If you want to build a fancy
synchronization primitive, then let us know! Again this is not a bug.
* General type unification performance can be improved drastically. I
will have to implement the fast algorithm so that we can scale to very
large mcl programs. Help is wanted if you are familiar with "unionfind"
and/or type unification.
TALKS
I don't have anything planned until CfgMgmtCamp 2025. If you'd like to
book me for a private event, or sponsor my travel for your conference,
please let me know.
I recently gave two talks: one at CfgMgmtCamp 2024, and one at FOSDEM
in the golang room. Both are available online and demonstrated an
earlier version of the provisioning tool which is fully available
today. The talks can be found here: https://purpleidea.com/talks/
PARTNER PROGRAM
We have a new mgmt partner program which gets you early access to
releases, bug fixes, support, and many other goodies. Please sign-up
today: https://bit.ly/mgmt-partner-program
MISC
Our mailing list host (Red Hat) is no longer letting non-Red Hat
employees use their infrastructure. We're looking for a new home. I've
opened a ticket with Freedesktop. If you have any sway with them or
other recommendations, please let me know:
https://gitlab.freedesktop.org/freedesktop/freedesktop/-/issues/1082
We're still looking for new contributors, and there are easy, medium
and hard issues available! You're also welcome to suggest your own!
Please join us in #mgmtconfig on Libera IRC or Matrix (preferred) and
ping us if you'd like help getting started! For details please see:
https://github.com/purpleidea/mgmt/blob/master/docs/faq.md#how-do-i-con
tribute-to-the-project-if-i-dont-know-golang
Many tagged #mgmtlove issues exist:
https://github.com/purpleidea/mgmt/issues?q=is%3Aissue+is%3Aopen+label%
3Amgmtlove
Although asking in IRC/matrix is the best way to find something to work
on.
MENTORING
We offer mentoring for new golang/mgmt hackers who want to get
involved. This is fun and friendly! You get to improve your skills,
and we get some patches in return. Ping me off-list for details.
THANKS
Thanks (alphabetically) to everyone who contributed to the latest
release:
James Shubin
We had 1 unique committers since 0.0.25, and have had 90 overall.
Happy hacking,
James
@purpleidea

View File

@@ -60,7 +60,10 @@ it is not specified, but others cannot, and some might poorly infer if the
struct name is ambiguous.
If you'd like your resource to be accessible by the `YAML` graph API (GAPI),
then you'll need to include the appropriate YAML fields as shown below.
then you'll need to include the appropriate YAML fields as shown below. This is
used by the `puppet` compiler as well, so make sure you include these struct
tags if you want existing `puppet` code to be able to run using the `mgmt`
engine.
#### Example
@@ -620,7 +623,7 @@ func init() { // special golang method that runs once
To support YAML unmarshalling for your resource, you must implement an
additional method. It is recommended if you want to use your resource with the
`yaml` compiler.
`puppet` compiler.
```golang
UnmarshalYAML(unmarshal func(interface{}) error) error // optional
@@ -713,7 +716,7 @@ Higher level resource collections will be possible once the `mgmt` DSL is ready.
### Why does the resource API have `CheckApply` instead of two separate methods?
In an early version we actually had both "parts" as separate methods, namely:
`StateOK` (Check) and `Apply`, but the [decision](58f41eddd9c06b183f889f15d7c97af81b0331cc)
`StateOK` (Check) and `Apply`, but the [decision](https://github.com/purpleidea/mgmt/commit/58f41eddd9c06b183f889f15d7c97af81b0331cc)
was made to merge the two into a single method. There are two reasons for this:
1. Many situations would involve the engine running both `Check` and `Apply`. If

145
docs/service-guide.md Normal file
View File

@@ -0,0 +1,145 @@
# Service API design guide
This document is intended as a short instructional design guide in building a
service management API. It is certainly intended for someone who wishes to use
`mgmt` resources and functions to interact with their facilities, however it may
be of more general use as well. Hopefully this will help you make smarter design
considerations early on, and prevent some amount of unnecessary technical debt.
## Main aspects
What follows are some of the most common considerations which you may wish to
take into account when building your service. This list is non-exhaustive. Of
particular note, as of the writing of this document, many of these designs are
not taken into account or not well-handled or implemented by the major API
("cloud") providers.
### Authentication
#### The status-quo
Many services naturally require you to authenticate yourself. Usually the
initial user who sets up the account and provides credit card details will need
to download secret credentials in order to access the service. The onus is on
the user to keep those credentials private, and to prevent leaking them. It is
convenient (and insecure) to store them in `git` repositories containing scripts
and configuration management code. Since it's likely you will use multiple
different services, it also means you will have a ton of different credentials
to guard.
#### An alternative
Instead, build your service to accept a public key that you store in the users
account. Only consumers that can correctly sign messages matching this public
key should be authorized. This mechanism is well-understood by anyone who has
ever uploaded their public SSH key to a server. You can use SSH keys, GPG keys,
or even get into Kerberos if that's appropriate. Best of all, if you and other
services use a standardized mechanism like GPG, a user might only need to keep
track of their single key-pair, even when they're using multiple services!
### Events
#### The problem
People have been building "[CRUD](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete)"
and "[REST](https://en.wikipedia.org/wiki/REST)"ful API's for years. The biggest
missing part that most of them don't provide is events. If users want to know
when a resource changes, they have to repeatedly poll the server, which is both
network intensive, and introduces latency. When services were simpler, this
wasn't as much of a consideration, but these days it matters. An embarrassingly
small number of major software vendors implement these correctly, if at all.
#### Why events?
The `mgmt` tool is different from most other static tools in that it allows
reading streams of incoming data, and stream of change events from resources we
are managing. If an event API is not available, we can still poll, but this is
not as desirable. An event-capable API doesn't prevent polling if that's
preferred, you can always repeat a read request periodically.
#### Variants
The two common mechanisms for receiving events are "callbacks" and
"long-polling". In the former, the service contacts the consumer when something
happens. In the latter, the consumer opens a connection, and the service either
closes the connection or sends the reply, when it's ready. Long-polling is often
preferred since it doesn't require an open firewall on the consumers side.
Callbacks are preferred because it's often cheaper for the service to implement
that. It's also less reliable since it's hard to know if the callback message
wasn't received because it was dropped, or if there just wasn't an event. And it
requires static timeouts when retrying a callback message, and so on. It's best
to implement long-polling or something equivalent at a minimum.
#### "Since" requests
When making an event request, some API's will let you tack on a "since" style
parameter that tells the endpoint that we're interested in all of the events
_since_ a particular timestamp, or _since_ a particular sequence ID. This can be
very useful if missing an intermediate event is a concern. Implement this if you
can, but it's better for all concerned if purely declarative facilities are all
that is required. It also forces the endpoint to maintain some state, which may
be undesirable for them.
#### Out of band
Some providers have the event system tacked on to a separate facility. If it's
not part of the core API, then it's not useful. You shouldn't have to configure
a separate system in order to start getting events.
### Batching
With so many resources, you might expect to have 1000's of long-polling
connections all sitting open and idle. That can't be efficient! It's not, which
is why good API's need a batching facility. This lets the consumer group
together many watches (all waiting on a long-poll) inside of a single call. That
way, a single connection might only be needed for a large amount of information.
### Don't auto-generate junk
Please build an elegant API. Many services auto-generate a "phone book" SDK of
junk. It might seem inevitable, so if you absolutely need to do this, then put
some extra effort into making it idiomatic. If I'm using an SDK generated for
`golang` and I see an internal `foo.String` wrapper, then chances are you have
designed your API and code to be easier to maintain for you, instead of
prioritizing your customers. Surely the total volume of all customer code is
more than your own, so why optimize for that instead of the putting the customer
first?
### Resources and functions
`Mgmt` has a concept of "resources" and "functions". Resources are used in an
idempotent model to express desired state and perform that work, and "functions"
are used to receive and pull data into the system. That separation has shown to
be an elegant one. Consider it when designing your API's. For example, if some
vital information can only be obtained after performing a modifying operation,
then it might signal that you're missing some sort of a lookup or event-log
system. Design your API's to be idempotent, this solves many distributed-system
problems involving receiving duplicate messages, and so on.
## Using mgmt as a library
Instead of building a new service from scratch, and re-inventing the typical
management and CLI layer, consider using `mgmt` as a library, and directly
benefiting from that work. This has not been done for a large production
service, but the author believes it would be quite efficient, particularly if
your application is written in golang. It's equivalently easy to do it for other
languages as well, you just end up with two binaries instead of one. (Or you can
embed the other binary into the new golang management tool.)
## Cloud API considerations
Many "cloud" companies have a lot of technical debt and a lot of customers. As a
result, it might be very hard for them to improve their API's, particularly
without breaking compatibility promises for their existing customers. As a
result, they should either add a versioned API, which lets newer consumers get
the benefit, or add new parallel services which offer the modern features. If
they don't, the only solution is for new competitors to build-in these better
efficiencies, eventually offering better value to cost ratios, which will then
make legacy products less lucrative and therefore unmaintainable as compared to
their competitors.
## Suggestions
If you have any ideas for suggestions or other improvements to this guide,
please let us know! I hope this was helpful. Please reach out if you are
building an API that you might like to have `mgmt` consume!

View File

@@ -67,6 +67,37 @@ 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 pointers
You almost always want any method receivers to be declared on the pointer to the
struct. There are only a few rare situations where this is not the case. This
makes it easier to merge future changes that mutate the state without wondering
why you now have two different copies of a struct. When you do need to copy a
a struct, you can add a `Copy()` method to it. It's true that in many situations
adding the pointer adds a small performance penalty, but we haven't found them
to be significant in practice. If you do have a performance sensitive patch
which benefits from skipping the pointer, please demonstrate this need with
data first.
#### Example
```golang
type Foo struct {
Whatever string
// ...
}
// Bar is implemented correctly as a pointer on Foo.
func (obj *Foo) Bar(baz string) int {
// ...
}
// Bar is implemented *incorrectly* without a pointer to Foo.
func (obj Foo) Bar(baz string) int {
// ...
}
```
### Method receiver naming
[Contrary](https://github.com/golang/go/wiki/CodeReviewComments#receiver-names)

103
docs/util/metadata.go Normal file
View File

@@ -0,0 +1,103 @@
// Mgmt
// Copyright (C) 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 <https://www.gnu.org/licenses/>.
//
// Additional permission under GNU GPL version 3 section 7
//
// If you modify this program, or any covered work, by linking or combining it
// with embedded mcl code and modules (and that the embedded mcl code and
// modules which link with this program, contain a copy of their source code in
// the authoritative form) containing parts covered by the terms of any other
// license, the licensors of this program grant you additional permission to
// convey the resulting work. Furthermore, the licensors of this program grant
// the original author, James Shubin, additional permission to update this
// additional permission if he deems it necessary to achieve the goals of this
// additional permission.
// Package util handles metadata for documentation generation.
package util
import (
"fmt"
)
var (
registeredResourceMetadata = make(map[string]*Metadata) // must initialize
registeredFunctionMetadata = make(map[string]*Metadata) // must initialize
)
// RegisterResource records the metadata for a resource of this kind.
func RegisterResource(kind string, metadata *Metadata) error {
if _, exists := registeredResourceMetadata[kind]; exists {
return fmt.Errorf("metadata kind %s is already registered", kind)
}
registeredResourceMetadata[kind] = metadata
return nil
}
// LookupResource looks up the metadata for a resource of this kind.
func LookupResource(kind string) (*Metadata, error) {
metadata, exists := registeredResourceMetadata[kind]
if !exists {
return nil, fmt.Errorf("not found")
}
return metadata, nil
}
// RegisterFunction records the metadata for a function of this name.
func RegisterFunction(name string, metadata *Metadata) error {
if _, exists := registeredFunctionMetadata[name]; exists {
return fmt.Errorf("metadata named %s is already registered", name)
}
registeredFunctionMetadata[name] = metadata
return nil
}
// LookupFunction looks up the metadata for a function of this name.
func LookupFunction(name string) (*Metadata, error) {
metadata, exists := registeredFunctionMetadata[name]
if !exists {
return nil, fmt.Errorf("not found")
}
return metadata, nil
}
// Metadata stores some additional information about the function or resource.
// This is used to automatically generate documentation.
type Metadata struct {
// Filename is the filename (without any base dir path) that this is in.
Filename string
// Typename is the string name of the main resource struct or function.
Typename string
}
// GetMetadata returns some metadata about the func. It can be called at any
// time. This must not be named the same as the struct it's on or using it as an
// anonymous embedded struct will stop us from being able to call this method.
func (obj *Metadata) GetMetadata() *Metadata {
//if obj == nil { // TODO: Do I need this?
// return nil
//}
return &Metadata{
Filename: obj.Filename,
Typename: obj.Typename,
}
}

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2024+ James Shubin and the project contributors
// Copyright (C) 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-2024+ James Shubin and the project contributors
// Copyright (C) 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-2024+ James Shubin and the project contributors
// Copyright (C) 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-2024+ James Shubin and the project contributors
// Copyright (C) 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-2024+ James Shubin and the project contributors
// Copyright (C) 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-2024+ James Shubin and the project contributors
// Copyright (C) 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-2024+ James Shubin and the project contributors
// Copyright (C) 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-2024+ James Shubin and the project contributors
// Copyright (C) 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-2024+ James Shubin and the project contributors
// Copyright (C) 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-2024+ James Shubin and the project contributors
// Copyright (C) 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
@@ -192,11 +192,15 @@ func (obj *Engine) Process(ctx context.Context, vertex pgraph.Vertex) error {
} else {
// run the CheckApply!
if obj.Debug {
obj.Logf("%s: CheckApply(%t)", res, !noop)
}
// if this fails, don't UpdateTimestamp()
checkOK, err = res.CheckApply(ctx, !noop)
if !checkOK && obj.Debug { // don't log on (checkOK == true)
obj.Logf("%s: CheckApply(%t): Return(%t, %s)", res, !noop, checkOK, engineUtil.CleanError(err))
}
}
if checkOK && err != nil { // should never return this way
return fmt.Errorf("%s: resource programming error: CheckApply(%t): %t, %+v", res, !noop, checkOK, err)

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2024+ James Shubin and the project contributors
// Copyright (C) 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-2024+ James Shubin and the project contributors
// Copyright (C) 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 @@ import (
// AutoEdge adds the automatic edges to the graph.
func AutoEdge(graph *pgraph.Graph, debug bool, logf func(format string, v ...interface{})) error {
logf("adding autoedges...")
logf("building...")
// initially get all of the autoedges to seek out all possible errors
var err error
@@ -63,7 +63,9 @@ func AutoEdge(graph *pgraph.Graph, debug bool, logf func(format string, v ...int
continue
}
if autoEdgeObj == nil {
if debug {
logf("no auto edges were found for: %s", res)
}
continue // next vertex
}
autoEdgeObjMap[res] = autoEdgeObj // save for next loop
@@ -86,9 +88,9 @@ func AutoEdge(graph *pgraph.Graph, debug bool, logf func(format string, v ...int
break // inner loop
}
if debug {
logf("autoedge: UIDS:")
logf("UIDS:")
for i, u := range uids {
logf("autoedge: UID%d: %v", i, u)
logf("UID%d: %v", i, u)
}
}
@@ -129,7 +131,7 @@ func addEdgesByMatchingUIDS(res engine.EdgeableRes, uids []engine.ResUID, graph
continue
}
if debug {
logf("autoedge: Match: %s with UID: %s", r, uid)
logf("match: %s with UID: %s", r, uid)
}
// we must match to an effective UID for the resource,
// that is to say, the name value of a res is a helpful
@@ -138,13 +140,13 @@ func addEdgesByMatchingUIDS(res engine.EdgeableRes, uids []engine.ResUID, graph
if UIDExistsInUIDs(uid, r.UIDs()) {
// add edge from: r -> res
if uid.IsReversed() {
txt := fmt.Sprintf("%s -> %s (autoedge)", r, res)
logf("autoedge: adding: %s", txt)
txt := fmt.Sprintf("%s -> %s", r, res)
logf("adding: %s", txt)
edge := &engine.Edge{Name: txt}
graph.AddEdge(r, res, edge)
} else { // edges go the "normal" way, eg: pkg resource
txt := fmt.Sprintf("%s -> %s (autoedge)", res, r)
logf("autoedge: adding: %s", txt)
txt := fmt.Sprintf("%s -> %s", res, r)
logf("adding: %s", txt)
edge := &engine.Edge{Name: txt}
graph.AddEdge(res, r, edge)
}

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2024+ James Shubin and the project contributors
// Copyright (C) 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-2024+ James Shubin and the project contributors
// Copyright (C) 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-2024+ James Shubin and the project contributors
// Copyright (C) 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-2024+ James Shubin and the project contributors
// Copyright (C) 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-2024+ James Shubin and the project contributors
// Copyright (C) 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-2024+ James Shubin and the project contributors
// Copyright (C) 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-2024+ James Shubin and the project contributors
// Copyright (C) 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,7 +106,8 @@ func (obj *Engine) Init() error {
if obj.Prefix == "" || obj.Prefix == "/" {
return fmt.Errorf("the prefix of `%s` is invalid", obj.Prefix)
}
if err := os.MkdirAll(obj.Prefix, 0770); err != nil {
// 0775 since we want children to be able to read this!
if err := os.MkdirAll(obj.Prefix, 0775); err != nil {
return errwrap.Wrapf(err, "can't create prefix")
}
@@ -224,7 +225,7 @@ func (obj *Engine) Commit() error {
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 {
//if err := os.MkdirAll(statePrefix, 0775); err != nil {
// return errwrap.Wrapf(err, "can't create state prefix")
//}
@@ -268,7 +269,7 @@ func (obj *Engine) Commit() error {
obj.wlock.Unlock()
}()
if obj.Debug || true {
if obj.Debug {
obj.Logf("%s: Working...", v)
}
// contains the Watch and CheckApply loops

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2024+ James Shubin and the project contributors
// Copyright (C) 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-2024+ James Shubin and the project contributors
// Copyright (C) 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-2024+ James Shubin and the project contributors
// Copyright (C) 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-2024+ James Shubin and the project contributors
// Copyright (C) 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-2024+ James Shubin and the project contributors
// Copyright (C) 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-2024+ James Shubin and the project contributors
// Copyright (C) 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-2024+ James Shubin and the project contributors
// Copyright (C) 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-2024+ James Shubin and the project contributors
// Copyright (C) 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,8 @@ func (obj *State) varDir(extra string) (string, error) {
// an empty string at the end has no effect
p := fmt.Sprintf("%s/", path.Join(obj.Prefix, extra))
if err := os.MkdirAll(p, 0770); err != nil {
// 0775 since we want children to be able to read this!
if err := os.MkdirAll(p, 0775); err != nil {
return "", errwrap.Wrapf(err, "can't create prefix in: %s", p)
}

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2024+ James Shubin and the project contributors
// Copyright (C) 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-2024+ James Shubin and the project contributors
// Copyright (C) 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
@@ -38,6 +38,7 @@ import (
"fmt"
"os"
"path"
"strconv"
"strings"
"sync"
@@ -54,7 +55,15 @@ type API struct {
Logf func(format string, v ...interface{})
// Each piece of the API can take a handle here.
*Value
*Value // TODO: Rename to ValueImpl?
// VarDirImpl is the implementation for the VarDir API's. The API's are
// the collection of public methods that exist on this struct.
*VarDirImpl
// PoolImpl is the implementation for the Pool API's. The API's are the
// collection of public methods that exist on this struct.
*PoolImpl
}
// Init initializes the API before first use. It returns itself so it can be
@@ -67,6 +76,20 @@ func (obj *API) Init() *API {
Logf: obj.Logf,
})
obj.VarDirImpl = &VarDirImpl{}
obj.VarDirImpl.Init(&VarDirInit{
Prefix: obj.Prefix,
Debug: obj.Debug,
Logf: obj.Logf,
})
obj.PoolImpl = &PoolImpl{}
obj.PoolImpl.Init(&PoolInit{
Prefix: obj.Prefix,
Debug: obj.Debug,
Logf: obj.Logf,
})
return obj
}
@@ -332,3 +355,240 @@ func valueRemove(ctx context.Context, prefix, key string) error {
}
return nil // ignore not found errors
}
// VarDirInit are the init values that the VarDir API needs to work correctly.
type VarDirInit struct {
Prefix string
Debug bool
Logf func(format string, v ...interface{})
}
// VarDirImpl is the implementation for the VarDir API's. The API's are the
// collection of public methods that exist on this struct.
type VarDirImpl struct {
init *VarDirInit
mutex *sync.Mutex
prefix string
prefixExists bool // is it okay to use the prefix?
}
// Init runs some initialization code for the VarDir API.
func (obj *VarDirImpl) Init(init *VarDirInit) {
obj.init = init
obj.mutex = &sync.Mutex{}
obj.prefix = fmt.Sprintf("%s/", path.Join(obj.init.Prefix, "vardir"))
}
// VarDir returns a directory rooted at the internal prefix.
func (obj *VarDirImpl) VarDir(ctx context.Context, reldir string) (string, error) {
if strings.HasPrefix(reldir, "/") {
return "", fmt.Errorf("path must be relative")
}
if !strings.HasSuffix(reldir, "/") {
return "", fmt.Errorf("path must be a dir")
}
// NOTE: The above checks ensure we don't get either "" or "/" as input!
prefix, err := obj.getPrefix()
if err != nil {
return "", err
}
result := fmt.Sprintf("%s/", path.Join(prefix, reldir))
// TODO: Should we mkdir this?
obj.mutex.Lock()
defer obj.mutex.Unlock()
if err := os.MkdirAll(result, 0755); err != nil {
return "", err
}
return result, nil
}
// getPrefix gets the prefix dir to use, or errors if it can't make one. It
// makes it on first use, and returns quickly from any future calls to it.
func (obj *VarDirImpl) getPrefix() (string, error) {
// NOTE: Moving this mutex to just below the first early return, would
// be a benign race, but as it turns out, it's possible that a compiler
// would see this behaviour as "undefined" and things might not work as
// intended. It could perhaps be replaced with a sync/atomic primitive
// if we wanted better performance here.
obj.mutex.Lock()
defer obj.mutex.Unlock()
if obj.prefixExists { // former race read
return obj.prefix, nil
}
// MkdirAll instead of Mkdir because we have no idea if the parent
// local/ directory was already made yet or not. (If at all.) If path is
// already a directory, MkdirAll does nothing and returns nil. (Good!)
// TODO: I hope MkdirAll is thread-safe on path creation in case another
// future local API tries to make the base (parent) directory too!
if err := os.MkdirAll(obj.prefix, 0755); err != nil {
return "", err
}
obj.prefixExists = true // former race write
return obj.prefix, nil
}
// PoolInit are the init values that the Pool API needs to work correctly.
type PoolInit struct {
Prefix string
Debug bool
Logf func(format string, v ...interface{})
}
// PoolConfig configures how the Pool operates.
// XXX: These are not implemented yet.
type PoolConfig struct {
// Expiry specifies that we expire old values that have not been read
// for this many seconds. Zero disables this and they never expire.
Expiry int64 // TODO: or time.Time ?
// Random lets you allocate a random integer instead of sequential ones.
Random bool
// Max specifies the maximum integer to allocate.
Max int
}
// PoolImpl is the implementation for the Pool API's. The API's are the
// collection of public methods that exist on this struct.
type PoolImpl struct {
init *PoolInit
mutex *sync.Mutex
prefix string
prefixExists bool // is it okay to use the prefix?
}
// Init runs some initialization code for the Pool API.
func (obj *PoolImpl) Init(init *PoolInit) {
obj.init = init
obj.mutex = &sync.Mutex{}
obj.prefix = fmt.Sprintf("%s/", path.Join(obj.init.Prefix, "pool"))
}
// Pool returns a unique integer from a pool of numbers. Within a given
// namespace, it returns the same integer for a given name. It is a simple
// mechanism to allocate numbers to different inputs when we don't have a
// hashing alternative. It does not allocate zero.
func (obj *PoolImpl) Pool(ctx context.Context, namespace, uid string, config *PoolConfig) (int, error) {
if namespace == "" {
return 0, fmt.Errorf("namespace is empty")
}
if strings.Contains(namespace, "/") {
return 0, fmt.Errorf("namespace contains slash")
}
if uid == "" {
return 0, fmt.Errorf("uid is empty")
}
if strings.Contains(uid, "/") {
return 0, fmt.Errorf("uid contains slash")
}
prefix, err := obj.getPrefix()
if err != nil {
return 0, err
}
dir := fmt.Sprintf("%s/", path.Join(prefix, namespace))
file := fmt.Sprintf("%s.uid", path.Join(dir, uid)) // file
// TODO: Run clean up funcs here to get rid of any stale/expired values.
// TODO: This will happen based on the future config options we build...
obj.mutex.Lock()
defer obj.mutex.Unlock()
if err := os.MkdirAll(dir, 0755); err != nil {
return 0, err
}
fn := func(p string) (int, error) {
b, err := os.ReadFile(p)
if err != nil && !os.IsNotExist(err) {
return 0, err // real error
}
if err != nil {
return 0, nil // absent!
}
// File exists!
d, err := strconv.Atoi(strings.TrimSpace(string(b)))
if err != nil {
// Someone put corrupt data in a uid file.
return 0, err // real error
}
return d, nil // value already allocated!
}
d, err := fn(file)
if err != nil {
return 0, err // real error
}
if d != 0 {
return d, nil // Value already allocated! We're done early.
}
// Not found, so find the max. (0 without error means not found!)
files, err := os.ReadDir(dir) // ([]os.DirEntry, error)
if err != nil {
return 0, err // real error
}
m := 0
for _, f := range files {
if f.IsDir() {
continue // unexpected!
}
d, err := fn(path.Join(dir, f.Name()))
if err != nil {
return 0, err // real error
}
if d == 0 {
// Must be someone deleting files without our mutex!
return 0, fmt.Errorf("unexpected missing file")
}
m = max(m, d)
}
m++ // increment
data := []byte(fmt.Sprintf("%d\n", m)) // it's polite to end with \n
if err := os.WriteFile(file, data, 0600); err != nil {
return 0, err
}
return m, nil
}
// getPrefix gets the prefix dir to use, or errors if it can't make one. It
// makes it on first use, and returns quickly from any future calls to it.
func (obj *PoolImpl) getPrefix() (string, error) {
// NOTE: Moving this mutex to just below the first early return, would
// be a benign race, but as it turns out, it's possible that a compiler
// would see this behaviour as "undefined" and things might not work as
// intended. It could perhaps be replaced with a sync/atomic primitive
// if we wanted better performance here.
obj.mutex.Lock()
defer obj.mutex.Unlock()
if obj.prefixExists { // former race read
return obj.prefix, nil
}
// MkdirAll instead of Mkdir because we have no idea if the parent
// local/ directory was already made yet or not. (If at all.) If path is
// already a directory, MkdirAll does nothing and returns nil. (Good!)
// TODO: I hope MkdirAll is thread-safe on path creation in case another
// future local API tries to make the base (parent) directory too!
if err := os.MkdirAll(obj.prefix, 0755); err != nil {
return "", err
}
obj.prefixExists = true // former race write
return obj.prefix, nil
}

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2024+ James Shubin and the project contributors
// Copyright (C) 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-2024+ James Shubin and the project contributors
// Copyright (C) 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
@@ -52,6 +52,7 @@ var DefaultMetaParams = &MetaParams{
//Sema: []string{},
Rewatch: false,
Realize: false, // true would be more awesome, but unexpected for users
Dollar: false,
}
// MetaRes is the interface a resource must implement to support meta params.
@@ -132,6 +133,13 @@ type MetaParams struct {
// the resource is blocked because of a failed pre-requisite resource.
// XXX: Not implemented!
Realize bool `yaml:"realize"`
// Dollar allows you to name a resource to start with the dollar
// character. We don't allow this by default since it's probably not
// needed, and is more likely to be a typo where the user forgot to
// interpolate a variable name. In the rare case when it's needed, you
// can disable that check with this meta param.
Dollar bool `yaml:"dollar"`
}
// Cmp compares two AutoGroupMeta structs and determines if they're equivalent.
@@ -178,6 +186,9 @@ func (obj *MetaParams) Cmp(meta *MetaParams) error {
if obj.Realize != meta.Realize {
return fmt.Errorf("values for Realize are different")
}
if obj.Dollar != meta.Dollar {
return fmt.Errorf("values for Dollar are different")
}
return nil
}
@@ -218,6 +229,7 @@ func (obj *MetaParams) Copy() *MetaParams {
Sema: sema,
Rewatch: obj.Rewatch,
Realize: obj.Realize,
Dollar: obj.Dollar,
}
}

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2024+ James Shubin and the project contributors
// Copyright (C) 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-2024+ James Shubin and the project contributors
// Copyright (C) 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-2024+ James Shubin and the project contributors
// Copyright (C) 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,7 +33,12 @@ import (
"context"
"encoding/gob"
"fmt"
"path/filepath"
"reflect"
"runtime"
"strings"
docsUtil "github.com/purpleidea/mgmt/docs/util"
"github.com/purpleidea/mgmt/engine/local"
"github.com/purpleidea/mgmt/pgraph"
"github.com/purpleidea/mgmt/util/errwrap"
@@ -41,6 +46,12 @@ import (
"gopkg.in/yaml.v2"
)
const (
// ResourcesRelDir is the path where the resources are kept, relative to
// the main source code root.
ResourcesRelDir = "engine/resources/"
)
// TODO: should each resource be a sub-package?
var registeredResources = map[string]func() Res{}
@@ -56,6 +67,23 @@ func RegisterResource(kind string, fn func() Res) {
}
gob.Register(f)
registeredResources[kind] = fn
// Additional metadata for documentation generation!
_, filename, _, ok := runtime.Caller(1)
if !ok {
panic(fmt.Sprintf("could not locate resource filename for %s", kind))
}
sp := strings.Split(reflect.TypeOf(f).String(), ".")
if len(sp) != 2 {
panic(fmt.Sprintf("could not parse resource struct name for %s", kind))
}
if err := docsUtil.RegisterResource(kind, &docsUtil.Metadata{
Filename: filepath.Base(filename),
Typename: sp[1],
}); err != nil {
panic(fmt.Sprintf("could not register resource metadata for %s", kind))
}
}
// RegisteredResourcesNames returns the kind of the registered resources.
@@ -272,6 +300,12 @@ func Validate(res Res) error {
return errwrap.Wrapf(err, "the Res has an invalid meta param")
}
// TODO: pull dollar prefix from a constant
// This catches typos where the user meant to use ${var} interpolation.
if !res.MetaParams().Dollar && strings.HasPrefix(res.Name(), "$") {
return fmt.Errorf("the Res name starts with a $")
}
return res.Validate()
}

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2024+ James Shubin and the project contributors
// Copyright (C) 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
@@ -206,7 +206,6 @@ func (obj *AugeasRes) checkApplySet(ctx context.Context, apply bool, ag *augeas.
// CheckApply method for Augeas resource.
func (obj *AugeasRes) CheckApply(ctx context.Context, apply bool) (bool, error) {
obj.init.Logf("CheckApply: %s", obj.File)
// By default we do not set any option to augeas, we use the defaults.
opts := augeas.None
if obj.Lens != "" {

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2024+ James Shubin and the project contributors
// Copyright (C) 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
@@ -638,7 +638,7 @@ func (obj *AwsEc2Res) snsWatch(ctx context.Context) error {
// CheckApply method for AwsEc2 resource.
func (obj *AwsEc2Res) CheckApply(ctx context.Context, apply bool) (bool, error) {
obj.init.Logf("CheckApply(%t)", apply)
obj.init.Logf("CheckApply(%t)", apply) // XXX: replace with logf on change
// find the instance we need to check
instance, err := describeInstanceByName(obj.client, obj.prependName())

View File

@@ -0,0 +1,466 @@
// Mgmt
// Copyright (C) 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 <https://www.gnu.org/licenses/>.
//
// Additional permission under GNU GPL version 3 section 7
//
// If you modify this program, or any covered work, by linking or combining it
// with embedded mcl code and modules (and that the embedded mcl code and
// modules which link with this program, contain a copy of their source code in
// the authoritative form) containing parts covered by the terms of any other
// license, the licensors of this program grant you additional permission to
// convey the resulting work. Furthermore, the licensors of this program grant
// the original author, James Shubin, additional permission to update this
// additional permission if he deems it necessary to achieve the goals of this
// additional permission.
package resources
import (
"context"
"fmt"
"net"
"net/url"
"strconv"
"strings"
"github.com/purpleidea/mgmt/engine"
"github.com/purpleidea/mgmt/engine/traits"
bmclib "github.com/bmc-toolbox/bmclib/v2"
"github.com/bmc-toolbox/bmclib/v2/providers/rpc"
)
func init() {
engine.RegisterResource("bmc:power", func() engine.Res { return &BmcPowerRes{} })
}
const (
// DefaultBmcPowerPort is the default port we try to connect on.
DefaultBmcPowerPort = 443
// BmcDriverSecureSuffix is the magic char we append to a driver name to
// specify we want the SSL/TLS variant.
BmcDriverSecureSuffix = "s"
// BmcDriverRPC is the RPC driver.
BmcDriverRPC = "rpc"
// BmcDriverGofish is the gofish driver.
BmcDriverGofish = "gofish"
)
// BmcPowerRes is a resource that manages power state of a BMC. This is usually
// used for turning computers on and off. The name value can be a big URL string
// in the form: `driver://user:pass@hostname:port` for example you may see:
// gofishs://ADMIN:hunter2@127.0.0.1:8800 to use the "https" variant of the
// gofish driver.
//
// NOTE: New drivers should either not end in "s" or at least not be identical
// to the name of another driver an "s" is added or removed to the end.
type BmcPowerRes struct {
traits.Base // add the base methods without re-implementation
init *engine.Init
// Hostname to connect to. If not specified, we parse this from the
// Name.
Hostname string `lang:"hostname" yaml:"hostname"`
// Port to connect to. If not specified, we parse this from the Name.
Port int `lang:"port" yaml:"port"`
// Username to use to connect. If not specified, we parse this from the
// Name.
// TODO: If the Username field is not set, should we parse from the
// Name? It's not really part of the BMC unique identifier so maybe we
// shouldn't use that.
Username string `lang:"username" yaml:"username"`
// Password to use to connect. We do NOT parse this from the Name unless
// you set InsecurePassword to true.
// XXX: Use mgmt magic credentials in the future.
Password string `lang:"password" yaml:"password"`
// InsecurePassword can be set to true to allow a password in the Name.
InsecurePassword bool `lang:"insecure_password" yaml:"insecure_password"`
// Driver to use, such as: "gofish" or "rpc". This is a different
// concept than the "bmclib" driver vs provider distinction. Here we
// just statically pick what we're using without any magic. If not
// specified, we parse this from the Name scheme. If this ends with an
// extra "s" then we use https instead of http.
Driver string `lang:"driver" yaml:"driver"`
// State of machine power. Can be "on" or "off".
State string `lang:"state" yaml:"state"`
driver string
scheme string
}
// validDriver determines if we are using a valid drive. This does not include
// the magic "s" bits. This function need to be expanded as we support more
// drivers.
func (obj *BmcPowerRes) validDriver(driver string) error {
if driver == BmcDriverRPC {
return nil
}
if driver == BmcDriverGofish {
return nil
}
return fmt.Errorf("unknown driver: %s", driver)
}
// getHostname returns the hostname that we want to connect to. If the Hostname
// field is set, we use that, otherwise we parse from the Name.
func (obj *BmcPowerRes) getHostname() string {
if obj.Hostname != "" {
return obj.Hostname
}
u, err := url.Parse(obj.Name())
if err != nil || u == nil {
return ""
}
// SplitHostPort splits a network address of the form "host:port",
// "host%zone:port", "[host]:port" or "[host%zone]:port" into host or
// host%zone and port.
host, port, err := net.SplitHostPort(u.Host)
if err != nil {
return u.Host // must be a naked hostname or ip w/o port
}
_ = port
return host
}
// getPort returns the port that we want to connect to. If the Port field is
// set, we use that, otherwise we parse from the Name.
//
// NOTE: We return a string since all the bmclib things usually expect a string,
// but if that gets fixed we should return an int here instead.
func (obj *BmcPowerRes) getPort() string {
if obj.Port != 0 {
return strconv.Itoa(obj.Port)
}
u, err := url.Parse(obj.Name())
if err != nil || u == nil {
return ""
}
// SplitHostPort splits a network address of the form "host:port",
// "host%zone:port", "[host]:port" or "[host%zone]:port" into host or
// host%zone and port.
host, port, err := net.SplitHostPort(u.Host)
if err != nil {
return strconv.Itoa(DefaultBmcPowerPort) // default port
}
_ = host
return port
}
// getUsername returns the username that we want to connect with. If the
// Username field is set, we use that, otherwise we parse from the Name.
// TODO: If the Username field is not set, should we parse from the Name? It's
// not really part of the BMC unique identifier so maybe we shouldn't use that.
func (obj *BmcPowerRes) getUsername() string {
if obj.Username != "" {
return obj.Username
}
u, err := url.Parse(obj.Name())
if err != nil || u == nil || u.User == nil {
return ""
}
return u.User.Username()
}
// getPassword returns the password that we want to connect with.
// XXX: Use mgmt magic credentials in the future.
func (obj *BmcPowerRes) getPassword() string {
if obj.Password != "" || !obj.InsecurePassword {
return obj.Password
}
// NOTE: We don't look at any password string from the name unless the
// InsecurePassword field is true.
u, err := url.Parse(obj.Name())
if err != nil || u == nil || u.User == nil {
return ""
}
password, ok := u.User.Password()
if !ok {
return ""
}
return password
}
// getRawDriver returns the raw magic driver string. If the Driver field is set,
// we use that, otherwise we parse from the Name. This version may include the
// magic "s" at the end.
func (obj *BmcPowerRes) getRawDriver() string {
if obj.Driver != "" {
return obj.Driver
}
u, err := url.Parse(obj.Name())
if err != nil || u == nil {
return ""
}
return u.Scheme
}
// getDriverAndScheme figures out which driver and scheme we want to use.
func (obj *BmcPowerRes) getDriverAndScheme() (string, string, error) {
driver := obj.getRawDriver()
err := obj.validDriver(driver)
if err == nil {
return driver, "http", nil
}
driver = strings.TrimSuffix(driver, BmcDriverSecureSuffix)
if err := obj.validDriver(driver); err == nil {
return driver, "https", nil
}
return "", "", err // return the first error
}
// getDriver returns the actual driver that we want to connect with. If the
// Driver field is set, we use that, otherwise we parse from the Name. This
// version does NOT include the magic "s" at the end.
func (obj *BmcPowerRes) getDriver() string {
return obj.driver
}
// getScheme figures out which scheme we want to use.
func (obj *BmcPowerRes) getScheme() string {
return obj.scheme
}
// Default returns some sensible defaults for this resource.
func (obj *BmcPowerRes) Default() engine.Res {
return &BmcPowerRes{}
}
// Validate if the params passed in are valid data.
func (obj *BmcPowerRes) Validate() error {
// XXX: Force polling until we have real events...
if obj.MetaParams().Poll == 0 {
return fmt.Errorf("events are not yet supported, use polling")
}
if obj.getHostname() == "" {
return fmt.Errorf("need a Hostname")
}
//if obj.getUsername() == "" {
// return fmt.Errorf("need a Username")
//}
if obj.getRawDriver() == "" {
return fmt.Errorf("need a Driver")
}
if _, _, err := obj.getDriverAndScheme(); err != nil {
return err
}
return nil
}
// Init runs some startup code for this resource.
func (obj *BmcPowerRes) Init(init *engine.Init) error {
obj.init = init // save for later
driver, scheme, err := obj.getDriverAndScheme()
if err != nil {
// programming error (we checked in Validate)
return err
}
obj.driver = driver
obj.scheme = scheme
return nil
}
// Cleanup is run by the engine to clean up after the resource is done.
func (obj *BmcPowerRes) Cleanup() error {
return nil
}
// client builds the bmclib client. The API to build it is complicated.
func (obj *BmcPowerRes) client() *bmclib.Client {
// NOTE: The bmclib API is weird, you can't put the port in this string!
u := fmt.Sprintf("%s://%s", obj.getScheme(), obj.getHostname())
uPort := u
if p := obj.getPort(); p != "" {
uPort = u + ":" + p
}
opts := []bmclib.Option{}
if obj.getDriver() == BmcDriverRPC {
opts = append(opts, bmclib.WithRPCOpt(rpc.Provider{
// NOTE: The main API cannot take a port, but here we do!
ConsumerURL: uPort,
}))
}
if p := obj.getPort(); p != "" {
switch obj.getDriver() {
case BmcDriverRPC:
// TODO: ???
case BmcDriverGofish:
// XXX: Why doesn't this accept an int?
opts = append(opts, bmclib.WithRedfishPort(p))
//case BmcDriverOpenbmc:
// // XXX: Why doesn't this accept an int?
// opts = append(opts, openbmc.WithPort(p))
default:
// TODO: error or pass through?
obj.init.Logf("unhandled driver: %s", obj.getDriver())
}
}
client := bmclib.NewClient(u, obj.getUsername(), obj.Password, opts...)
if obj.getDriver() != "" && obj.getDriver() != BmcDriverRPC {
client = client.For(obj.getDriver()) // limit to this provider
}
return client
}
// Watch is the primary listener for this resource and it outputs events.
func (obj *BmcPowerRes) Watch(ctx context.Context) error {
obj.init.Running() // when started, notify engine that we're running
select {
case <-ctx.Done(): // closed by the engine to signal shutdown
}
//obj.init.Event() // notify engine of an event (this can block)
return nil
}
// CheckApply method for BmcPower resource. Does nothing, returns happy!
func (obj *BmcPowerRes) CheckApply(ctx context.Context, apply bool) (bool, error) {
client := obj.client()
if err := client.Open(ctx); err != nil {
return false, err
}
defer client.Close(ctx) // (err error)
if obj.init.Debug {
obj.init.Logf("connected ok")
}
state, err := client.GetPowerState(ctx)
if err != nil {
return false, err
}
state = strings.ToLower(state) // normalize
obj.init.Logf("get state: %s", state)
if !apply {
return false, nil
}
if obj.State == state {
return true, nil
}
// TODO: should this be "On" and "Off"? Does case matter?
ok, err := client.SetPowerState(ctx, obj.State)
if err != nil {
return false, err
}
if !ok {
// TODO: When is this ever false?
}
obj.init.Logf("set state: %s", obj.State)
return false, nil
}
// Cmp compares two resources and returns an error if they are not equivalent.
func (obj *BmcPowerRes) Cmp(r engine.Res) error {
// we can only compare BmcPowerRes to others of the same resource kind
res, ok := r.(*BmcPowerRes)
if !ok {
return fmt.Errorf("not a %s", obj.Kind())
}
if obj.Hostname != res.Hostname {
return fmt.Errorf("the Hostname differs")
}
if obj.Port != res.Port {
return fmt.Errorf("the Port differs")
}
if obj.Username != res.Username {
return fmt.Errorf("the Username differs")
}
if obj.Password != res.Password {
return fmt.Errorf("the Password differs")
}
if obj.InsecurePassword != res.InsecurePassword {
return fmt.Errorf("the InsecurePassword differs")
}
if obj.Driver != res.Driver {
return fmt.Errorf("the Driver differs")
}
if obj.State != res.State {
return fmt.Errorf("the State differs")
}
return nil
}
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
// primarily useful for setting the defaults.
func (obj *BmcPowerRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
type rawRes BmcPowerRes // indirection to avoid infinite recursion
def := obj.Default() // get the default
res, ok := def.(*BmcPowerRes) // put in the right format
if !ok {
return fmt.Errorf("could not convert to BmcPowerRes")
}
raw := rawRes(*res) // convert; the defaults go here
if err := unmarshal(&raw); err != nil {
return err
}
*obj = BmcPowerRes(raw) // restore from indirection with type conversion!
return nil
}

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2024+ James Shubin and the project contributors
// Copyright (C) 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-2024+ James Shubin and the project contributors
// Copyright (C) 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,6 +27,8 @@
// additional permission if he deems it necessary to achieve the goals of this
// additional permission.
//go:build !noconsul
package resources
import (

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2024+ James Shubin and the project contributors
// Copyright (C) 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,6 +27,8 @@
// additional permission if he deems it necessary to achieve the goals of this
// additional permission.
//go:build !root || !noconsul
package resources
import (

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2024+ James Shubin and the project contributors
// Copyright (C) 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
@@ -36,7 +36,6 @@ import (
"os/user"
"path"
"strings"
"time"
"github.com/purpleidea/mgmt/engine"
"github.com/purpleidea/mgmt/engine/traits"
@@ -76,10 +75,6 @@ const (
// in 'man systemd-timer', and whose format is a time span as defined in
// 'man systemd-time'.
OnUnitInactiveSec = "OnUnitInactiveSec"
// ctxTimeout is the delay, in seconds, before the calls to restart or stop
// the systemd unit will error due to timeout.
ctxTimeout = 30
)
func init() {
@@ -104,6 +99,11 @@ type CronRes struct {
// State must be 'exists' or 'absent'.
State string `lang:"state" yaml:"state"`
// Startup specifies what should happen on startup. Values can be:
// enabled, disabled, and undefined (empty string). We default to
// enabled.
Startup string `lang:"startup" yaml:"startup"`
// Session, if true, creates the timer as the current user, rather than
// root. The service it points to must also be a user unit. It defaults
// to false.
@@ -154,6 +154,7 @@ type CronRes struct {
func (obj *CronRes) Default() engine.Res {
return &CronRes{
State: "exists",
Startup: "enabled",
RemainAfterElapse: true,
}
}
@@ -188,6 +189,9 @@ func (obj *CronRes) Validate() error {
if obj.State != "absent" && obj.State != "exists" {
return fmt.Errorf("state must be 'absent' or 'exists'")
}
if obj.Startup != "enabled" && obj.Startup != "disabled" && obj.Startup != "" {
return fmt.Errorf("startup must be either `enabled` or `disabled` or undefined")
}
// validate trigger
if obj.State == "absent" && obj.Trigger == "" {
@@ -264,12 +268,12 @@ func (obj *CronRes) Watch(ctx context.Context) error {
args := []string{}
args = append(args, "type='signal'")
args = append(args, "interface='org.freedesktop.systemd1.Manager'")
args = append(args, "eavesdrop='true'")
//args = append(args, "eavesdrop='true'") // XXX: not allowed anymore?
args = append(args, fmt.Sprintf("arg2='%s.timer'", obj.Name()))
// match dbus messsages
if call := bus.BusObject().Call(engineUtil.DBusAddMatch, 0, strings.Join(args, ",")); call.Err != nil {
return err
return call.Err
}
defer bus.BusObject().Call(engineUtil.DBusRemoveMatch, 0, args) // ignore the error
@@ -390,14 +394,10 @@ func (obj *CronRes) unitCheckApply(ctx context.Context, apply bool) (bool, error
}
// systemctl daemon-reload
if err := conn.Reload(); err != nil {
if err := conn.ReloadContext(ctx); err != nil {
return false, errwrap.Wrapf(err, "error reloading daemon")
}
// context for stopping/restarting the unit
ctx, cancel := context.WithTimeout(ctx, ctxTimeout*time.Second)
defer cancel()
// godbus connection for stopping/restarting the unit
if obj.Session {
godbusConn, err = util.SessionBusPrivateUsable()
@@ -409,6 +409,18 @@ func (obj *CronRes) unitCheckApply(ctx context.Context, apply bool) (bool, error
}
defer godbusConn.Close()
// We probably always want to enable this...
svc := fmt.Sprintf("%s.timer", obj.Name()) // systemd name
files := []string{svc} // the svc represented in a list
if obj.Startup == "enabled" {
_, _, err = conn.EnableUnitFilesContext(ctx, files, false, true)
} else if obj.Startup == "disabled" {
_, err = conn.DisableUnitFilesContext(ctx, files, false)
}
if err != nil {
return false, errwrap.Wrapf(err, "unable to change startup status")
}
// stop or restart the unit
if obj.State == "absent" {
return false, engineUtil.StopUnit(ctx, godbusConn, fmt.Sprintf("%s.timer", obj.Name()))
@@ -426,6 +438,9 @@ func (obj *CronRes) Cmp(r engine.Res) error {
if obj.State != res.State {
return fmt.Errorf("state differs: %s vs %s", obj.State, res.State)
}
if obj.Startup != res.Startup {
return fmt.Errorf("the Startup differs")
}
if obj.Trigger != res.Trigger {
return fmt.Errorf("trigger differs: %s vs %s", obj.Trigger, res.Trigger)
}

View File

@@ -0,0 +1,528 @@
// Mgmt
// Copyright (C) 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 <https://www.gnu.org/licenses/>.
//
// Additional permission under GNU GPL version 3 section 7
//
// If you modify this program, or any covered work, by linking or combining it
// with embedded mcl code and modules (and that the embedded mcl code and
// modules which link with this program, contain a copy of their source code in
// the authoritative form) containing parts covered by the terms of any other
// license, the licensors of this program grant you additional permission to
// convey the resulting work. Furthermore, the licensors of this program grant
// the original author, James Shubin, additional permission to update this
// additional permission if he deems it necessary to achieve the goals of this
// additional permission.
package resources
import (
"archive/tar"
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"io/fs"
"os"
"path"
"strings"
"github.com/purpleidea/mgmt/engine"
"github.com/purpleidea/mgmt/engine/traits"
"github.com/purpleidea/mgmt/util/errwrap"
"github.com/purpleidea/mgmt/util/recwatch"
"github.com/spf13/afero"
)
func init() {
engine.RegisterResource("deploy:tar", func() engine.Res { return &DeployTar{} })
}
// DeployTar is a resource that archives a deploy filesystem using tar, thus
// combining them into a single file. The name of the resource is the path to
// the resultant archive file. The input comes from the current deploy. This
// uses hashes to determine if something was changed, so as a result, this may
// not be suitable if you can create a sha256 hash collision.
// TODO: support send/recv to send the output instead of writing to a file?
// TODO: This resource is very similar to the tar resource. Update that one if
// this changes, or consider porting this to use that as a composite resource.
// TODO: consider using a `deploy.get_archive()` function to make a .tar, and a
// file resource to store those contents on disk with whatever mode we want...
type DeployTar struct {
traits.Base // add the base methods without re-implementation
init *engine.Init
// Path, which defaults to the name if not specified, represents the
// destination path for the compressed file being created. It must be an
// absolute path, and as a result must start with a slash. Since it is a
// file, it must not end with a slash.
Path string `lang:"path" yaml:"path"`
// Format is the header format to use. If you change this, then the
// file will get rearchived. The strange thing is that it seems the
// header format is stored for each individual file. The available
// values are: const.res.tar.format.unknown, const.res.tar.format.ustar,
// const.res.tar.format.pax, and const.res.tar.format.gnu which have
// values of 0, 2, 4, and 8 respectively.
Format int `lang:"format" yaml:"format"`
// SendOnly specifies that we don't write the file to disk, and as a
// result, the output is only be accessible by the send/recv mechanism.
// TODO: Rename this?
// TODO: Not implemented
//SendOnly bool `lang:"sendonly" yaml:"sendonly"`
// varDirPathInput is the path we use to store the content hash.
varDirPathInput string
// varDirPathOutput is the path we use to store the output file hash.
varDirPathOutput string
}
// getPath returns the actual path to use for this resource. It computes this
// after analysis of the Path and Name.
func (obj *DeployTar) getPath() string {
p := obj.Path
if obj.Path == "" { // use the name as the path default if missing
p = obj.Name()
}
return p
}
// Default returns some sensible defaults for this resource.
func (obj *DeployTar) Default() engine.Res {
return &DeployTar{
Format: int(tar.FormatUnknown), // TODO: will this let it auto-choose?
}
}
// Validate if the params passed in are valid data.
func (obj *DeployTar) Validate() error {
if obj.getPath() == "" {
return fmt.Errorf("path is empty")
}
if !strings.HasPrefix(obj.getPath(), "/") {
return fmt.Errorf("path must be absolute")
}
if strings.HasSuffix(obj.getPath(), "/") {
return fmt.Errorf("path must not end with a slash")
}
return nil
}
// Init runs some startup code for this resource.
func (obj *DeployTar) Init(init *engine.Init) error {
obj.init = init // save for later
dir, err := obj.init.VarDir("")
if err != nil {
return errwrap.Wrapf(err, "could not get VarDir in Init()")
}
// return unique files
obj.varDirPathInput = path.Join(dir, "input.sha256")
obj.varDirPathOutput = path.Join(dir, "output.sha256")
return nil
}
// Cleanup is run by the engine to clean up after the resource is done.
func (obj *DeployTar) Cleanup() error {
return nil
}
// Watch is the primary listener for this resource and it outputs events.
func (obj *DeployTar) Watch(ctx context.Context) error {
recurse := false // single (output) file
recWatcher, err := recwatch.NewRecWatcher(obj.getPath(), recurse)
if err != nil {
return err
}
defer recWatcher.Close()
obj.init.Running() // when started, notify engine that we're running
var send = false // send event?
for {
select {
case event, ok := <-recWatcher.Events():
if !ok { // channel shutdown
// TODO: Should this be an error? Previously it
// was a `return nil`, and i'm not sure why...
//return nil
return fmt.Errorf("unexpected close")
}
if err := event.Error; err != nil {
return errwrap.Wrapf(err, "unknown %s watcher error", obj)
}
if obj.init.Debug { // don't access event.Body if event.Error isn't nil
obj.init.Logf("event(%s): %v", event.Body.Name, event.Body.Op)
}
send = true
case <-ctx.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 checks the resource state and applies the resource if the bool
// input is true. It returns error info and if the state check passed or not.
// This is where we actually do the archiving into a tar file work when needed.
func (obj *DeployTar) CheckApply(ctx context.Context, apply bool) (bool, error) {
uri := obj.init.World.URI() // request each time to ensure it's fresh!
filesystem, err := obj.init.World.Fs(uri) // open the remote file system
if err != nil {
return false, errwrap.Wrapf(err, "can't load code from file system `%s`", uri)
}
h1, err := obj.hashFile(obj.getPath()) // output
if err != nil {
return false, err
}
h2, err := obj.readHashFile(obj.varDirPathOutput, true)
if err != nil {
return false, err
}
i1 := ""
i1 = obj.formatPrefix() + "\n" // add the prefix so it is considered
// TODO: use standard filesystem API's when we can make them work!
//fsys := afero.NewIOFS(filesystem)
if err := afero.Walk(filesystem, "/", func(path string, info fs.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
if path == "/" { // special case for root
i1 += path + "|" + "\n"
return nil
}
// hash the dir itself too (eg: empty dirs!)
i1 += path + "/" + "|" + "\n"
return nil
}
h, err := obj.hashFileAferoFs(filesystem, path)
if err != nil {
return err
}
i1 += path + "|" + h + "\n"
return nil
}); err != nil {
return false, err
}
i2, err := obj.readHashFile(obj.varDirPathInput, false)
if err != nil {
return false, err
}
// We're cheating by computing this before we know if we errored!
inputMatches := i1 == i2
outputMatches := h1 == h2
if err == nil && inputMatches && outputMatches {
// If the two hashes match, we assume that the file is correct!
// The file has to also exist of course...
return true, nil
}
if !apply {
return false, nil
}
fail := true // assume we have a failure
defer func() {
if !fail {
return
}
// Don't leave a partial file lying around...
obj.init.Logf("removing partial tar file")
err := os.Remove(obj.getPath())
if err == nil || os.IsNotExist(err) {
return
}
obj.init.Logf("error removing corrupt tar file: %v", err)
}()
// FIXME: Do we instead want to write to a tmp file and do a move once
// we finish writing to be atomic here and avoid partial corrupt files?
// FIXME: Add a param called Atomic to specify that behaviour. It's
// instant so that might be preferred as it might generate fewer events,
// but there's a chance it's copying from obj.init.VarDir() to a
// different filesystem.
outputFile, err := os.Create(obj.getPath()) // io.Writer
if err != nil {
return false, err
}
//defer outputFile.Sync() // not needed?
defer outputFile.Close()
hash := sha256.New()
// Write to both to avoid needing to wait for fsync to calculate hash!
multiWriter := io.MultiWriter(outputFile, hash)
tarWriter := tar.NewWriter(multiWriter) // (*tar.Writer, error)
defer tarWriter.Close() // Might as well always close if we error early!
// TODO: formerly tarWriter.AddFS(fsys) // buggy!
if err := obj.addAferoFs(tarWriter, filesystem); err != nil {
return false, errwrap.Wrapf(err, "error writing fs")
}
// NOTE: Must run this before hashing so that it includes the footer!
if err := tarWriter.Close(); err != nil {
return false, err
}
sha256sum := hex.EncodeToString(hash.Sum(nil))
// TODO: add better logging counts if we can see tarWriter.AddFs too!
//obj.init.Logf("wrote %d files into archive", ?)
obj.init.Logf("wrote tar archive")
// After tar is successfully written, store the hashed input result.
if !inputMatches {
if err := os.WriteFile(obj.varDirPathInput, []byte(i1), 0600); err != nil {
return false, err
}
}
// Also store the new hashed output result.
if !outputMatches || h2 == "" { // If missing, we always write it out!
if err := os.WriteFile(obj.varDirPathOutput, []byte(sha256sum+"\n"), 0600); err != nil {
return false, err
}
}
fail = false // defer can exit safely!
return false, nil
}
// formatPrefix is a simple helper to add a format identifier for our hash.
func (obj *DeployTar) formatPrefix() string {
return fmt.Sprintf("format:%d|%s", obj.Format, tar.Format(obj.Format))
}
// hashContent is a simple helper to run our hashing function.
func (obj *DeployTar) hashContent(handle io.Reader) (string, error) {
hash := sha256.New()
if _, err := io.Copy(hash, handle); err != nil {
return "", err
}
return hex.EncodeToString(hash.Sum(nil)), nil
}
// hashFile is a helper that returns the hash of the specified file. If the file
// doesn't exist, it returns the empty string. Otherwise it errors.
func (obj *DeployTar) hashFile(file string) (string, error) {
f, err := os.Open(file) // io.Reader
if err != nil && !os.IsNotExist(err) {
// This is likely a permissions error.
return "", err
} else if err != nil {
return "", nil // File doesn't exist!
}
defer f.Close()
// File exists, lets hash it!
return obj.hashContent(f)
}
// hashFileAferoFs is a helper that returns the hash of the specified file with
// an Afero fs. If the file doesn't exist, it returns the empty string.
// Otherwise it errors.
func (obj *DeployTar) hashFileAferoFs(fsys afero.Fs, file string) (string, error) {
f, err := fsys.Open(file) // io.Reader
if err != nil && !os.IsNotExist(err) {
// This is likely a permissions error.
return "", err
} else if err != nil {
return "", nil // File doesn't exist!
}
defer f.Close()
// File exists, lets hash it!
return obj.hashContent(f)
}
// readHashFile reads the hashed value that we stored for the output file.
func (obj *DeployTar) readHashFile(file string, trim bool) (string, error) {
// TODO: Use io.ReadFull to avoid reading in a file that's too big!
if expected, err := os.ReadFile(file); err != nil && !os.IsNotExist(err) { // ([]byte, error)
// This is likely a permissions error?
return "", err
} else if err == nil {
if trim {
return strings.TrimSpace(string(expected)), nil
}
return string(expected), nil
}
// File doesn't exist!
return "", nil
}
// addFS is an edited copy of archive/tar's *Writer.AddFs function. This version
// correctly adds the directories too! https://github.com/golang/go/issues/69459
func (obj *DeployTar) addFS(tw *tar.Writer, fsys fs.FS) error {
return fs.WalkDir(fsys, ".", func(name string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if name == "." {
return nil
}
info, err := d.Info()
if err != nil {
return err
}
// TODO: Handle symlinks when fs.ReadLinkFS is available. (#49580)
if !info.Mode().IsRegular() && !info.Mode().IsDir() {
return fmt.Errorf("deploy:tar: cannot add non-regular file")
}
h, err := tar.FileInfoHeader(info, "")
if err != nil {
return err
}
h.Name = name
h.Format = tar.Format(obj.Format)
if d.IsDir() {
h.Name += "/" // dir
}
if err := tw.WriteHeader(h); err != nil {
return err
}
if d.IsDir() {
return nil // no contents to copy in
}
f, err := fsys.Open(name)
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(tw, f)
return err
})
}
// addAferoFs is an edited copy of archive/tar's *Writer.AddFs function but for
// the deprecated Afero.Fs API. This version correctly adds the directories too!
// https://github.com/golang/go/issues/69459
func (obj *DeployTar) addAferoFs(tw *tar.Writer, fsys afero.Fs) error {
return afero.Walk(fsys, "/", func(name string, info fs.FileInfo, err error) error {
if err != nil {
return err
}
if name == "/" {
return nil
}
// TODO: Handle symlinks when fs.ReadLinkFS is available. (#49580)
if !info.Mode().IsRegular() && !info.Mode().IsDir() {
return fmt.Errorf("deploy:tar: cannot add non-regular file")
}
h, err := tar.FileInfoHeader(info, "")
if err != nil {
return err
}
h.Name = name
h.Format = tar.Format(obj.Format)
if info.IsDir() {
h.Name += "/" // dir
}
if err := tw.WriteHeader(h); err != nil {
return err
}
if info.IsDir() {
return nil // no contents to copy in
}
f, err := fsys.Open(name)
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(tw, f)
return err
})
}
// Cmp compares two resources and returns an error if they are not equivalent.
func (obj *DeployTar) Cmp(r engine.Res) error {
// we can only compare DeployTar to others of the same resource kind
res, ok := r.(*DeployTar)
if !ok {
return fmt.Errorf("not a %s", obj.Kind())
}
if obj.Path != res.Path {
return fmt.Errorf("the Path differs")
}
if obj.Format != res.Format {
return fmt.Errorf("the Format differs")
}
return nil
}
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
// primarily useful for setting the defaults.
func (obj *DeployTar) UnmarshalYAML(unmarshal func(interface{}) error) error {
type rawRes DeployTar // indirection to avoid infinite recursion
def := obj.Default() // get the default
res, ok := def.(*DeployTar) // put in the right format
if !ok {
return fmt.Errorf("could not convert to DeployTar")
}
raw := rawRes(*res) // convert; the defaults go here
if err := unmarshal(&raw); err != nil {
return err
}
*obj = DeployTar(raw) // restore from indirection with type conversion!
return nil
}

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2024+ James Shubin and the project contributors
// Copyright (C) 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
@@ -894,6 +894,10 @@ func (obj *DHCPServerRes) handler4() func(net.PacketConn, net.Addr, *dhcpv4.DHCP
tmp.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeOffer))
case dhcpv4.MessageTypeRequest:
tmp.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeAck))
case dhcpv4.MessageTypeDecline:
// If mask is not set, some DHCP clients will DECLINE.
obj.init.Logf("handler4: Unhandled decline message: %+v", req)
return
default:
obj.init.Logf("handler4: Unhandled message type: %v", mt)
return
@@ -979,6 +983,7 @@ func (obj *DHCPServerRes) handler4() func(net.PacketConn, net.Addr, *dhcpv4.DHCP
if resp != nil {
if obj.init.Debug {
// NOTE: This is very useful for debugging!
obj.init.Logf("sending a DHCPv4 packet: %s", resp.Summary())
}
var peer net.Addr
@@ -1251,7 +1256,7 @@ func (obj *DHCPHostRes) handler4(data *HostData) (func(*dhcpv4.DHCPv4, *dhcpv4.D
// XXX: https://tools.ietf.org/html/rfc2132#section-3.3
// If both the subnet mask and the router option are specified
// in a DHCP reply, the subnet mask option MUST be first.
// XXX: Should we do this? Does it matter? Does the lib do it?
// If mask is not set, some DHCP clients will DECLINE.
resp.Options.Update(dhcpv4.OptSubnetMask(obj.ipv4Mask)) // net.IPMask
// nbp section
@@ -1714,7 +1719,7 @@ func (obj *DHCPRangeRes) Init(init *engine.Init) error {
obj.init.Logf("from: %s", obj.from)
obj.init.Logf(" to: %s", obj.to)
obj.init.Logf("mask: %s", obj.mask) // TODO: print as cidr or dotted quad
obj.init.Logf("mask: %s", netmaskAsQuadString(obj.mask))
return nil
}
@@ -1932,8 +1937,8 @@ func (obj *DHCPRangeRes) handler4(data *HostData) (func(*dhcpv4.DHCPv4, *dhcpv4.
// XXX: https://tools.ietf.org/html/rfc2132#section-3.3
// If both the subnet mask and the router option are specified
// in a DHCP reply, the subnet mask option MUST be first.
// XXX: Should we do this? Does it matter? Does the lib do it?
//resp.Options.Update(dhcpv4.OptSubnetMask(obj.mask)) // net.IPMask
// If mask is not set, some DHCP clients will DECLINE.
resp.Options.Update(dhcpv4.OptSubnetMask(obj.mask)) // net.IPMask
// nbp section
if obj.opt66 != nil && req.IsOptionRequested(dhcpv4.OptionTFTPServerName) {
@@ -2049,3 +2054,9 @@ func checkValidNetmask(netmask net.IPMask) bool {
y := x + 1
return (y & x) == 0
}
// netmaskAsQuadString returns a dotted-quad string giving you something like:
// 255.255.255.0 instead of ffffff00 which is what's seen when you print it now.
func netmaskAsQuadString(netmask net.IPMask) string {
return fmt.Sprintf("%d.%d.%d.%d", netmask[0], netmask[1], netmask[2], netmask[3])
}

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2024+ James Shubin and the project contributors
// Copyright (C) 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-2024+ James Shubin and the project contributors
// Copyright (C) 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
@@ -47,6 +47,7 @@ import (
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/client"
"github.com/docker/go-connections/nat"
)
@@ -234,7 +235,7 @@ func (obj *DockerContainerRes) CheckApply(ctx context.Context, apply bool) (bool
defer cancel()
// List any container whose name matches this resource.
opts := types.ContainerListOptions{
opts := container.ListOptions{
All: true,
Filters: filters.NewArgs(filters.KeyValuePair{Key: "name", Value: obj.Name()}),
}
@@ -279,14 +280,14 @@ func (obj *DockerContainerRes) CheckApply(ctx context.Context, apply bool) (bool
if err := obj.containerStop(ctx, id, nil); err != nil {
return false, err
}
return false, obj.containerRemove(ctx, id, types.ContainerRemoveOptions{})
return false, obj.containerRemove(ctx, id, container.RemoveOptions{})
}
if destroy {
if err := obj.containerStop(ctx, id, nil); err != nil {
return false, err
}
if err := obj.containerRemove(ctx, id, types.ContainerRemoveOptions{}); err != nil {
if err := obj.containerRemove(ctx, id, container.RemoveOptions{}); err != nil {
return false, err
}
containerList = []types.Container{} // zero the list
@@ -294,7 +295,7 @@ func (obj *DockerContainerRes) CheckApply(ctx context.Context, apply bool) (bool
if len(containerList) == 0 { // no container was found
// Download the specified image if it doesn't exist locally.
p, err := obj.client.ImagePull(ctx, obj.Image, types.ImagePullOptions{})
p, err := obj.client.ImagePull(ctx, obj.Image, image.PullOptions{})
if err != nil {
return false, errwrap.Wrapf(err, "error pulling image")
}
@@ -334,11 +335,11 @@ func (obj *DockerContainerRes) CheckApply(ctx context.Context, apply bool) (bool
id = c.ID
}
return false, obj.containerStart(ctx, id, types.ContainerStartOptions{})
return false, obj.containerStart(ctx, id, container.StartOptions{})
}
// containerStart starts the specified container, and waits for it to start.
func (obj *DockerContainerRes) containerStart(ctx context.Context, id string, opts types.ContainerStartOptions) error {
func (obj *DockerContainerRes) containerStart(ctx context.Context, id string, opts container.StartOptions) error {
// Get an events channel for the container we're about to start.
eventOpts := types.EventsOptions{
Filters: filters.NewArgs(filters.KeyValuePair{Key: "container", Value: id}),
@@ -377,7 +378,7 @@ func (obj *DockerContainerRes) containerStop(ctx context.Context, id string, tim
// containerRemove removes the specified container and waits for it to be
// removed.
func (obj *DockerContainerRes) containerRemove(ctx context.Context, id string, opts types.ContainerRemoveOptions) error {
func (obj *DockerContainerRes) containerRemove(ctx context.Context, id string, opts container.RemoveOptions) error {
ch, errCh := obj.client.ContainerWait(ctx, id, container.WaitConditionRemoved)
obj.client.ContainerRemove(ctx, id, opts)
select {

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2024+ James Shubin and the project contributors
// Copyright (C) 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
@@ -40,9 +40,9 @@ import (
"testing"
"time"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/image"
)
var res *DockerContainerRes
@@ -75,14 +75,14 @@ func BrokenTestContainerStart(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
if err := res.containerStart(ctx, id, types.ContainerStartOptions{}); err != nil {
if err := res.containerStart(ctx, id, container.StartOptions{}); err != nil {
t.Errorf("containerStart() error: %s", err)
return
}
l, err := res.client.ContainerList(
ctx,
types.ContainerListOptions{
container.ListOptions{
Filters: filters.NewArgs(
filters.KeyValuePair{Key: "id", Value: id},
filters.KeyValuePair{Key: "status", Value: "running"},
@@ -110,7 +110,7 @@ func BrokenTestContainerStop(t *testing.T) {
l, err := res.client.ContainerList(
ctx,
types.ContainerListOptions{
container.ListOptions{
Filters: filters.NewArgs(
filters.KeyValuePair{Key: "id", Value: id},
),
@@ -130,14 +130,14 @@ func BrokenTestContainerRemove(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
if err := res.containerRemove(ctx, id, types.ContainerRemoveOptions{}); err != nil {
if err := res.containerRemove(ctx, id, container.RemoveOptions{}); err != nil {
t.Errorf("containerRemove() error: %s", err)
return
}
l, err := res.client.ContainerList(
ctx,
types.ContainerListOptions{
container.ListOptions{
All: true,
Filters: filters.NewArgs(
filters.KeyValuePair{Key: "id", Value: id},
@@ -163,7 +163,7 @@ func setup() error {
res = &DockerContainerRes{}
res.Init(res.init)
p, err := res.client.ImagePull(ctx, "alpine", types.ImagePullOptions{})
p, err := res.client.ImagePull(ctx, "alpine", image.PullOptions{})
if err != nil {
return fmt.Errorf("error pulling image: %s", err)
}
@@ -195,7 +195,7 @@ func cleanup() error {
l, err := res.client.ContainerList(
ctx,
types.ContainerListOptions{
container.ListOptions{
All: true,
Filters: filters.NewArgs(filters.KeyValuePair{Key: "id", Value: id}),
},
@@ -209,7 +209,7 @@ func cleanup() error {
if err := res.client.ContainerStop(ctx, id, stopOpts); err != nil {
return fmt.Errorf("error stopping container: %s", err)
}
if err := res.client.ContainerRemove(ctx, id, types.ContainerRemoveOptions{}); err != nil {
if err := res.client.ContainerRemove(ctx, id, container.RemoveOptions{}); err != nil {
return fmt.Errorf("error removing container: %s", err)
}
}

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2024+ James Shubin and the project contributors
// Copyright (C) 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
@@ -44,6 +44,7 @@ import (
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/client"
errwrap "github.com/pkg/errors"
)
@@ -188,7 +189,7 @@ func (obj *DockerImageRes) CheckApply(ctx context.Context, apply bool) (checkOK
ctx, cancel := context.WithTimeout(ctx, dockerImageCheckApplyCtxTimeout*time.Second)
defer cancel()
s, err := obj.client.ImageList(ctx, types.ImageListOptions{
s, err := obj.client.ImageList(ctx, image.ListOptions{
Filters: filters.NewArgs(filters.Arg("reference", obj.image)),
})
if err != nil {
@@ -211,14 +212,14 @@ func (obj *DockerImageRes) CheckApply(ctx context.Context, apply bool) (checkOK
if obj.State == "absent" {
// TODO: force? prune children?
if _, err := obj.client.ImageRemove(ctx, obj.image, types.ImageRemoveOptions{}); err != nil {
if _, err := obj.client.ImageRemove(ctx, obj.image, image.RemoveOptions{}); 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{})
p, err := obj.client.ImagePull(ctx, obj.image, image.PullOptions{})
if err != nil {
return false, errwrap.Wrapf(err, "error pulling image")
}

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2024+ James Shubin and the project contributors
// Copyright (C) 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
@@ -83,7 +83,10 @@ type ExecRes struct {
// 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.)
// not the process being run here.) Keep in mind that if you're running
// this command as a user that does not have perms to the current
// directory, you may wish to set this to `/` to avoid hitting an error
// such as: `could not change directory to "/root": Permission denied`.
Cwd string `lang:"cwd" yaml:"cwd"`
// Shell is the (optional) shell to use to run the cmd. If you specify

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2024+ James Shubin and the project contributors
// Copyright (C) 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-2024+ James Shubin and the project contributors
// Copyright (C) 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
@@ -40,6 +40,7 @@ import (
"os"
"path"
"path/filepath"
"sort"
"strconv"
"strings"
"sync"
@@ -238,18 +239,19 @@ func (obj *FileRes) isDir() bool {
// the case where the mode is not specified. The caller should check obj.Mode is
// not empty.
func (obj *FileRes) mode() (os.FileMode, error) {
// First check if this is an octal number.
if n, err := strconv.ParseInt(obj.Mode, 8, 32); err == nil {
return os.FileMode(n), nil
}
// Try parsing symbolically by first getting the files current mode.
stat, err := os.Stat(obj.getPath())
if err != nil {
return os.FileMode(0), errwrap.Wrapf(err, "failed to get the current file mode")
from := os.FileMode(0) // default
if stat, err := os.Stat(obj.getPath()); err == nil {
from = stat.Mode()
}
modes := strings.Split(obj.Mode, ",")
m, err := engineUtil.ParseSymbolicModes(modes, stat.Mode(), FileModeAllowAssign)
m, err := engineUtil.ParseSymbolicModes(modes, from, FileModeAllowAssign)
if err != nil {
return os.FileMode(0), errwrap.Wrapf(err, "mode should be an octal number or symbolic mode (%s)", obj.Mode)
}
@@ -352,13 +354,6 @@ func (obj *FileRes) Validate() error {
return fmt.Errorf("can't set Owner or Group on this platform")
}
}
if _, err := engineUtil.GetUID(obj.Owner); obj.Owner != "" && err != nil {
return err
}
if _, err := engineUtil.GetGID(obj.Group); obj.Group != "" && err != nil {
return err
}
// TODO: should we silently ignore this error or include it?
//if obj.State == FileStateAbsent && obj.Mode != "" {
@@ -555,11 +550,16 @@ func (obj *FileRes) fileCheckApply(ctx context.Context, apply bool, src io.ReadS
obj.init.Logf("fileCheckApply: %v -> %s", src, dst)
}
length := int64(-1)
srcFile, isFile := src.(*os.File)
_, isBytes := src.(*bytes.Reader) // supports seeking!
srcReader, isBytes := src.(*bytes.Reader) // supports seeking!
if !isFile && !isBytes {
return "", false, fmt.Errorf("can't open src as either file or buffer")
}
if isBytes {
length = int64(srcReader.Len())
}
var srcStat os.FileInfo
if isFile {
@@ -572,6 +572,8 @@ func (obj *FileRes) fileCheckApply(ctx context.Context, apply bool, src io.ReadS
if !srcStat.Mode().IsRegular() { // can't copy non-regular files or dirs
return "", false, fmt.Errorf("non-regular src file: %s (%q)", srcStat.Name(), srcStat.Mode())
}
length = srcStat.Size()
}
dstFile, err := os.Open(dst)
@@ -610,7 +612,7 @@ func (obj *FileRes) fileCheckApply(ctx context.Context, apply bool, src io.ReadS
}
// FIXME: respect obj.Recurse here...
// there is a dir here, where we want a file...
obj.init.Logf("fileCheckApply: removing (force): %s", cleanDst)
obj.init.Logf("removing (force): %s", cleanDst)
if err := os.RemoveAll(cleanDst); err != nil { // dangerous ;)
return "", false, err
}
@@ -656,7 +658,7 @@ func (obj *FileRes) fileCheckApply(ctx context.Context, apply bool, src io.ReadS
return sha256sum, false, nil
}
if obj.init.Debug {
obj.init.Logf("fileCheckApply: apply: %v -> %s", src, dst)
obj.init.Logf("apply: %v -> %s", src, dst)
}
dstClose() // unlock file usage so we can write to it
@@ -676,13 +678,16 @@ func (obj *FileRes) fileCheckApply(ctx context.Context, apply bool, src io.ReadS
// syscall.Splice(rfd int, roff *int64, wfd int, woff *int64, len int, flags int) (n int64, err error)
// TODO: should we offer a way to cancel the copy on ^C ?
if obj.init.Debug {
obj.init.Logf("fileCheckApply: copy: %v -> %s", src, dst)
if isFile {
obj.init.Logf("copy %d bytes from: %v", length, src)
} else if isBytes {
obj.init.Logf("copy %d bytes", length)
}
if n, err := io.Copy(dstFile, src); err != nil {
return sha256sum, false, err
} else if obj.init.Debug {
obj.init.Logf("fileCheckApply: copied: %v", n)
obj.init.Logf("copied: %v", n)
}
return sha256sum, false, dstFile.Sync()
}
@@ -709,7 +714,7 @@ func (obj *FileRes) dirCheckApply(ctx context.Context, apply bool) (bool, error)
// the path exists and is not a directory
// delete the file if force is given
if err == nil && !fileInfo.IsDir() {
obj.init.Logf("dirCheckApply: removing (force): %s", obj.getPath())
obj.init.Logf("removing (force): %s", obj.getPath())
if err := os.Remove(obj.getPath()); err != nil {
return false, err
}
@@ -725,6 +730,7 @@ func (obj *FileRes) dirCheckApply(ctx context.Context, apply bool) (bool, error)
if obj.Recurse {
// TODO: add recurse limit here
obj.init.Logf("mkdir -p -m %s", mode)
return false, os.MkdirAll(obj.getPath(), mode)
}
@@ -738,7 +744,7 @@ func (obj *FileRes) dirCheckApply(ctx context.Context, apply bool) (bool, error)
// with the exception that a sync *can* convert a file to a dir, or vice-versa.
func (obj *FileRes) syncCheckApply(ctx context.Context, apply bool, src, dst string, excludes []string) (bool, error) {
if obj.init.Debug {
obj.init.Logf("syncCheckApply: %s -> %s", src, dst)
obj.init.Logf("sync: %s -> %s", src, dst)
}
// an src of "" is now supported, if dst is a dir
if dst == "" {
@@ -760,12 +766,12 @@ func (obj *FileRes) syncCheckApply(ctx context.Context, apply bool, src, dst str
if !srcIsDir && !dstIsDir && src != "" {
if obj.init.Debug {
obj.init.Logf("syncCheckApply: %s -> %s", src, dst)
obj.init.Logf("sync: %s -> %s", src, dst)
}
fin, err := os.Open(src)
if err != nil {
if obj.init.Debug && os.IsNotExist(err) { // if we get passed an empty src
obj.init.Logf("syncCheckApply: missing src: %s", src)
obj.init.Logf("missing src: %s", src)
}
return false, err
}
@@ -787,7 +793,9 @@ func (obj *FileRes) syncCheckApply(ctx context.Context, apply bool, src, dst str
return false, err
}
smartSrc = mapPaths(srcFiles)
obj.init.Logf("syncCheckApply: srcFiles: %v", srcFiles)
if obj.init.Debug {
obj.init.Logf("srcFiles: %v", printFiles(smartSrc))
}
}
dstFiles, err := ReadDir(dst)
@@ -795,7 +803,9 @@ func (obj *FileRes) syncCheckApply(ctx context.Context, apply bool, src, dst str
return false, err
}
smartDst := mapPaths(dstFiles)
obj.init.Logf("syncCheckApply: dstFiles: %v", dstFiles)
if obj.init.Debug {
obj.init.Logf("dstFiles: %v", printFiles(smartDst))
}
for relPath, fileInfo := range smartSrc {
absSrc := fileInfo.AbsPath // absolute path
@@ -819,7 +829,7 @@ func (obj *FileRes) syncCheckApply(ctx context.Context, apply bool, src, dst str
if absCleanDst == "" || absCleanDst == "/" {
return false, fmt.Errorf("don't want to remove root") // safety
}
obj.init.Logf("syncCheckApply: removing (force): %s", absCleanDst)
obj.init.Logf("removing (force): %s", absCleanDst)
if err := os.Remove(absCleanDst); err != nil {
return false, err
}
@@ -827,7 +837,7 @@ func (obj *FileRes) syncCheckApply(ctx context.Context, apply bool, src, dst str
}
if obj.init.Debug {
obj.init.Logf("syncCheckApply: mkdir -m %s '%s'", fileInfo.Mode(), absDst)
obj.init.Logf("mkdir -m %s '%s'", fileInfo.Mode(), absDst)
}
if err := os.Mkdir(absDst, fileInfo.Mode()); err != nil {
return false, err
@@ -838,11 +848,11 @@ func (obj *FileRes) syncCheckApply(ctx context.Context, apply bool, src, dst str
}
if obj.init.Debug {
obj.init.Logf("syncCheckApply: recurse: %s -> %s", absSrc, absDst)
obj.init.Logf("recurse: %s -> %s", absSrc, absDst)
}
if obj.Recurse {
if c, err := obj.syncCheckApply(ctx, apply, absSrc, absDst, excludes); err != nil { // recurse
return false, errwrap.Wrapf(err, "syncCheckApply: recurse failed")
return false, errwrap.Wrapf(err, "recurse failed")
} else if !c { // don't let subsequent passes make this true
checkOK = false
}
@@ -887,7 +897,7 @@ func (obj *FileRes) syncCheckApply(ctx context.Context, apply bool, src, dst str
if isExcluded(absDst) { // skip removing excluded files
continue
}
obj.init.Logf("syncCheckApply: removing: %s", absCleanDst)
obj.init.Logf("removing: %s", absCleanDst)
if apply {
if err := os.RemoveAll(absCleanDst); err != nil { // dangerous ;)
return false, err
@@ -897,16 +907,16 @@ func (obj *FileRes) syncCheckApply(ctx context.Context, apply bool, src, dst str
continue
}
_ = absSrc
//obj.init.Logf("syncCheckApply: recurse rm: %s -> %s", absSrc, absDst)
//obj.init.Logf("recurse rm: %s -> %s", absSrc, absDst)
//if c, err := obj.syncCheckApply(ctx, apply, absSrc, absDst, excludes); err != nil {
// return false, errwrap.Wrapf(err, "syncCheckApply: recurse rm failed")
// return false, errwrap.Wrapf(err, "recurse rm failed")
//} else if !c { // don't let subsequent passes make this true
// checkOK = false
//}
//if isExcluded(absDst) { // skip removing excluded files
// continue
//}
//obj.init.Logf("syncCheckApply: removing: %s", absCleanDst)
//obj.init.Logf("removing: %s", absCleanDst)
//if apply { // safety
// if err := os.Remove(absCleanDst); err != nil {
// return false, err
@@ -953,7 +963,7 @@ func (obj *FileRes) stateCheckApply(ctx context.Context, apply bool) (bool, erro
if p == "/" {
return false, fmt.Errorf("don't want to remove root") // safety
}
obj.init.Logf("stateCheckApply: removing: %s", p)
obj.init.Logf("removing: %s", p)
// TODO: add recurse limit here
if obj.Recurse {
return false, os.RemoveAll(p) // dangerous ;)
@@ -1058,13 +1068,13 @@ func (obj *FileRes) sourceCheckApply(ctx context.Context, apply bool) (bool, err
}
}
if obj.init.Debug {
obj.init.Logf("syncCheckApply: excludes: %+v", excludes)
obj.init.Logf("excludes: %+v", excludes)
}
// XXX: should this work with obj.Purge && obj.Source != "" or not?
checkOK, err := obj.syncCheckApply(ctx, apply, obj.Source, obj.getPath(), excludes)
if err != nil {
obj.init.Logf("syncCheckApply: error: %v", err)
obj.init.Logf("error: %v", err)
return false, err
}
@@ -1192,6 +1202,7 @@ func (obj *FileRes) chownCheckApply(ctx context.Context, apply bool) (bool, erro
return false, nil
}
obj.init.Logf("chown %s:%s", obj.Owner, obj.Group)
return false, os.Chown(obj.getPath(), expectedUID, expectedGID)
}
@@ -1226,6 +1237,7 @@ func (obj *FileRes) chmodCheckApply(ctx context.Context, apply bool) (bool, erro
return false, nil
}
obj.init.Logf("chmod %s", obj.Mode)
return false, os.Chmod(obj.getPath(), mode)
}
@@ -1724,3 +1736,20 @@ func mapPaths(fileInfos []FileInfo) map[string]FileInfo {
}
return paths
}
// printFiles is a pretty print function to make log messages less ugly.
func printFiles(fileInfos map[string]FileInfo) string {
s := ""
keys := []string{}
for k := range fileInfos {
keys = append(keys, k)
}
sort.Strings(keys)
for i, k := range keys {
s += fileInfos[k].RelPath
if i < len(keys)-1 {
s += ", "
}
}
return s
}

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2024+ James Shubin and the project contributors
// Copyright (C) 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-2024+ James Shubin and the project contributors
// Copyright (C) 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
@@ -318,7 +318,9 @@ func (obj *FirewalldRes) CheckApply(ctx context.Context, apply bool) (bool, erro
if obj.zone == "" {
return false, fmt.Errorf("unexpected empty zone")
}
obj.init.Logf("zone: %s\n", obj.zone)
if obj.init.Debug {
obj.init.Logf("zone: %s", obj.zone)
}
}
checkOK := true

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2024+ James Shubin and the project contributors
// Copyright (C) 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
@@ -135,8 +135,6 @@ func (obj *GroupRes) Watch(ctx context.Context) error {
// CheckApply method for Group resource.
func (obj *GroupRes) CheckApply(ctx context.Context, apply bool) (bool, error) {
obj.init.Logf("CheckApply(%t)", apply)
// check if the group exists
exists := true
group, err := user.LookupGroup(obj.Name())

518
engine/resources/gzip.go Normal file
View File

@@ -0,0 +1,518 @@
// Mgmt
// Copyright (C) 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 <https://www.gnu.org/licenses/>.
//
// Additional permission under GNU GPL version 3 section 7
//
// If you modify this program, or any covered work, by linking or combining it
// with embedded mcl code and modules (and that the embedded mcl code and
// modules which link with this program, contain a copy of their source code in
// the authoritative form) containing parts covered by the terms of any other
// license, the licensors of this program grant you additional permission to
// convey the resulting work. Furthermore, the licensors of this program grant
// the original author, James Shubin, additional permission to update this
// additional permission if he deems it necessary to achieve the goals of this
// additional permission.
package resources
import (
"compress/gzip"
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"os"
"path"
"strings"
"github.com/purpleidea/mgmt/engine"
"github.com/purpleidea/mgmt/engine/traits"
"github.com/purpleidea/mgmt/lang/funcs/vars"
"github.com/purpleidea/mgmt/lang/interfaces"
"github.com/purpleidea/mgmt/lang/types"
"github.com/purpleidea/mgmt/util/errwrap"
"github.com/purpleidea/mgmt/util/recwatch"
)
func init() {
engine.RegisterResource("gzip", func() engine.Res { return &GzipRes{} })
// const.res.gzip.level.no_compression = 0
// const.res.gzip.level.best_speed = 1
// const.res.gzip.level.best_compression = 9
// const.res.gzip.level.default_compression = -1
// const.res.gzip.level.huffman_only = -2
vars.RegisterResourceParams("gzip", map[string]map[string]func() interfaces.Var{
"level": {
"no_compression": func() interfaces.Var {
return &types.IntValue{
V: gzip.NoCompression,
}
},
"best_speed": func() interfaces.Var {
return &types.IntValue{
V: gzip.BestSpeed,
}
},
"best_compression": func() interfaces.Var {
return &types.IntValue{
V: gzip.BestCompression,
}
},
"default_compression": func() interfaces.Var {
return &types.IntValue{
V: gzip.DefaultCompression,
}
},
"huffman_only": func() interfaces.Var {
return &types.IntValue{
V: gzip.HuffmanOnly,
}
},
},
})
}
// GzipRes is a resource that compresses a path or some raw data using gzip. The
// name of the resource is the path to the resultant compressed file. The input
// can either come from a file path if specified with Input or it looks at the
// Content field for raw data. It uses hashes to determine if something was
// changed, so as a result, this may not be suitable if you can create a sha256
// hash collision.
// TODO: support send/recv to send the output instead of writing to a file?
type GzipRes struct {
traits.Base // add the base methods without re-implementation
init *engine.Init
// Path, which defaults to the name if not specified, represents the
// destination path for the compressed file being created. It must be an
// absolute path, and as a result must start with a slash. Since it is a
// file, it must not end with a slash.
Path string `lang:"path" yaml:"path"`
// Input represents the input file to be compressed. It must be an
// absolute path, and as a result must start with a slash. Since it is a
// file, it must not end with a slash. If this is specified, we use it,
// otherwise we use the Content parameter.
Input *string `lang:"input" yaml:"input"`
// Content is the raw data to compress. If Input is not specified, then
// we use this parameter. If you forget to specify both of these, then
// you will compress zero-length data!
// TODO: If this is also empty should we just error at Validate?
// FIXME: Do we need []byte here? Do we need a binary type?
Content string `lang:"content" yaml:"content"`
// Level is the compression level to use. If you change this, then the
// file will get recompressed. The available values are:
// const.res.gzip.level.no_compression, const.res.gzip.level.best_speed,
// const.res.gzip.level.best_compression,
// const.res.gzip.level.default_compression, and
// const.res.gzip.level.huffman_only.
Level int `lang:"level" yaml:"level"`
// SendOnly specifies that we don't write the file to disk, and as a
// result, the output is only be accessible by the send/recv mechanism.
// TODO: Rename this?
// TODO: Not implemented
//SendOnly bool `lang:"sendonly" yaml:"sendonly"`
// sha256sum is the hash of the content if it's using obj.Content here.
sha256sum string
// varDirPathInput is the path we use to store the content hash.
varDirPathInput string
// varDirPathOutput is the path we use to store the output file hash.
varDirPathOutput string
}
// getPath returns the actual path to use for this resource. It computes this
// after analysis of the Path and Name.
func (obj *GzipRes) getPath() string {
p := obj.Path
if obj.Path == "" { // use the name as the path default if missing
p = obj.Name()
}
return p
}
// Default returns some sensible defaults for this resource.
func (obj *GzipRes) Default() engine.Res {
return &GzipRes{
Level: gzip.DefaultCompression,
}
}
// Validate if the params passed in are valid data.
func (obj *GzipRes) Validate() error {
if obj.getPath() == "" {
return fmt.Errorf("path is empty")
}
if !strings.HasPrefix(obj.getPath(), "/") {
return fmt.Errorf("path must be absolute")
}
if strings.HasSuffix(obj.getPath(), "/") {
return fmt.Errorf("path must not end with a slash")
}
if obj.Input != nil {
if !strings.HasPrefix(*obj.Input, "/") {
return fmt.Errorf("input must be absolute")
}
if strings.HasSuffix(*obj.Input, "/") {
return fmt.Errorf("input must not end with a slash")
}
}
// This validation logic was observed in the gzip source code.
if obj.Level < gzip.HuffmanOnly || obj.Level > gzip.BestCompression {
return fmt.Errorf("invalid compression level: %d", obj.Level)
}
return nil
}
// Init runs some startup code for this resource.
func (obj *GzipRes) Init(init *engine.Init) error {
obj.init = init // save for later
dir, err := obj.init.VarDir("")
if err != nil {
return errwrap.Wrapf(err, "could not get VarDir in Init()")
}
// return unique files
obj.varDirPathInput = path.Join(dir, "input.sha256")
obj.varDirPathOutput = path.Join(dir, "output.sha256")
if obj.Input != nil {
return nil
}
// This is all stuff that's done when we're using obj.Content instead...
sha256sum, err := obj.hashContent(strings.NewReader(obj.Content))
if err != nil {
return err
}
obj.sha256sum = sha256sum
return nil
}
// Cleanup is run by the engine to clean up after the resource is done.
func (obj *GzipRes) Cleanup() error {
return nil
}
// Watch is the primary listener for this resource and it outputs events.
func (obj *GzipRes) Watch(ctx context.Context) error {
recurse := false // single file
recWatcher, err := recwatch.NewRecWatcher(obj.getPath(), recurse)
if err != nil {
return err
}
defer recWatcher.Close()
var events chan recwatch.Event
if obj.Input != nil {
recWatcher, err := recwatch.NewRecWatcher(*obj.Input, recurse)
if err != nil {
return err
}
defer recWatcher.Close()
events = recWatcher.Events()
}
obj.init.Running() // when started, notify engine that we're running
var send = false // send event?
for {
select {
case event, ok := <-recWatcher.Events():
if !ok { // channel shutdown
// TODO: Should this be an error? Previously it
// was a `return nil`, and i'm not sure why...
//return nil
return fmt.Errorf("unexpected close")
}
if err := event.Error; err != nil {
return errwrap.Wrapf(err, "unknown %s watcher error", obj)
}
if obj.init.Debug { // don't access event.Body if event.Error isn't nil
obj.init.Logf("event(%s): %v", event.Body.Name, event.Body.Op)
}
send = true
case event, ok := <-events:
if !ok { // channel shutdown
return fmt.Errorf("unexpected close")
}
if err := event.Error; err != nil {
return err
}
if obj.init.Debug { // don't access event.Body if event.Error isn't nil
obj.init.Logf("event(%s): %v", event.Body.Name, event.Body.Op)
}
send = true
case <-ctx.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 checks the resource state and applies the resource if the bool
// input is true. It returns error info and if the state check passed or not.
// This is where we actually do the compression work when needed.
func (obj *GzipRes) CheckApply(ctx context.Context, apply bool) (bool, error) {
h1, err := obj.hashFile(obj.getPath()) // output
if err != nil {
return false, err
}
h2, err := obj.readHashFile(obj.varDirPathOutput)
if err != nil {
return false, err
}
i1 := obj.sha256sum
if obj.Input != nil {
h, err := obj.hashFile(*obj.Input)
if err != nil {
return false, err
}
i1 = h
}
i1 = obj.levelPrefix() + i1 // add the level prefix so it is considered
i2, err := obj.readHashFile(obj.varDirPathInput)
if err != nil {
return false, err
}
// We're cheating by computing this before we know if we errored!
inputMatches := i1 == i2
outputMatches := h1 == h2
if err == nil && inputMatches && outputMatches {
// If the two hashes match, we assume that the file is correct!
// The file has to also exist of course...
return true, nil
}
if !apply {
return false, nil
}
fail := true // assume we have a failure
defer func() {
if !fail {
return
}
// Don't leave a partial file lying around...
obj.init.Logf("removing partial gzip file")
err := os.Remove(obj.getPath())
if err == nil || os.IsNotExist(err) {
return
}
obj.init.Logf("error removing corrupt gzip file: %v", err)
}()
// FIXME: Do we instead want to write to a tmp file and do a move once
// we finish writing to be atomic here and avoid partial corrupt files?
// FIXME: Add a param called Atomic to specify that behaviour. It's
// instant so that might be preferred as it might generate fewer events,
// but there's a chance it's copying from obj.init.VarDir() to a
// different filesystem.
outputFile, err := os.Create(obj.getPath()) // io.Writer
if err != nil {
return false, err
}
//defer outputFile.Sync() // not needed?
defer outputFile.Close()
hash := sha256.New()
// Write to both to avoid needing to wait for fsync to calculate hash!
multiWriter := io.MultiWriter(outputFile, hash)
gzipWriter, err := gzip.NewWriterLevel(multiWriter, obj.Level) // (*gzip.Writer, error)
if err != nil {
return false, err
}
var input io.Reader
if obj.Input != nil {
f, err := os.Open(*obj.Input) // io.Reader
if err != nil && !os.IsNotExist(err) {
// This is likely a permissions error.
return false, err
} else if err != nil {
return false, err // File doesn't exist!
}
defer f.Close()
input = f
} else {
input = strings.NewReader(obj.Content)
}
// Copy the input file into the writer, which writes it out compressed.
count, err := io.Copy(gzipWriter, input) // dst, src
if err != nil {
gzipWriter.Close() // Might as well always close!
return false, err
}
// NOTE: Must run this before hashing so that it includes the footer!
if err := gzipWriter.Close(); err != nil {
return false, err
}
sha256sum := hex.EncodeToString(hash.Sum(nil))
obj.init.Logf("wrote %d gzipped bytes", count)
// After gzip is successfully written, store the hashed input result.
if !inputMatches {
if err := os.WriteFile(obj.varDirPathInput, []byte(i1+"\n"), 0600); err != nil {
return false, err
}
}
// Also store the new hashed output result.
if !outputMatches || h2 == "" { // If missing, we always write it out!
if err := os.WriteFile(obj.varDirPathOutput, []byte(sha256sum+"\n"), 0600); err != nil {
return false, err
}
}
fail = false // defer can exit safely!
return false, nil
}
// levelPrefix is a simple helper to add a level identifier for our hash.
func (obj *GzipRes) levelPrefix() string {
return fmt.Sprintf("level:%d|", obj.Level)
}
// hashContent is a simple helper to run our hashing function.
func (obj *GzipRes) hashContent(handle io.Reader) (string, error) {
hash := sha256.New()
if _, err := io.Copy(hash, handle); err != nil {
return "", err
}
return hex.EncodeToString(hash.Sum(nil)), nil
}
// hashFile is a helper that returns the hash of the specified file. If the file
// doesn't exist, it returns the empty string. Otherwise it errors.
func (obj *GzipRes) hashFile(file string) (string, error) {
f, err := os.Open(file) // io.Reader
if err != nil && !os.IsNotExist(err) {
// This is likely a permissions error.
return "", err
} else if err != nil {
return "", nil // File doesn't exist!
}
defer f.Close()
// File exists, lets hash it!
return obj.hashContent(f)
}
// readHashFile reads the hashed value that we stored for the output file.
func (obj *GzipRes) readHashFile(file string) (string, error) {
// TODO: Use io.ReadFull to avoid reading in a file that's too big!
if expected, err := os.ReadFile(file); err != nil && !os.IsNotExist(err) { // ([]byte, error)
// This is likely a permissions error?
return "", err
} else if err == nil {
return strings.TrimSpace(string(expected)), nil
}
// File doesn't exist!
return "", nil
}
// Cmp compares two resources and returns an error if they are not equivalent.
func (obj *GzipRes) Cmp(r engine.Res) error {
// we can only compare GzipRes to others of the same resource kind
res, ok := r.(*GzipRes)
if !ok {
return fmt.Errorf("not a %s", obj.Kind())
}
if obj.Path != res.Path {
return fmt.Errorf("the Path differs")
}
if (obj.Input == nil) != (res.Input == nil) { // xor
return fmt.Errorf("the Input differs")
}
if obj.Input != nil && res.Input != nil {
if *obj.Input != *res.Input { // compare the strings
return fmt.Errorf("the contents of Input differ")
}
}
if obj.Content != res.Content {
return fmt.Errorf("the Content differs")
}
if obj.Level != res.Level {
return fmt.Errorf("the Level differs")
}
return nil
}
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
// primarily useful for setting the defaults.
func (obj *GzipRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
type rawRes GzipRes // indirection to avoid infinite recursion
def := obj.Default() // get the default
res, ok := def.(*GzipRes) // put in the right format
if !ok {
return fmt.Errorf("could not convert to GzipRes")
}
raw := rawRes(*res) // convert; the defaults go here
if err := unmarshal(&raw); err != nil {
return err
}
*obj = GzipRes(raw) // restore from indirection with type conversion!
return nil
}

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2024+ James Shubin and the project contributors
// Copyright (C) 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-2024+ James Shubin and the project contributors
// Copyright (C) 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,6 +39,7 @@ import (
engineUtil "github.com/purpleidea/mgmt/engine/util"
"github.com/purpleidea/mgmt/util"
"github.com/purpleidea/mgmt/util/errwrap"
"github.com/purpleidea/mgmt/util/recwatch"
"github.com/godbus/dbus/v5"
)
@@ -57,7 +58,10 @@ const (
// resource is insufficient for the resource to do any useful work.
var ErrResourceInsufficientParameters = errors.New("insufficient parameters for this resource")
// HostnameRes is a resource that allows setting and watching the hostname.
// HostnameRes is a resource that allows setting and watching the hostname. If
// you don't specify any parameters, the Name is used. The Hostname field is
// used if none of the other parameters are used. If the parameters are set to
// the empty string, then those variants are not managed by the resource.
type HostnameRes struct {
traits.Base // add the base methods without re-implementation
@@ -72,22 +76,54 @@ type HostnameRes struct {
// PrettyHostname is a free-form UTF8 host name for presentation to the
// user.
PrettyHostname string `lang:"pretty_hostname" yaml:"pretty_hostname"`
PrettyHostname *string `lang:"pretty_hostname" yaml:"pretty_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 string `lang:"static_hostname" yaml:"static_hostname"`
StaticHostname *string `lang:"static_hostname" yaml:"static_hostname"`
// 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.
TransientHostname string `lang:"transient_hostname" yaml:"transient_hostname"`
TransientHostname *string `lang:"transient_hostname" yaml:"transient_hostname"`
conn *dbus.Conn
}
func (obj *HostnameRes) getHostname() string {
if obj.Hostname != "" {
return obj.Hostname
}
return obj.Name()
}
func (obj *HostnameRes) getPrettyHostname() string {
if obj.PrettyHostname != nil {
return *obj.PrettyHostname // this may be empty!
}
return obj.getHostname()
}
func (obj *HostnameRes) getStaticHostname() string {
if obj.StaticHostname != nil {
return *obj.StaticHostname // this may be empty!
}
return obj.getHostname()
}
func (obj *HostnameRes) getTransientHostname() string {
if obj.TransientHostname != nil {
return *obj.TransientHostname // this may be empty!
}
return obj.getHostname()
}
// Default returns some sensible defaults for this resource.
func (obj *HostnameRes) Default() engine.Res {
return &HostnameRes{}
@@ -95,7 +131,10 @@ func (obj *HostnameRes) Default() engine.Res {
// Validate if the params passed in are valid data.
func (obj *HostnameRes) Validate() error {
if obj.PrettyHostname == "" && obj.StaticHostname == "" && obj.TransientHostname == "" {
a := obj.getPrettyHostname() == ""
b := obj.getStaticHostname() == ""
c := obj.getTransientHostname() == ""
if a && b && c && obj.getHostname() == "" {
return ErrResourceInsufficientParameters
}
return nil
@@ -105,15 +144,6 @@ func (obj *HostnameRes) Validate() error {
func (obj *HostnameRes) Init(init *engine.Init) error {
obj.init = init // save for later
if obj.PrettyHostname == "" {
obj.PrettyHostname = obj.Hostname
}
if obj.StaticHostname == "" {
obj.StaticHostname = obj.Hostname
}
if obj.TransientHostname == "" {
obj.TransientHostname = obj.Hostname
}
return nil
}
@@ -124,6 +154,13 @@ func (obj *HostnameRes) Cleanup() error {
// Watch is the primary listener for this resource and it outputs events.
func (obj *HostnameRes) Watch(ctx context.Context) error {
recurse := false // single file
recWatcher, err := recwatch.NewRecWatcher("/etc/hostname", recurse)
if err != nil {
return err
}
defer recWatcher.Close()
// if we share the bus with others, we will get each others messages!!
bus, err := util.SystemBusPrivateUsable() // don't share the bus connection!
if err != nil {
@@ -149,7 +186,23 @@ func (obj *HostnameRes) Watch(ctx context.Context) error {
var send = false // send event?
for {
select {
case <-signals:
case _, ok := <-signals:
if !ok { // channel shutdown
return fmt.Errorf("unexpected close")
}
//signals = nil
send = true
case event, ok := <-recWatcher.Events():
if !ok { // channel shutdown
return fmt.Errorf("unexpected close")
}
if err := event.Error; err != nil {
return err
}
if obj.init.Debug { // don't access event.Body if event.Error isn't nil
obj.init.Logf("event(%s): %v", event.Body.Name, event.Body.Op)
}
send = true
case <-ctx.Done(): // closed by the engine to signal shutdown
@@ -189,10 +242,10 @@ func (obj *HostnameRes) updateHostnameProperty(object dbus.BusObject, expectedVa
}
// attempting to apply the changes
obj.init.Logf("Changing %s: %s => %s", property, propertyValue, expectedValue)
if err := object.Call("org.freedesktop.hostname1."+setterName, 0, expectedValue, false).Err; err != nil {
return false, errwrap.Wrapf(err, "failed to call org.freedesktop.hostname1.%s", setterName)
}
obj.init.Logf("changed %s: `%s` => `%s`", property, propertyValue, expectedValue)
// all good changes should now be applied again
return false, nil
@@ -209,22 +262,22 @@ func (obj *HostnameRes) CheckApply(ctx context.Context, apply bool) (bool, error
hostnameObject := conn.Object(hostname1Iface, hostname1Path)
checkOK := true
if obj.PrettyHostname != "" {
propertyCheckOK, err := obj.updateHostnameProperty(hostnameObject, obj.PrettyHostname, "PrettyHostname", "SetPrettyHostname", apply)
if h := obj.getPrettyHostname(); h != "" {
propertyCheckOK, err := obj.updateHostnameProperty(hostnameObject, h, "PrettyHostname", "SetPrettyHostname", apply)
if err != nil {
return false, err
}
checkOK = checkOK && propertyCheckOK
}
if obj.StaticHostname != "" {
propertyCheckOK, err := obj.updateHostnameProperty(hostnameObject, obj.StaticHostname, "StaticHostname", "SetStaticHostname", apply)
if h := obj.getStaticHostname(); h != "" {
propertyCheckOK, err := obj.updateHostnameProperty(hostnameObject, h, "StaticHostname", "SetStaticHostname", apply)
if err != nil {
return false, err
}
checkOK = checkOK && propertyCheckOK
}
if obj.TransientHostname != "" {
propertyCheckOK, err := obj.updateHostnameProperty(hostnameObject, obj.TransientHostname, "Hostname", "SetHostname", apply)
if h := obj.getTransientHostname(); h != "" {
propertyCheckOK, err := obj.updateHostnameProperty(hostnameObject, h, "Hostname", "SetHostname", apply)
if err != nil {
return false, err
}
@@ -242,13 +295,13 @@ func (obj *HostnameRes) Cmp(r engine.Res) error {
return fmt.Errorf("not a %s", obj.Kind())
}
if obj.PrettyHostname != res.PrettyHostname {
if engineUtil.StrPtrCmp(obj.PrettyHostname, res.PrettyHostname) != nil {
return fmt.Errorf("the PrettyHostname differs")
}
if obj.StaticHostname != res.StaticHostname {
if engineUtil.StrPtrCmp(obj.StaticHostname, res.StaticHostname) != nil {
return fmt.Errorf("the StaticHostname differs")
}
if obj.TransientHostname != res.TransientHostname {
if engineUtil.StrPtrCmp(obj.TransientHostname, res.TransientHostname) != nil {
return fmt.Errorf("the TransientHostname differs")
}
@@ -271,9 +324,9 @@ func (obj *HostnameRes) UIDs() []engine.ResUID {
x := &HostnameUID{
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
name: obj.Name(),
prettyHostname: obj.PrettyHostname,
staticHostname: obj.StaticHostname,
transientHostname: obj.TransientHostname,
prettyHostname: obj.getPrettyHostname(),
staticHostname: obj.getStaticHostname(),
transientHostname: obj.getTransientHostname(),
}
return []engine.ResUID{x}
}

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2024+ James Shubin and the project contributors
// Copyright (C) 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-2024+ James Shubin and the project contributors
// Copyright (C) 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
@@ -238,12 +238,8 @@ func (obj *HTTPFlagRes) Watch(ctx context.Context) error {
// CheckApply never has anything to do for this resource, so it always succeeds.
func (obj *HTTPFlagRes) CheckApply(ctx context.Context, apply bool) (bool, error) {
if obj.init.Debug {
obj.init.Logf("CheckApply")
}
if obj.init.Debug || true { // XXX: maybe we should always do this?
obj.init.Logf("CheckApply: value: %+v", obj.value)
obj.init.Logf("value: %+v", obj.value)
}
// TODO: can we send an empty (nil) value to show it has been removed?

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2024+ James Shubin and the project contributors
// Copyright (C) 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
@@ -196,7 +196,8 @@ func (obj *HTTPProxyRes) serveHTTP(ctx context.Context, requestPath string) (han
// Tell the client right away, that we're working on things...
// TODO: Is this valuable to give us more time to block?
w.WriteHeader(http.StatusProcessing) // http 102, RFC 2518, 10.1
// NOTE: Using this header breaks wget2!
//w.WriteHeader(http.StatusProcessing) // http 102, RFC 2518, 10.1
response, err := client.Do(request) // (*Response, error)
if err != nil {

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