A Survey of Golang 'plugins' in 2020
For the purpose of this blog post, my definition of plugin is:
A method of extending the functionality of program without forking it
In particular, I’ll be looking at methods of extending Golang (go) programs.
Official Golang Plugins
Official Golang Plugins are a native way of extending go codebases without forking them.
How They Work
Official Golang Plugins work by compiling your plugin in a special way:
go build -buildmode=plugin
This build mode can output a .so
file with symbols that a “main” program can load dynamically. A quick example would look like with your plugin code:
package main
import "fmt"
var PluginInputNumber int
func KnownPluginFunction() { fmt.Printf("My custom code prints PluginInputNumber: %d\n", PluginInputNumber) }
And then the “main” code would load plugins and operate on known functions:
p, _ := plugin.Open("myplugin.so")
in, _ := p.Lookup("PluginInputNumber")
f, _ := p.Lookup("KnownPluginFunction")
*in.(*int) = 7
f.(func())() // prints "My custom code prints PluginInputNumber: 7"
Pros
- In the golang stdlib. Simple and sparse.
- High performance, it is just calling functions in a library using native interfaces
Cons
- Plugins can crash the main loop
- Linux, FreeBSD, and macOS only
- Plugins and the main program must be built in the exact same environment. (same golang version, mod dependencies, go path, cgo)
Examples
Hashicorp go-plugin
Hashicorp go-plugin is the method that Hashicorp uses in many of its products to support extending them.
How They Work
Hashicorp go-plugin works by having the main process spawn the plugin as a separate process and communicate with it over gRPC (via http2, or a few other options). This communication usually happens over a unix socket or localhost. This is a more full-featured version of pie.
Pros
- Writing plugins feels natural because you are talking to a golang interface
- Cross-platform and cross-language (it is “just” gRPC over a socket)
- Logging, stdout/err, and even TTYs are transparently preserved!
- Plugins can be versioned to help support backwards-compatible changes
- Plugins cannot crash the main program and can even be hot-reloaded
Cons
- Lower performance than dynamic library function calls (native golang plugins)
Examples
- Kubernetes Auth Vault Plugin
- Any of the Terraform first-party providers or third-party providers
- (add more?)
Kubernetes-Scheduler-Style Plugins
The Kubernetes Scheduler has a framework for writing your own plugins to the Kubernetes scheduler (but not to any other Kubernetes component at the time of this writing).
How They Work
These k8s plugins work by wrapping the main kubernetes scheduler binary with a special invocation:
func main() {
command := app.NewSchedulerCommand(
app.WithPlugin(coscheduling.Name, coscheduling.New),
app.WithPlugin(qos.Name, qos.New),
)
if err := command.Execute(); err != nil {
os.Exit(1)
}
}
The resulting binary is a mono-binary of both your and the k8s code. It is kinda like the k8s code will “inherit” the functions you overwrite:
// Less is the function used by the activeQ heap algorithm to sort pods.
// It sorts pods based on their priorities. When the priorities are equal, it uses
// the Pod QoS classes to break the tie.
func (*Sort) Less(pInfo1, pInfo2 *framework.PodInfo) bool {
p1 := pod.GetPodPriority(pInfo1.Pod)
p2 := pod.GetPodPriority(pInfo2.Pod)
return (p1 > p2) || (p1 == p2 && compQOS(pInfo1.Pod, pInfo2.Pod))
}
It is kinda a stretch to call these plugins, but it does allow you to extend Kubernetes without actually forking the codebase.
Pros
- Simpler interface.
- No real magic, just normal go code
Cons
- Nothing fancy, you have to compile the main program and your plugin together as a single binary.
- You must compile the whole binary yourself, you cannot use a pre-packaged binary and connect to it.