Writing Kong plugins with Go
Matheus Mina
Posted on October 4, 2023
The Kong, quoting its own documentation, is an open-source API gateway, cloud-native, platform-agnostic, scalable API Gateway distinguished for its high performance and extensibility via plugins. Kong has a lot of official plugins that allow us to customize what we need, and when there aren't any available, you can build your own.
The default language for building plugins its Lua, but other languages are supported, including Go. I don't have much experience writing code with Lua, so using Go allows me a higher development speed and quality on my plugins.
Development
Building a Go plugin is easy, as you need to follow the signature proposed by Kong's plugin development kit or PDK, but it is easy to understand. To demonstrate that, we will build a plugin that will read a request header and set a new response header.
The first thing to do is define both constants: version
and priority
. As the name says, we will configure the current version of the plugin and its execution priority within the other plugins. Its worth mentioning that the higher the priority, the earlier it will be executed. If you wanna understand more about it, check this post.
var Version = "0.0.1"
var Priority = 1
After, we will define which params the plugin accepts by building a struct
called Config
. Here, we will receive the field message as a string.
type Config struct {
Message string `json:"message"`
}
All kong plugins work based on which phase access they need to run. The possible phase access are:
- Certificate
- Rewrite
- Access
- Response
- Preread
- Log
In this example, we will be using Access
because we need to update the response before forwarding the request to the responsible microservice. You don't need to worry about it because the signatures for all phases are the same. To know more about all phase access, check this documentation.
func (conf Config) Access(kong *pdk.PDK) {
host, err := kong.Request.GetHeader("host")
if err != nil {
log.Printf("Error reading 'host' header: %s", err.Error())
}
message := conf.Message
if message == "" {
message = "hello"
}
kong.Response.SetHeader("x-hello-from-go", fmt.Sprintf("Go says %s to %s", message, host))
}
This function uses the PDK to read the available headers by using kong.Request.GetHeader
, and later we add a response header using kong.Response.SetHeader
. For more information about all available functions, you can check the Go PDK.
At last, let's check the main function:
package main
import (
"fmt"
"log"
"github.com/Kong/go-pdk"
"github.com/Kong/go-pdk/server"
)
func main() {
server.StartServer(New, Version, Priority)
}
func New() interface{} {
return &Config{}
}
As we can notice, the main function initializes the Kong server, sending the Config
, Version
, and Priority
defined before. The full code is:
package main
import (
"fmt"
"log"
"github.com/Kong/go-pdk"
"github.com/Kong/go-pdk/server"
)
var Version = "0.0.1"
var Priority = 1
func main() {
server.StartServer(New, Version, Priority)
}
type Config struct {
Message string `json:"message"`
}
func New() interface{} {
return &Config{}
}
func (conf Config) Access(kong *pdk.PDK) {
host, err := kong.Request.GetHeader("host")
if err != nil {
log.Printf("Error reading 'host' header: %s", err.Error())
}
message := conf.Message
if message == "" {
message = "hello"
}
kong.Response.SetHeader("x-hello-from-go", fmt.Sprintf("Go says %s to %s", message, host))
}
Tests
Testing our Go plugins is also easy once the PDK gives us tools that make it straightforward. Let's test our plugin:
package main
import (
"testing"
"github.com/Kong/go-pdk/test"
"github.com/stretchr/testify/assert"
)
func TestPluginWithoutConfig(t *testing.T) {
env, err := test.New(t, test.Request{
Method: "GET",
Url: "http://example.com?q=search&x=9",
Headers: map[string][]string{"host": {"localhost"}},
})
assert.NoError(t, err)
env.DoHttps(&Config{})
assert.Equal(t, 200, env.ClientRes.Status)
assert.Equal(t, "Go says hello to localhost", env.ClientRes.Headers.Get("x-hello-from-go"))
}
func TestPluginWithConfig(t *testing.T) {
env, err := test.New(t, test.Request{
Method: "GET",
Url: "http://example.com?q=search&x=9",
Headers: map[string][]string{"host": {"localhost"}},
})
assert.NoError(t, err)
env.DoHttps(&Config{Message: "nice to meet you"})
assert.Equal(t, 200, env.ClientRes.Status)
assert.Equal(t, "Go says nice to meet you to localhost", env.ClientRes.Headers.Get("x-hello-from-go"))
}
Deploy
To deploy our plugin, we build an executable and must add it to the Kong image.
# Build Golang plugins
FROM golang:1.20 AS plugin-builder
WORKDIR /builder
COPY ./hello ./go_plugins/hello
RUN find ./go_plugins -maxdepth 1 -mindepth 1 -type d -not -path "*/.git*" | \
while read dir; do \
cd $dir && go build -o /builds/$dir main.go ; \
done
# Build Kong
FROM kong:3.4.0-ubuntu
COPY --from=plugin-builder ./builds/go_plugins/ ./kong/
USER kong
Kong needs some information to be able to find and load the customized plugin, so we need to add some environment variables:
KONG_PLUGINS: "bundled,hello"
KONG_PLUGINSERVER_NAMES: hello
KONG_PLUGINSERVER_HELLO_START_CMD: /kong/hello
KONG_PLUGINSERVER_HELLO_QUERY_CMD: /kong/hello -dump
Considerations about performance
The Kong company already did a study about the performance, and you can find it here.
Conclusion
Go is an excellent tool for writing plugins for Kong. We can have all the performance and tools it provides to us. In my case, I have also been able to speed up my deliveries and have a higher code coverage. It is also possible to isolate the plugin's logic with the PDK use, making it easy to write plugins that work for different technologies, decoupling Kong.
If you wanna check all the code shown in this post, you can access the repo.
You also can find me on Twitter, Github, or LinkedIn.
References
Posted on October 4, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.