Event-Driven Architecture
This document describes an architectural transition inside of Helm. The client-server model of Helm 2 will be replaced with a single event-driven model.
High level logic will emit events, while event responders will handle events.
Some event handlers will be internal-only. Other event handlers will be exposed to the new lifecycle events subsystem (with its Lua engine). Those events will be open to extension.
The Lifecycle Events Architecture
Helm 3 is internally reorganized around an event model. Some events are internal only, while others are exposed to the Lua scripting engine. In this latter case, chart authors may script certain behaviors.
During various phases of an operation (like install), Helm will execute pre-defined events. Charts will have an opportunity to respond to these events.
For example, during helm install, the process my look something like this:
- Load chart
- Merge values
- Emit
pre-renderevent - Render templates
- Parse resulting manifest into objects
- Emit
post-renderevent - Validate all objects
- Call
pre-installevent - Install the manifests into Kubernetes
- Exit
Scripting Event Handlers
An embedded Lua interpreter can load scripts at runtime, which makes it possible to write code which can be stored inside of a chart, but executed in answer to a lifecycle event.
In Lua, executing a particular hook might look something like this:
-- events is used to register event handlers.
events.on("pre-render", 0.5, function (_)
-- Accessing an object
print("got chart " + _.chart.name)
-- Mutating an object
_.values.myvalue = "My Value"
end)
The event handler format is: events.on(_), where _ is a variable pointing to
a context object (see next section).
Script locations
Scripts are stored inside charts. They are stored in charts for the following reasons:
- Charts remain self-contained. There is no need to negotiate on which plugins a chart needs.
- Dependency resolution is relegated to the chart scope
- The ordering problem is clearly solved (see below) if the scripts are located inside of the chart.
Lua scripts are stored in a chart's ext/lua folder. The ext/lua/chart.lua file
is loaded automatically along with the chart, and it may require() in other
Lua scripts.
The order of loading goes like this:
- Chart is verified (if requested) against
.provfile - Chart is loaded into memory
- Chart.yaml is parsed, validated, and loaded
ext/lua/chart.luafiles are parsed and loaded into a runtimechart-loadedevent is fired on chart that was loaded
Note: Values from
-fand--setare coalesced prior to chart loading.
Charts and subcharts may each have Lua. When events are triggered, the event
handler will do a depth-first execution of the chart.lua files, ending with the
topmost chart.lua file.
The Context Object for Events
Every event will receive a context, which will contain data about that event. In pseudo-code, the context stucture looks like this:
context = {
-- Loaded from chart.yaml
chart = {
version = function () return "v2" end,
name = function () return "alpine" end,
version = function () return "1.1.0" end,
description = function() return "This is an example chart." end,
keywords = function() return { "alpine", "utility", "debugging" } end,
home = function() return "https://docs.helm.sh" end,
sources = function() return { "https://github.com/helm/helm" } end,
maintainers = function() return {
name = "M Butcher",
email = "mbutcher@example.com",
url = "http://mbutcher.example.com"
} end,
icon = function() return "https://example.com/alpine.svg" end,
appVersion = function() return "3.8" end,
kubernetesVersions = function() return ">1.9.0" end,
},
-- All user-specified. None of these is hard-coded
values = {
rbac = { enabled = true },
image = { name = "alpine", tag = "3.8"}
},
-- These are supplied by the system and the user
release = {
name = function() return "my-alpine" end
},
-- Information about the cluster that Helm is currently
-- targeting
capabilities = {
helmVersion = function() return "3.0.0-alpha.1" end,
-- Kubernetes version, as reported by Kubernetes
kubernetesVersion = function() return "1.10.0" end,
apiVersions = function() return {
"v1",
"batch/v1",
"batch/v2",
"batch/v2beta1",
-- ...
} end
},
-- Files specified in the chart, exclusing templates and
-- the contents of ext/
files = {
{ name = "config.json", data = "{}" }
},
-- Any gotopl files. Depending on the phase, modification of
-- these may have no impact.
templates = {
{ name = "pod.yaml", data = "apiVersion: v1\nkind: Pod\nname: {{.Release.Name }}\n#..." }
},
dependencies = {
{name = "nginx", version = "1.2.0", repository = "https://example.com/charts"}
},
-- Depending on phase, this may contain the list of already
-- rendered objects. It may be empty. Depending on the
-- phase, the contents of this may, on return, be preserved.
objects = {
{
apiVersion = "v1",
kind = "Pod",
name = "my-alpine"
}
}
}
Note that some of the properties in the context are functions. These are implemented as functions because their values are immutable. (The implementation of the function is informational pseudo-code, and is not the real implementation.)
[name=Matt Butcher] Options for subcharts: We can either collapse them into one top-level context or nest subcharts in a
charts = { context::new() }way.
[name=Stuart P. Bentley] As a Lua developer, I don't see any reason why immutable values should be specified as functions - it introduces an arbitrary distinction between access to mutable and immutable values, and I could still just clobber the function by assigning over it. If you want to enforce a read-only constraint on the immutable fields, the proper way to do it would be to use a table with a
__indexmetamethod/table for the immutable values (and a__newindexmetamethod to stop them from being overwritten).
Events
This is meant to be an exhaustive list of the events that will be emitted in Helm 3.
The following are events by command. If a command is not present, then that command is not slated for inclusion in Helm 3.
For each command, the events are presented in the order that they occur.
Create
Events for helm create:
pre-createpost-create
The pre-create event will receive a minimal context with no chart information. A post-create will receive the initialized chart. After a post-create, the chart will be written to the filesystem.
Dependency Build
pre-dependency-buildpost-dependency-build
The pre- hook will have access to the base chart's context.
The pre- hook will be able to modify the dependencies object before dependencies are resolved or fetched.
The post- hook will have access to the context as it appears after all dependencies have been resolved. The results of this hook are not written to the filesystem.
Dependency [List, Update]
No other helm dependency * commands have hooks
Delete
pre-delete
The pre-delete event will receive a minimal context. It will not receive the chart.
Fetch
There are no defined hooks for this command
Get [manifests, values]
There are no defined hooks for these commands
History
There are no defined hooks for this command
Home
There are no defined hooks for this command
Init
There are no defined hooks for this command
Inspect
There are no defined hooks for this command
Install
pre-renderpost-renderpre-install
pre-/post-render
The pre- and post-render events are common across several operations.
pre-render occurs before the contents of the context.templates have been rendered. The context.objects array is thus empty prior to pre-render.
The post-render occurs after templates have been rendered to objects. Any further modifications to context.templates are ignored. The context.objects array will contain object representations of the things created after the template run.
pre-install
The pre-install event fires after post-render, and will receive the context returned from post-render. This event only fires on the install command, and thus is for install-specific work.
Lint
pre-renderpost-renderpre-lint
The pre-lint event is fired after post-render, and is only fired for lint.
Package
pre-package
The pre-package hook is fired immediately before the package operation, and changes to context may be written to the packaged chart, but not onto the filesystem.
Plugin [*]
There are no defined hooks for this command
Repo [*]
There are no defined hooks for this command
Rollback
This is dependent on a possible re-architecting of helm rollback that will no longer include a render phase.
pre-renderpost-renderpre-rollback
The pre-rollback event fires after the post-render, and with the same context. This event only fires on rollback.
Search
There are no defined hooks for this command
Template
pre-renderpost-renderpost-template(name may change)
The post-template event fires after post-render and receives the same context. It fires immediately before the output of the command is written to STDOUT.
Test
pre-renderpost-renderpre-testpost-testfailed-test
pre-test is executed immediately before the test, and it may modify the charts prior to testing. post-test runs after the test. failed-test runs only if the test is marked failed.
Upgrade
pre-renderpost-renderpre-upgradepost-upgrade
The pre-upgrade event fires after post-render, receiving the same context. post-upgrade fires after the chart has been submitted to the server. These two events only fire during upgrades.
Verify
There are no defined hooks for this command
Version
There are no defined hooks for this command