Skip to content

Commit

Permalink
Add api.Plugin as a base type for guest plugins
Browse files Browse the repository at this point in the history
This adds `api.Plugin` as a base type for guest plugins. This is similar
to `framework.Plugin`, but doesn't yet define `Name()` as there's no
purpose for it. Notes on that are added to RATIONALE.md.

This mostly gets the guest SDK looking much more like the framework one.
This also makes it easier to test for misconfiguration, such as
registering multiple instances of plugins.

Signed-off-by: Adrian Cole <adrian@tetrate.io>
  • Loading branch information
Adrian Cole committed Jul 12, 2023
1 parent 73c8b13 commit d39d13c
Show file tree
Hide file tree
Showing 34 changed files with 356 additions and 238 deletions.
15 changes: 15 additions & 0 deletions RATIONALE.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,21 @@ noted below, and also there are options for mitigation not yet implemented:
* [polyglot][3] can generate code from protos and might automatically convert
protos to its more efficient representation.

## Why doesn't the ABI to set the plugin name?

Framework plugins all share a base type `Plugin` with only one method: `Name`.
This is conventionally set to the same constant used to register the plugin
factory `app.WithPlugin`. Effectively, this is static configuration because
there is no configuration you can read prior to invoking this.

Until this changes, there's no reason to define an ABI for the name of a
wasm plugin. Even if we could read it from the guest, the plugin name would
have already been associated with the factory. This has some problems until
something changes:

* There can only be one wasm based plugin defined at a time.
* Wasm plugins with significantly different behavior will use the same metrics.

## How is `framework.CycleState` implemented in WebAssembly?

`framework.CycleState` is a primarily a key value storage. Plugins store values
Expand Down
8 changes: 5 additions & 3 deletions examples/filter-simple/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,13 @@ import (
)

func main() {
filter.SetPlugin(api.FilterFunc(nameEqualsPodSpec))
filter.SetPlugin(nameEqualsPodSpec{})
}

// nameEqualsPodSpec schedules this node if its name equals its pod spec.
func nameEqualsPodSpec(_ api.CycleState, pod api.Pod, nodeInfo api.NodeInfo) *api.Status {
// nameEqualsPodSpec schedules a node if its name equals its pod spec.
type nameEqualsPodSpec struct{}

func (nameEqualsPodSpec) Filter(_ api.CycleState, pod api.Pod, nodeInfo api.NodeInfo) *api.Status {
// First, check if the pod spec node name is empty. If so, pass!
podSpecNodeName := nilToEmpty(pod.Spec().NodeName)
if len(podSpecNodeName) == 0 {
Expand Down
Binary file modified examples/filter-simple/main.wasm
Binary file not shown.
8 changes: 5 additions & 3 deletions examples/prefilter-simple/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,13 @@ import (
)

func main() {
prefilter.SetPlugin(api.PreFilterFunc(podSpecName))
prefilter.SetPlugin(podSpecName{})
}

// podSpecName returns the pod spec name, unless there is none.
func podSpecName(_ api.CycleState, pod api.Pod) ([]string, *api.Status) {
// podSpecName schedules a node if its name equals its pod spec.
type podSpecName struct{}

func (podSpecName) PreFilter(_ api.CycleState, pod api.Pod) ([]string, *api.Status) {
// First, check if the pod spec node name is empty. If so, pass!
podSpecNodeName := nilToEmpty(pod.Spec().NodeName)
if len(podSpecNodeName) == 0 {
Expand Down
Binary file modified examples/prefilter-simple/main.wasm
Binary file not shown.
8 changes: 5 additions & 3 deletions examples/score-simple/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,13 @@ import (
)

func main() {
score.SetPlugin(api.ScoreFunc(score100IfNameEqualsPodSpec))
score.SetPlugin(score100IfNameEqualsPodSpec{})
}

// score100IfNameEqualsPodSpec scores 100 if this node name equals its pod spec.
func score100IfNameEqualsPodSpec(_ api.CycleState, pod api.Pod, nodeName string) (int32, *api.Status) {
// score100IfNameEqualsPodSpec returns 100 if a node name equals its pod spec.
type score100IfNameEqualsPodSpec struct{}

func (score100IfNameEqualsPodSpec) Score(_ api.CycleState, pod api.Pod, nodeName string) (int32, *api.Status) {
podSpecNodeName := nilToEmpty(pod.Spec().NodeName)
if nodeName == podSpecNodeName {
return 100, nil
Expand Down
Binary file modified examples/score-simple/main.wasm
Binary file not shown.
41 changes: 11 additions & 30 deletions guest/api/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ type CycleState interface {
Delete(key string)
}

// Plugin is a WebAssembly implementation of framework.Plugin.
type Plugin interface {
// This doesn't define `Name() string`. See /RATIONALE.md for impact
}

// PreFilterPlugin is a WebAssembly implementation of
// framework.PreFilterPlugin. When non-nil, the `nodeNames` result contains a
// unique set of node names to process.
Expand All @@ -49,49 +54,25 @@ type CycleState interface {
// global variables.
// - Duplicate nodeNames are a bug, but will not cause a failure.
type PreFilterPlugin interface {
PreFilter(state CycleState, pod Pod) (nodeNames []string, status *Status)
}

var _ PreFilterPlugin = PreFilterFunc(nil)
Plugin

// PreFilterFunc adapts an ordinary function to a PreFilterPlugin.
type PreFilterFunc func(state CycleState, pod Pod) (nodeNames []string, status *Status)

// PreFilter returns f(state, pod).
func (f PreFilterFunc) PreFilter(state CycleState, pod Pod) (nodeNames []string, status *Status) {
return f(state, pod)
PreFilter(state CycleState, pod Pod) (nodeNames []string, status *Status)
}

// FilterPlugin is a WebAssembly implementation of framework.FilterPlugin.
type FilterPlugin interface {
Filter(state CycleState, pod Pod, nodeInfo NodeInfo) *Status
}
Plugin

var _ FilterPlugin = FilterFunc(nil)

// FilterFunc adapts an ordinary function to a FilterPlugin.
type FilterFunc func(state CycleState, pod Pod, nodeInfo NodeInfo) *Status

// Filter returns f(state, pod, nodeInfo).
func (f FilterFunc) Filter(state CycleState, pod Pod, nodeInfo NodeInfo) *Status {
return f(state, pod, nodeInfo)
Filter(state CycleState, pod Pod, nodeInfo NodeInfo) *Status
}

// ScorePlugin is a WebAssembly implementation of framework.ScorePlugin.
//
// Note: This is int32, not int64. See /RATIONALE.md for why.
type ScorePlugin interface {
Score(state CycleState, pod Pod, nodeName string) (int32, *Status)
}

var _ ScorePlugin = ScoreFunc(nil)
Plugin

// ScoreFunc adapts an ordinary function to a ScorePlugin.
type ScoreFunc func(state CycleState, pod Pod, nodeName string) (int32, *Status)

// Score returns f(pod, nodeName).
func (f ScoreFunc) Score(state CycleState, pod Pod, nodeName string) (int32, *Status) {
return f(state, pod, nodeName)
Score(state CycleState, pod Pod, nodeName string) (int32, *Status)
}

type NodeInfo interface {
Expand Down
65 changes: 62 additions & 3 deletions guest/filter/filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,77 @@
// package when setting Plugin, as doing otherwise will cause overhead.
package filter

import "sigs.k8s.io/kube-scheduler-wasm-extension/guest/api"
import (
"sigs.k8s.io/kube-scheduler-wasm-extension/guest/api"
"sigs.k8s.io/kube-scheduler-wasm-extension/guest/internal/cyclestate"
"sigs.k8s.io/kube-scheduler-wasm-extension/guest/internal/imports"
"sigs.k8s.io/kube-scheduler-wasm-extension/guest/internal/plugin"
protoapi "sigs.k8s.io/kube-scheduler-wasm-extension/kubernetes/proto/api"
)

// filter is the current plugin assigned with SetPlugin.
var filter api.FilterPlugin

// SetPlugin should be called in `main` to assign an api.FilterPlugin instance.
//
// For example:
//
// func main() {
// filter.SetPlugin(nameEqualsPodSpec)
// filter.SetPlugin(nameEqualsPodSpec{})
// }
//
// type nameEqualsPodSpec struct{}
//
// func (nameEqualsPodSpec) Filter(state api.CycleState, pod api.Pod, nodeInfo api.NodeInfo) (status *api.Status) {
// panic("implement me")
// }
func SetPlugin(filterPlugin api.FilterPlugin) {
if filterPlugin == nil {
panic("nil filterPlugin")
}
plugin = filterPlugin
filter = filterPlugin
plugin.MustSet(filter)
}

// prevent unused lint errors (lint is run with normal go).
var _ func() uint32 = _filter

// filter is only exported to the host.
//
//export filter
func _filter() uint32 { //nolint
if filter == nil {
// If we got here, someone imported the package, but forgot to set the
// filter. Panic with what's wrong.
panic("filter imported, but filter.SetPlugin not called")
}

s := filter.Filter(cyclestate.Values, cyclestate.Pod, &nodeInfo{})

return imports.StatusToCode(s)
}

var _ api.NodeInfo = (*nodeInfo)(nil)

// nodeInfo is lazy so that a plugin which doesn't read fields avoids a
// relatively expensive unmarshal penalty.
type nodeInfo struct {
n *protoapi.Node
}

func (n *nodeInfo) Node() *protoapi.Node {
return n.node()
}

func (n *nodeInfo) node() *protoapi.Node {
if node := n.n; node != nil {
return node
}

var msg protoapi.Node
if err := imports.NodeInfoNode(msg.UnmarshalVT); err != nil {
panic(err)
}
n.n = &msg
return n.n
}
69 changes: 0 additions & 69 deletions guest/filter/filter_exports.go

This file was deleted.

42 changes: 42 additions & 0 deletions guest/internal/plugin/plugin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
Copyright 2023 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

// Package plugin includes utilities needed for any api.Plugin.
package plugin

import (
"reflect"

"sigs.k8s.io/kube-scheduler-wasm-extension/guest/api"
)

var current api.Plugin

// MustSet sets the plugin once
func MustSet(plugin api.Plugin) {
if !set(plugin) {
panic("only one plugin instance is supported")
}
}

func set(plugin api.Plugin) bool {
if current == nil {
current = plugin
return true
}
// current == plugin with the same value works in Go, but not TinyGo.
return reflect.DeepEqual(current, plugin)
}
Loading

0 comments on commit d39d13c

Please sign in to comment.