Updated v2: Writing a Module (markdown)

Matt Holt 2019-08-21 12:29:50 -06:00
parent 9e59dcdc3b
commit 14fdbecfac

@ -23,18 +23,37 @@ If you are writing a Caddy 2 module, please let us know in our [forum](https://c
Caddy extensions are called "modules" (distinct from Go modules), or "plugins".
Modules register themselves with Caddy when they are imported:
Module packages register their modules with Caddy when they are imported:
```go
package mymodule
import "github.com/caddyserver/caddy/v2"
func init() {
caddy.RegisterModule(caddy.Module{
Name: "foo.bar.gizmo",
New: func() interface{} { return new(Gizmo) },
})
// give Caddy a plain/empty instance of your module
// so it can call CaddyModule() and get its info
err := caddy.RegisterModule(Gizmo{})
if err != nil {
log.Fatal(err)
}
}
```
// ...
// CaddyModule returns the Caddy module information.
func (Gizmo) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
// name must be properly namespaced ("foo.bar")
Name: "foo.bar.gizmo",
// the returned value must satisfy an interface
// expected for modules in the "foo.bar" namespace;
// and the returned value MUST be a pointer
New: func() caddy.Module { return new(Gizmo) },
}
}
```
A module can then be "plugged in" by adding an import to Caddy's `main` package:
@ -46,27 +65,13 @@ import _ "github.com/example/mymodule"
# Module Requirements
Modules work successfully when they:
Caddy modules:
1. Have a unique name in the proper namespace
2. Satisfy the host module's defined interface for that namespace
1. Implement the `caddy.Module` interface to provide a name and constructor
2. Have a unique name in the proper namespace
3. Satisfy the host module's defined interface for that namespace
You can see how registering a module correlates to these two requirements:
```go
caddy.RegisterModule(caddy.Module{
// name must be properly namespaced ("foo.bar")
Name: "foo.bar.gizmo",
// the returned value must satisfy an interface
// expected for modules in the "foo.bar" namespace;
// if the value is not a pointer, it will be
// converted to one using reflection
New: func() interface{} { return new(Gizmo) },
})
```
We will show you how to satisfy these requirements in the next sections. (It's really quite easy!)
We will show you how to satisfy these requirements in the next sections. It's really quite easy!
# Module Names
@ -100,7 +105,7 @@ You must properly namespace your module in order for it to be recognized by the
For example, if you were to write an HTTP handler module called `gizmo`, your module's name would be `http.handlers.gizmo`, because the `http` app will look for handlers in the `http.handlers` namespace.
The `caddy` namespace is reserved for modules specific to Caddy's core configuration.
The `caddy` and `admin` namespaces are reserved.
To write modules which plug into 3rd-party host modules, consult those modules for their namespace documentation.
@ -123,6 +128,22 @@ This will ensure that config properties are consisently named and that users can
You can expect that instances of your module are filled out with any configuration they were loaded with. It is also possible to perform additional [provisioning](#provisioning) and [verification](#validating) steps when your module is loaded.
Modules which act as host modules will need a way to decode guest modules into their structure. This is typically done using the `json.RawMessage` type and then using the Provision method to load them. For example, if your module "Gizmo" has a "Gadget" guest module, you would add two fields:
```go
type Gizmo struct {
MyField string `json:"my_field,omitempty"`
Number int `json:"number,omitempty"`
GadgetRaw json.RawMessage `json:"gadget,omitempty"`
// decoded value would be stored here, but it doesn't
// get used for JSON config directly so we exclude it
Gadget Gadget `json:"-"`
}
```
The configuration for Gadget would come in on the GadgetRaw field and then your Provision method would be used to decode the config and load the instance of the Gadget.
## Table of Namespaces and Interfaces
| Kind of module | Namespace | Interface |
@ -149,7 +170,7 @@ When your module is loaded by a host module, the following happens:
5. At this point, the module value will probably be type-asserted by the host module into the interface it is expected to implement so that it can be used. However, this can vary per host module. It could be that some modules may optionally satisfy interfaces. Check the host module's documentation to know for sure.
6. When a module is no longer needed, if it is a `caddy.CleanerUpper`, the `Cleanup()` method is called.
Note that multiple, loaded instances of your module may be overlap. During config reloads, modules are started before the old ones are stopped. Be sure to use global state carefully.
Note that multiple, loaded instances of your module may overlap. During config reloads, modules are started before the old ones are stopped. Be sure to use global state carefully.
## Provisioning
@ -204,7 +225,6 @@ For example, an HTTP handler such as the static file server might satisfy multip
var (
_ caddy.Provisioner = (*FileServer)(nil)
_ caddyhttp.MiddlewareHandler = (*FileServer)(nil)
_ httpcaddyfile.HandlerDirective = (*FileServer)(nil)
)
```
@ -217,7 +237,7 @@ Without interface guards, confusing bugs can slip in. For example, if your modul
Let's suppose we want to write an HTTP handler module. This will be a contrived middleware for demonstration purposes which prints the visitor's IP address to a stream on every HTTP request.
We also want it to be configurable via the Caddyfile, because most people prefer to use the Caddyfile in non-automated situations. The two methods satisfying `httpcaddyfile.HandlerDirective` allow this module to be used with simple configuration in the Caddyfile, for example: `visitor_ip stdout`.
We also want it to be configurable via the Caddyfile, because most people prefer to use the Caddyfile in non-automated situations. We do this by registering a Caddyfile handler directive, which is a kind of directive that can add a handler to the HTTP route. We also implement `caddyfile.Unmarshaler` since it's a good convention, but not strictly required in this case (we could just parse the Caddyfile tokens directly in `parseCaddyfile` if we wanted). By adding these few lines of code, this module can be configured with the Caddyfile! For example: `visitor_ip stdout`.
Here is the code for such a module, with explanatory comments:
@ -237,10 +257,8 @@ import (
)
func init() {
caddy.RegisterModule(caddy.Module{
Name: "http.handlers.visitor_ip",
New: func() interface{} { return new(Middleware) },
})
caddy.RegisterModule(Middleware{})
httpcaddyfile.RegisterHandlerDirective("visitor_ip", parseCaddyfile)
}
// Middleware implements an HTTP handler that writes the
@ -253,6 +271,14 @@ type Middleware struct {
w io.Writer
}
// CaddyModule returns the Caddy module information.
func (Middleware) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
Name: "http.handlers.visitor_ip",
New: func() caddy.Module { return new(Middleware) },
}
}
// Provision implements caddy.Provisioner.
func (m *Middleware) Provision(ctx caddy.Context) error {
switch m.Output {
@ -281,6 +307,9 @@ func (m Middleware) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddy
}
// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
// NOTE: This is not strictly required for most modules, but is
// an elegant pattern, and some modules (like HTTP matchers or
// encodings) do require this, so we follow that convention.
func (m *Middleware) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
if !d.Args(&m.Output) {
return fmt.Errorf("an output stream is required: stdout or stderr")
@ -288,14 +317,18 @@ func (m *Middleware) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
return nil
}
// Bucket returns the HTTP directive bucket for this handler.
func (m Middleware) Bucket() int { return 1 }
// parseCaddyfile unmarshals tokens from h into a new Middleware.
func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
var m Middleware
err := m.UnmarshalCaddyfile(h.Dispenser)
return m, err
}
// Interface guards
var (
_ caddy.Provisioner = (*Middleware)(nil)
_ caddy.Validator = (*Middleware)(nil)
_ caddyhttp.MiddlewareHandler = (*Middleware)(nil)
_ httpcaddyfile.HandlerDirective = (*Middleware)(nil)
_ caddyfile.Unmarshaler = (*Middleware)(nil)
)
```