Replies: 1 comment
-
This has been updated to reflect what is currently implemented in the following two PRs: |
Beta Was this translation helpful? Give feedback.
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
-
Design for Chalk component/module system
John + Theo
Intro
Config file bits in examples and HOWTOs need to not require people to
have to edit configuration files, and should be COMPOSABLE.
For instance, we should have one component for the https server that
people can use with pretty much ANY content-based component. If some
component REQUIRES such a server component be installed, it could
explicitly require it.
We've spent some time thinking about how this might all manifest in con4m
in a way that wouldn't take too long to implement, and I have a strawman
here, that I'm going to introduce by example.
I'm going to take the existing app-inventory config
(chalkdust.io/app-inventory.c4m) and rewrite it using our strawman.
It would span multiple components; components would be individual files
that sort of act like parameterless functions in the code, you would invoke
them with a
use
statement, and you can invoke them multiple times frommultiple places.
We'll go into more detail about the semantics and the rational below. First,
we want to go through an example.
The how-to linked did a few things that are really a collection of small
things that some other configs we provide will want to do:
the default for when we're adding a server config)
the code was left commented in the source).
Those bits really should be composable across examples -- all of them
should be usable across multiple configs, and people should be able to
combine bits from different configs to their hearts' content.
But, whenever a component DOES get used, it should be configured the
same way, and only should need to be configured once, for instance, if
a micro-component X is used by A, B and C, and my config uses all
three of those.
The code below is presented w/o comments first, so you can
get the sense of the scope. I'll do a deep dive after. For now, just
note that things in "parameter" blocks will be the bits that can
appear in UI elements. The rest is just the module's code that gets run.
terminal_report.c4m
log_report.c4m
impersonate_docker.c4m
wrap_entrypoints.c4m
reporting_server.c4m
compliance_docker.c4m
Component basics, and the
use
statementAt runtime, the
use
statement causes a component to run a component.The 'from url' bit is optional, and defaults to chalkdust.io. The source of all
dependent components gets cached in the chalk mark of the chalk exe,
and currently do not get reloaded unless you remove your entire configuration
(
chalk load default
) .Statically, these statements are analyzed before we execute,
and any dependent components are fetched and validated.
Caching happens if the config successfully loads.
Therefore, at runtime, the
use
statement really is just a call intothe named module. And if you put a
use
statement in a loop,the module's code gets run each time through the loop.
Similarly, if the module is in an
if
conditional, it won't run unless thatbranch gets taken at runtime.
However, if a module uses any parameters (see below), a value
for each parameter must be provided before any execution begins. This is
true even for parameters that can be indirectly used if a module itself
use
sother modules.
The static checking forbids cycles in module importing.
Every component is uniquely identified via a hash of its source code,
which will be useful for versioning when we know what we're doing there.
Conflicts across components
When components execute, any values they explicitly set in their body
are locked, meaning that future assignments to those variables will fail if
they try to change the value.
This is how we ensure that multiple components are truly compatible, and
how we ensure that any custom config code written directly in con4m is
compatible. For example, if one component always sets
default_command = docker
, and another always setsdefault_command = help
, then the configuration validation phase, wherethe configuration gets run, would fail, because the second component will
blow up execution when it's now allowed to set the value.
In cases where there's conditional code based on the runtime environment,
if two components have conflicting code, but one doesn't run, then the
configuration is fine under those conditions.
The 'locking' semantics apply to any (non-builtin) con4m function called
during a module's execution. If those modules export functions that are
not run, but are expected to be called from the top-level user context,
they will not lock.
Parameter sections
Parameters must be either attributes or variables that are used inside
the code of the component.
If parameter declarations are variables, the variable name must be
proceeded by
var
; variable names can never be dotted the wayattributes can be.
The
default
field of a parameter would set the default value whenthe component is loaded, and this will override whatever is the config
default from chalk.c42spec or the base configuration. If this is not
provided, then the component will not run without a value being given
by the runtime first.
If the
default
field is set to a function pointer, then chalk will callit when it needs the default value, allowing dynamic defaults based
on the environment.
Note that if there were ever a reason to have a parameter that is itself
a callback of some sort, I believe the
default
field would only be usableas a value. I thought about just disallowing it, and chalk would not be
able to marshal callbacks anyway. For now, we will just disallow this in any
components we publish, as it's not important enough to worry about now.
The
default
value / callback ONLY applies when the user has not agreedupon a configured value. See the command-line usage section below for
details on the config process.
Once the config process completes, Chalk keeps the value of any parameters
cached in the chalk mark, next to the cached configuration. Once these values
are set, the cached values are always used unless one reconfigures.
Parameters also get an optional
validation
field, allowing you to specifya callback that will examine the value and test it, for when the provided
type checking isn't enough (for instance, the URL example used in the
sample code above).
Parameters also get a
doc
andshortdoc
fields. This documentationis presented in the configuration process. It's optional, but highly
recommended.
Command-lind usage
In the initial implementation, one uses
chalk load
to install a componentinto an existing configuration, and to configure any parameters used by that
component.
For instance,
chalk load foo.c4m
would cachefoo.c4m
from yourworking directory, and add a statement to your embedded configuration like:
Actually, it will add the "from" part too, but since it doesn't update its cached
copy unless you reset your config, that's not particularly relevant as long
as you don't edit the
from
portion in your config (the caching is done bythe full location at the time installed).
To avoid people overwriting their configs every time they want to install a
new module, the default behavior of
chalk load
is now to assume you areonly looking to add a component. Or, if you've already added it, to reconfigure
it.
However, the
--replace
flag (or theload.replace_conf
field in the configif you want to change the default behavior) will cause
chalk load
to removeany existing configuration before adding the specified component.
When you call
chalk load
, any parameters used by the module you'reloading will go through the configuration process. For now, that process
consists of interactive prompting, allowing you to just hit 'enter' to accept
the default (or the previous value).
This only triggers parameters that can be used by the component loaded,
but does include components used by any dependent components.
Notes on specific components above
The micro-component for terminal summary reports basically makes
terminal summary reports OPTIONAL by wrapping setting stuff in a
conditional controlled by a variable the user controls.
In contrast,
impersonate_docker
ends up REQUIRING that the resultingconfig sets
default_config
to true.Similarly,
entrypoint_wrapping
forcesdocker.wrap_entrypoint
to betrue. Because anything set in these components will be
locked
,per above.
Roadmap Items
Most of these items have no specific time frame, but constitute the
enhancements that, as of this writing, we'd definitely like to make at
some point.
Better execution-time errors (including stack traces). For instance,
when an attribute is 'locked' by virtue of a component setting its
value, if some conflict arises, we do not currently show where in the
config that happens. In fact, that's the case w/ any runtime con4m
error.
An app-store style (terminal) interface, which would be used for
both selecting components, and for configuring / reconfiguring. In the store
UI, we'd probably need some comment-based annotation in modules to
determine what to show / hide.
A con4m
import
statement that works more like a traditional module import;The import would only happen once, and they would NOT support parameters, or
locking.
Versioning and an update approach. I originally had some semantics to allow
version annotations, but I realized the logic around when to check for updates,
when to auto-update and when to warn all required a lot of thinking, so decided
to just kick the can down the road on this.
Currently, if module A uses module B, and module B has some parameter, there's
no explicit mechanism for B to declare the parameter moot. For instance, if we wrote
a component that auto-downloaded and ran the server container in the above example,
there would still be a configuration parameter that the user would get prompted for.
For now, that's fine with me; as a work-around can just dupe the functionality needed.
Implementation Notes
Most of the implementation was done in the
con4m
codebase; over time, partsof the Chalk side of this might also get generalized and pulled into that code base.
Beta Was this translation helpful? Give feedback.
All reactions