feat(helm): initial add of event emitter support

feat-v3/event-emitter-lua
Matt Butcher 7 years ago
parent 647ffaf7d3
commit 7ca7525755
No known key found for this signature in database
GPG Key ID: DCD5F5E5EF32C345

19
Gopkg.lock generated

@ -463,6 +463,17 @@
packages = ["."]
revision = "ab470f5e105a44d0c87ea21bacd6a335c4816d83"
[[projects]]
branch = "master"
name = "github.com/yuin/gopher-lua"
packages = [
".",
"ast",
"parse",
"pm"
]
revision = "b0fa786cf4ea360285924c4a7fd42325be57aec8"
[[projects]]
name = "golang.org/x/crypto"
packages = [
@ -1069,6 +1080,12 @@
packages = ["exec"]
revision = "aedf551cdb8b0119df3a19c65fde413a13b34997"
[[projects]]
name = "layeh.com/gopher-luar"
packages = ["."]
revision = "7b2b96926970546e504881ee7364c1127508eb4e"
version = "v1.0.2"
[[projects]]
name = "vbom.ml/util"
packages = ["sortorder"]
@ -1077,6 +1094,6 @@
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "4a0464d8de132c8a733f50549ad69e663b992e12537b58626302dc5dd14cd3f0"
inputs-digest = "0f24a23217f31025f10e8a3d20eb5b2495298ea8453634cf730462c0e377e1f9"
solver-name = "gps-cdcl"
solver-version = 1

@ -50,3 +50,11 @@
[prune]
go-tests = true
unused-packages = true
[[constraint]]
branch = "master"
name = "github.com/yuin/gopher-lua"
[[constraint]]
name = "layeh.com/gopher-luar"
version = "1.0.2"

@ -135,6 +135,8 @@ func LoadFiles(files []*BufferedFile) (*chart.Chart, error) {
c.Values = f.Data
} else if strings.HasPrefix(f.Name, "templates/") {
c.Templates = append(c.Templates, &chart.File{Name: f.Name, Data: f.Data})
} else if strings.HasPrefix(f.Name, "ext/") {
c.Ext = append(c.Ext, &chart.File{Name: f.Name, Data: f.Data})
} else if strings.HasPrefix(f.Name, "charts/") {
if filepath.Ext(f.Name) == ".prov" {
c.Files = append(c.Files, &chart.File{Name: f.Name, Data: f.Data})

@ -78,6 +78,10 @@ icon: https://example.com/64x64.png
Name: path.Join("templates", ServiceName),
Data: []byte(defaultService),
},
{
Name: path.Join("ext", "lua", "chart.lua"),
Data: []byte(`local events = require("events")`),
},
}
c, err := LoadFiles(goodFiles)
@ -97,6 +101,10 @@ icon: https://example.com/64x64.png
t.Errorf("Expected number of templates == 2, got %d", len(c.Templates))
}
if l := len(c.Ext); l != 1 {
t.Errorf("Expected number of extensions == 1, got %d", l)
}
_, err = LoadFiles([]*BufferedFile{})
if err == nil {
t.Fatal("Expected err to be non-nil")

@ -0,0 +1,40 @@
package events
import (
"k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/hapi/chart"
//"k8s.io/helm/pkg/hapi/release"
)
type Context struct {
// ReleaseName is the name of the release.
ReleaseName string
// Revision is the release revision. This is a ULID or empty if no release
// has been stored.
Revision string
// Chart is the chart.
Chart *chart.Metadata
// Values is the override values (not the default values)
Values chartutil.Values
Notes string
// Manifests is the manifests that Kubernetes will install.
// Assume this is filename, content for now
//Manifests map[string][]byte
Manifests []string
Hooks []string
// Release is the release object
Release chartutil.ReleaseOptions
// Capabilities are passed by reference to avoid modifications bubbling up.
Capabilities chartutil.Capabilities
Files chartutil.Files
}

@ -0,0 +1,18 @@
/* Package events provides an event system for Helm.
*
* This is not a general purpose event framework. It is a system for handling Helm
* objects.
*
* An event Emitter is an object that binds events and event handlers. An EventHandler
* is a function that can respond to an event. And a Context is the information
* passed from the calling location into the event handler. All together, the
* system works by creating an event emitter, registering event handlers, and
* then calling the event handlers at the appropriate place and time, passing in
* a context as you go.
*
* In this particular implementation, the Context is considered mutable. So an
* event handler is allowed to change the contents of the context. It is up to
* the calling code to decide whether to "accept" those changes back into the
* calling context.
*/
package events

@ -0,0 +1,90 @@
package events
const (
EventChartLoad = "chart-load"
EventPreRender = "pre-render"
EventPostRender = "post-render"
EventPreInstall = "pre-install"
EventPostInstall = "post-install"
)
// EventHandler is a function capabile of responding to an event.
type EventHandler func(*Context) error
// registry is the storage for events and handlers.
type registry map[string][]EventHandler
// Emitter provides a way to register, manage, and execute events.
type Emitter struct {
reg registry
}
// New creates a new event Emitter with no registered events.
func New() *Emitter {
return &Emitter{
reg: registry{},
}
}
// Emit executes the given event with the given context.
//
// The first time an error occurs, this will cease the event cycle and return the
// error.
func (e *Emitter) Emit(event string, ctx *Context) error {
fns := e.reg[event]
for _, fn := range fns {
if err := fn(ctx); err != nil {
return err
}
}
return nil
}
// On binds an EventHandler to an event.
// If an event already has EventHandlers, this will be appended to the end of the
// list.
func (e *Emitter) On(event string, fn EventHandler) {
handlers, ok := e.reg[event]
if !ok {
e.reg[event] = []EventHandler{fn}
return
}
e.reg[event] = append(handlers, fn)
}
// Handlers returns all of the EventHandlers for the given event.
//
// If no handlers are registered for this event, then an empty array is returned.
// This is not an error condition, because it is perfectly normal for an event to
// have no registered handlers.
func (e *Emitter) Handlers(event string) []EventHandler {
handlers, ok := e.reg[event]
if !ok {
return []EventHandler{}
}
return handlers
}
// SetHandlers sets all of the handlers for a particular event.
//
// This will overwrite the existing event handlers for this event. It is allowed
// to set this to an empty list.
func (e *Emitter) SetHandlers(event string, listeners []EventHandler) {
e.reg[event] = listeners
}
// Len returns the number of event handlers registered for the given event.
func (e *Emitter) Len(event string) int {
return len(e.reg[event])
}
// Events returns the names of all of the events that have been registered.
// Note that this does not ensure that these events have registered handlers.
// See SetHandlers above.
func (e *Emitter) Events() []string {
h := make([]string, 0, len(e.reg))
for name := range e.reg {
h = append(h, name)
}
return h
}

@ -0,0 +1,86 @@
package events
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
)
func stubFunc(*Context) error {
return nil
}
func stubErrFunc(*Context) error {
return errors.New("nope")
}
var _ EventHandler = stubFunc
var _ EventHandler = stubErrFunc
var stubCtx = &Context{}
func TestEmitter(t *testing.T) {
emitter := New()
is := assert.New(t)
is.Len(emitter.Events(), 0, "expect no events")
is.Equal(0, emitter.Len("foo"), "expect empty foo event")
}
func TestEmitter_On(t *testing.T) {
emitter := New()
is := assert.New(t)
emitter.On("foo", stubFunc)
is.Equal([]string{"foo"}, emitter.Events(), "expect foo event")
is.Equal(1, emitter.Len("foo"), "expect one foo event handler")
emitter.On("foo", stubErrFunc)
is.Equal(2, emitter.Len("foo"), "expect two foo event handlers")
emitter.On("bar", stubFunc)
is.Equal([]string{"foo", "bar"}, emitter.Events(), "expect foo, bar events")
is.Equal(1, emitter.Len("bar"), "expect one bar event handler")
}
func TestEmitter_Handlers(t *testing.T) {
emitter := New()
is := assert.New(t)
is.Len(emitter.Handlers("foo"), 0, "expect no handlers")
emitter.On("foo", stubFunc)
is.Len(emitter.Handlers("foo"), 1, "expect one handler")
emitter.On("foo", stubFunc)
emitter.On("foo", stubErrFunc)
is.Len(emitter.Handlers("foo"), 3, "expect 3 handlers")
fn := emitter.Handlers("foo")[2]
is.Error(fn(stubCtx), "expect last one to be the stubErrFunc")
}
func TestEmitter_SetHandlers(t *testing.T) {
emitter := New()
is := assert.New(t)
emitter.On("foo", stubFunc)
emitter.SetHandlers("foo", []EventHandler{stubErrFunc})
is.Equal(emitter.Len("foo"), 1, "Expect one handler")
fn := emitter.Handlers("foo")[0]
is.Error(fn(stubCtx), "expect only handler to be stubErrFunc")
}
func TestEmitter_Emit(t *testing.T) {
emitter := New()
is := assert.New(t)
emitter.On("foo", stubFunc)
is.Nil(emitter.Emit("foo", stubCtx), "Expect no error")
emitter.On("foo", stubErrFunc)
is.Error(emitter.Emit("foo", stubCtx), "Expect error")
}

@ -0,0 +1,11 @@
// Package lua provides Lua event handling bindings
//
// This package takes the Helm Go event system and extends it on into Lua. The
// library handles the bi-directional transformation of Lua and Go objects.
//
// A major design goal of this implementation is that it will be able to interoperate
// with handlers registered directly in Go and (in the future) other languages
// that are added to the events system. To this end, there are a number of "round
// trips" from Lua to Go and back again that otherwise could have been optimized
// out.
package lua

@ -0,0 +1,218 @@
package lua
import (
"github.com/yuin/gopher-lua"
"k8s.io/helm/pkg/events"
"layeh.com/gopher-luar"
"gopkg.in/yaml.v2"
)
// Emitter is a Lua-specific wrapper around an event.Emitter
//
// This wrapper servces two purposes.
//
// First, it exposes the events API to Lua code. An 'events' module is created,
// and methods are attached to it.
//
// Second, it transforms Lua events back into Go datastructures.
//
// When New() creates a new Emitter, it registers the events library within the
// given VM. This means that you should not try to create multiple emitters for
// a single Lua VM.
//
// Inside of the Lua VM, scripts can call `events.on`:
//
// -- This is Lua code
// local events = require("events")
// events.on("some_event", some_callback)
//
// When an event is triggered on the paremt events.Emitter, this Emitter will
// cause that event to trigger within the Lua VM.
//
// Note that in this model, Go events, Lua events, and events from other systems
// that implement this system can all interoperate with each other. However, the
// only shared pieces of context are (appropriately) the events.Context object
// and whatever Emitter methods are exposed.
type Emitter struct {
parent *events.Emitter
vm *lua.LState
}
// Bind links the vm and the events.Emitter via a lua.Emitter that is not returned.
func Bind(vm *lua.LState, emitter *events.Emitter) {
New(vm, emitter)
}
// New creates a new *lua.Emitter
//
// It registers the 'events' module into the Lua engine as well. If you do
// not want the events module registered (e.g. if you have done so already, or
// are doing it manually), you may construct a raw Emitter.
func New(vm *lua.LState, emitter *events.Emitter) *Emitter {
lee := &Emitter{
parent: emitter,
vm: vm,
}
// Register handlers inside the VM
lee.register()
return lee
}
// register creates a Lua module named "events" and registers the event handlers.
func (l *Emitter) register() {
exports := map[string]lua.LGFunction{
"on": l.On,
}
l.vm.PreloadModule("events", func(vm *lua.LState) int {
module := vm.SetFuncs(vm.NewTable(), exports)
vm.Push(module)
return 1
})
// YAML module
yaml := map[string]lua.LGFunction{
"encode": func(vm *lua.LState) int {
input := vm.CheckTable(1)
data, err := yaml.Marshal(tableToMap(input))
if err != nil {
panic(err)
}
vm.Push(lua.LString(string(data)))
return 1
},
"decode": func(vm *lua.LState) int {
data := vm.CheckString(1)
dest := map[string]interface{}{}
if err := yaml.Unmarshal([]byte(data), dest); err != nil {
panic(err)
}
return 0
},
}
l.vm.PreloadModule("yaml", func(vm *lua.LState) int {
module := vm.SetFuncs(vm.NewTable(), yaml)
vm.Push(module)
return 1
})
}
// On is the main event handler registration function.
//
// The given LState must be a function call with (eventName, callbackFunction) as
// the signature.
func (l *Emitter) On(vm *lua.LState) int {
eventName := vm.CheckString(1)
fn := vm.CheckFunction(2)
l.onWrapper(eventName, fn)
return 1
}
// onWrapper registers an event handler with the parent events.Emitter.
//
// A wrapper function is applied to match Lua's types to the expected Go types.
// Likewise the events.Context object is exposed in Lua as a UserData object.
func (l *Emitter) onWrapper(event string, fn *lua.LFunction) {
l.parent.On(event, func(ctx *events.Context) error {
println("called wrapper for", event)
lctx := luar.New(l.vm, ctx)
/*
luaCtx := l.vm.NewUserData()
luaCtx.Value = ctx
l.vm.SetMetatable(luaCtx, l.vm.GetTypeMetatable("context"))
*/
// I think we may need to trap a panic here.
return l.vm.CallByParam(lua.P{
Fn: fn,
NRet: 1,
Protect: true,
}, lctx)
})
}
func tableToMap(table *lua.LTable) map[string]interface{} {
res := map[string]interface{}{}
table.ForEach(func(k, v lua.LValue) {
key := ""
switch k.Type() {
case lua.LTString, lua.LTNumber:
key = k.String()
default:
panic("cannot convert table key to string")
}
switch raw := v.(type) {
case lua.LString:
res[key] = string(raw)
case lua.LBool:
res[key] = bool(raw)
case lua.LNumber:
res[key] = float64(raw)
case *lua.LUserData:
res[key] = raw.Value
case *lua.LTable:
// Test whether slice or map
intkeys := true
raw.ForEach(func(k, v lua.LValue) {
if k.Type() != lua.LTNumber {
intkeys = false
}
})
if intkeys {
res[key] = tableToSlice(raw)
return
}
res[key] = tableToMap(raw)
default:
if raw == lua.LNil {
res[key] = nil
return
}
panic("unknown")
}
})
return res
}
func tableToSlice(numberTable *lua.LTable) []interface{} {
slice := []interface{}{}
numberTable.ForEach(func(k, v lua.LValue) {
switch raw := v.(type) {
case lua.LString:
slice = append(slice, string(raw))
case lua.LBool:
slice = append(slice, bool(raw))
case lua.LNumber:
slice = append(slice, float64(raw))
case *lua.LUserData:
slice = append(slice, raw.Value)
case *lua.LTable:
// Test whether slice or map
intkeys := true
raw.ForEach(func(k, v lua.LValue) {
if k.Type() != lua.LTNumber {
intkeys = false
}
})
if intkeys {
slice = append(slice, tableToSlice(raw))
return
}
slice = append(slice, tableToMap(raw))
default:
if raw == lua.LNil {
slice = append(slice, nil)
return
}
panic("unknown")
}
})
return slice
}

@ -0,0 +1,109 @@
package lua
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/yuin/gopher-lua"
"k8s.io/helm/pkg/events"
)
func luaFunc(l *lua.LState) int {
println("PING")
return 0
}
var mockCtx = &events.Context{}
func TestEmitter_onWrapper(t *testing.T) {
emitter := events.New()
vm := lua.NewState()
defer vm.Close()
lee := &Emitter{
parent: emitter,
vm: vm,
}
lee.onWrapper("ping", vm.NewFunction(luaFunc))
assert.Nil(t, emitter.Emit("ping", mockCtx), "Expect no errors")
}
const onScript = `
local events = require("events")
events.on("ping", function(c) return "pong" end)
events.on("ping", function(c) return "zoinks" end)
events.on("other thing", function(c) return "hi" end)
`
func TestEmitter_On(t *testing.T) {
emitter := events.New()
vm := lua.NewState()
defer vm.Close()
lee := New(vm, emitter)
assert.Nil(t, vm.DoString(onScript), "load script")
// Emit an event in the Go events system, and expect it to propagate through
// the Lua system
lee.parent.Emit("ping", mockCtx)
// This is a trivial check. We just make sure that the top of the stack is
// the return value from calling "ping" event
result := vm.CheckString(1)
assert.Equal(t, "pong", result, "expect ping event to return pong")
println(vm.CheckString(2))
}
const luaTableScript = `
example = {
key = "value",
number = 123,
boolean = true,
nada = nil,
inner = {
name = "Matt"
},
list = { "one", "two" },
}
`
func TestTableToMap(t *testing.T) {
vm := lua.NewState()
vm.DoString(luaTableScript)
table := vm.GetGlobal("example")
res := tableToMap(table.(*lua.LTable))
if res["key"] != "value" {
t.Error("Expected key to be value")
}
if res["number"] != float64(123) {
t.Errorf("Expected number to be a float64 123, got %+v", res["number"])
}
if res["boolean"] != true {
t.Errorf("Expected bool true")
}
if res["nada"] != nil {
t.Error("Expected nada to be nil")
}
if res["inner"].(map[string]interface{})["name"] != "Matt" {
t.Error("Expected inner.name to be Matt")
}
list := []string{"one", "two"}
for i, val := range res["list"].([]interface{}) {
if val != list[i] {
t.Error("Expected list item to be", list[i])
}
}
t.Logf("res: %+v", res)
}

@ -0,0 +1,36 @@
package lua
import (
"fmt"
"path/filepath"
"github.com/yuin/gopher-lua"
hapi "k8s.io/helm/pkg/hapi/chart"
)
// LoadScripts is a depth-first script loader for Lua scripts
//
// It walks the chart and all dependencies, loading the ext/lua/chart.lua
// script for each chart.
//
// If a script fails to load, loading immediately ceases and the error is returned.
func LoadScripts(vm *lua.LState, chart *hapi.Chart) error {
// We go depth first so that the top level chart gets the final word.
// That is, the top level chart should be able to modify objects that the
// child charts set.
for _, child := range chart.Dependencies {
LoadScripts(vm, child)
}
// For now, we only load a `chart.lua`, since that is how other Lua impls
// do it (e.g. single entrypoint, not multi).
for _, script := range chart.Ext {
target := filepath.Join("ext", "lua", "chart.lua")
if script.Name == target {
if err := vm.DoString(string(script.Data)); err != nil {
return fmt.Errorf("failed to execute Lua for %s: %s", chart.Metadata.Name, err)
}
}
}
return nil
}

@ -0,0 +1,69 @@
package lua
import (
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/yuin/gopher-lua"
hapi "k8s.io/helm/pkg/hapi/chart"
)
func TestLoadScripts(t *testing.T) {
chart := &hapi.Chart{
Metadata: &hapi.Metadata{
Name: "starboard",
},
Ext: []*hapi.File{
{
Name: filepath.Join("ext", "lua", "chart.lua"),
Data: []byte(`hello="world"; name="Ishmael"`),
},
{
Name: filepath.Join("ext", "lua", "decoy.lua"),
Data: []byte(`hello="goodbye"`),
},
},
}
outterChart := &hapi.Chart{
Metadata: &hapi.Metadata{
Name: "port",
},
Ext: []*hapi.File{
{
Name: filepath.Join("ext", "lua", "chart.lua"),
Data: []byte(`hello="Nantucket"; goodbye ="Spouter"`),
},
{
Name: filepath.Join("ext", "lua", "decoy.lua"),
Data: []byte(`hello="goodbye"`),
},
},
Dependencies: []*hapi.Chart{chart},
}
// Simple test on a single chart
vm := lua.NewState()
LoadScripts(vm, chart)
world := vm.GetGlobal("hello").String()
assert.Equal(t, world, "world", `expected hello="world"`)
// Test on a nested chart
vm = lua.NewState()
LoadScripts(vm, outterChart)
// This should override the child chart's value
result := vm.GetGlobal("hello").String()
assert.Equal(t, result, "Nantucket", `expected hello="Nantucket"`)
// This should be unchanged
result = vm.GetGlobal("goodbye").String()
assert.Equal(t, result, "Spouter", `expected goodbye="Spouter"`)
// This should come from the child chart
result = vm.GetGlobal("name").String()
assert.Equal(t, result, "Ishmael", `expected name="Ishmael"`)
}

@ -29,4 +29,6 @@ type Chart struct {
// Files are miscellaneous files in a chart archive,
// e.g. README, LICENSE, etc.
Files []*File `json:"files,omitempty"`
// Ext jas extension scripts.
Ext []*File `json:"ext,omitempty"`
}

Loading…
Cancel
Save