Write and build a custom plugin¶
HOG has no runtime plugin loading — a plugin is a Go package whose init()
registers a module against a compile-time registry, and hog-build composes
it into one static binary alongside the built-ins. This example writes a
minimal terminal-handler plugin, declares it in the Gateway.plugins
manifest, and builds the binary with hog-build — no Go build pipeline of
your own required.
Prerequisites
Docker and a local clone of the HOG repository, to build the
hog-builder/hog-runtime base images (not published to a registry
yet — same caveat as the quick start).
1. Write the plugin¶
A terminal-handler plugin registers a factory under config.KindTerminalHandler
with hog.Register. The factory's signature is fixed:
func(instanceName string, cfg registry.RawConfig) (any, error), returning
anything satisfying http.Handler. Save this as plugin/plugin.go:
// Package helloplugin registers a minimal `hello` terminal handler.
package helloplugin
import (
"fmt"
"net/http"
"github.com/paulopiriquito/hog/v2"
"github.com/paulopiriquito/hog/v2/config"
"github.com/paulopiriquito/hog/v2/registry"
)
// helloConfig is this handler's own `spec.handler` fields.
type helloConfig struct {
Greeting string `yaml:"greeting"`
}
func init() {
hog.Register(config.KindTerminalHandler, "hello", func(name string, cfg registry.RawConfig) (any, error) {
var hc helloConfig
if err := cfg.Decode(&hc); err != nil {
return nil, fmt.Errorf("hello %q: %w", name, err)
}
if hc.Greeting == "" {
hc.Greeting = "hello"
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "%s, %s\n", hc.Greeting, r.URL.Query().Get("name"))
}), nil
})
}
cfg.Decode unmarshals the route's handler: mapping — including the
type key, which your struct can safely ignore — into helloConfig. See
writing plugins for the full contract
(module kinds, config decoding, testing your factory in isolation).
2. Declare it in the config¶
Gateway.plugins is both documentation and the build manifest — hog-build
reads this same file to know what to import:
kind: Gateway
metadata: { name: hog }
spec:
listen: ":8080"
plugins:
- github.com/acme/hog-hello@v1.0.0
---
kind: Route
metadata: { name: hello }
spec:
match: /hello
handler:
type: hello
greeting: "hi there"
access: { auth: public }
Each entry is <import-path>[@version] — the package whose init() calls
hog.Register. While you haven't published hog-hello yet, keep the entry
unversioned and override it with --replace (below); a pinned @version is
what you'd use once it's a real, tagged module.
3. Build the base images once¶
docker build -f build/Dockerfile.builder -t hog-builder .
docker build -f build/Dockerfile.runtime -t hog-runtime .
4. Compose the custom binary¶
A two-stage Dockerfile: hog-builder composes the binary, hog-runtime
serves it. --replace swaps the unpublished github.com/acme/hog-hello
import for your local plugin/ directory:
# syntax=docker/dockerfile:1
FROM hog-builder AS build
COPY plugin/ ./plugin/
COPY gateway.yaml .
RUN hog-build --config gateway.yaml -o /out/hog \
--replace github.com/acme/hog-hello=./plugin
FROM hog-runtime
COPY --from=build /out/hog /usr/local/bin/hog
COPY --chown=hog:hog gateway.yaml /etc/hog/gateway.yaml
hog-runtime already creates an empty, hog-owned /srv/web — this
example has nothing to serve from it, since its only route is /hello. Add
a COPY --chown=hog:hog dist/ /srv/web/ line here if your plugin's config
also serves static content.
hog-build reads the manifest, generates a throwaway main.go that
blank-imports every listed plugin next to hog.Main(), and runs go build
pinned against the hog source the hog-builder image already carries
(HOG_SOURCE=/src, baked into that image at docker build -f
Dockerfile.builder time) — so the composed binary always contains this
hog, never whatever go get would otherwise resolve.
docker build -t my-hog .
docker run --read-only --tmpfs /tmp -p 8080:8080 my-hog
curl "http://localhost:8080/hello?name=Ada"
Result
hi there, Ada — your plugin's handler, compiled into the same static
binary as every built-in terminal.
Local iteration without Docker¶
Outside a container, hog-build is a plain CLI: build it once from a clone
of the hog repo, then point --hog-source at that checkout (the
hog-builder image sets this for you via $HOG_SOURCE; bare CLI use needs
it explicitly):
cd /path/to/hog && go build -o hog-build ./cmd/hog-build
cd /path/to/your/plugin-project
/path/to/hog/hog-build --config gateway.yaml -o ./hog \
--hog-source /path/to/hog \
--replace github.com/acme/hog-hello=./plugin
./hog --config gateway.yaml
--keep retains the generated main.go/go.mod in a temp dir for
inspection after a failed build; --tags/--go pass through build tags and
a specific Go toolchain, respectively.
Next steps¶
- The full plugin contract — module kinds,
RawConfig, middleware plugins, testing: writing plugins. - Every
hog-buildflag and the image family in depth: building a custom binary. - Skip
hog-buildentirely and own thego buildyourself: framework mode, documented in framework mode.