docs: expand reference pages and fix children shortcode rendering

Enable Goldmark block-level attributes so the relearn `children`
shortcode applies its CSS classes instead of printing them literally
in the table-of-contents pages. Also expand the deploy, plugins,
records, and email reference docs and add the checks pages.
This commit is contained in:
nemunaire 2026-06-11 13:21:16 +09:00
commit 6b6a8c847f
13 changed files with 1457 additions and 501 deletions

View file

@ -1,311 +1,235 @@
---
title: "Writing a happyDomain Plugin"
description: "Technical guide for developing test plugins for happyDomain"
title: "Writing a happyDomain Checker Plugin"
description: "Technical guide for developing checker plugins for happyDomain"
---
happyDomain supports external **test plugins** — shared libraries (`.so` files) that add domain or service health checks to a running instance. Plugins are loaded at startup without recompiling the server; the operator simply drops a `.so` file into a configured directory.
happyDomain can be extended with external **checker plugins** — shared libraries (`.so` files) that add automated diagnostics on zones, domains, services or users. A checker plugin is loaded into the running happyDomain process at startup; the operator simply drops a `.so` file into a configured directory, no recompilation of the server required.
## How it works
A plugin receives a set of options assembled from several configuration scopes, runs a check (HTTP call, DNS query, …), and returns a result with a status level and an optional detailed report. Results are stored and displayed in the happyDomain UI alongside the domain or service they concern.
When happyDomain starts it scans every directory listed in the `plugins-directories` configuration option. For each file it finds, it:
1. Opens the shared library.
2. Looks up the exported symbol `NewTestPlugin`.
3. Calls `NewTestPlugin()` to obtain a plugin value.
4. Registers the plugin under each name returned by `PluginEnvName()`.
If the file is not a valid Go plugin, if `NewTestPlugin` is missing, or if it returns an error, a warning is logged and the file is skipped. The server always starts regardless of individual plugin load failures.
---
## The `TestPlugin` interface
Every plugin must implement four methods:
```go
type TestPlugin interface {
PluginEnvName() []string
Version() PluginVersionInfo
AvailableOptions() PluginOptionsDocumentation
RunTest(PluginOptions, map[string]string) (*PluginResult, error)
}
```
---
## Project structure
A plugin is a standalone Go module compiled with `-buildmode=plugin`. It must be in `package main` and export exactly one symbol:
```go
func NewTestPlugin() (happydns.TestPlugin, error)
```
Recommended layout:
```
myplugin/
├── go.mod
├── Makefile
└── plugin.go # (or split across multiple .go files)
```
### go.mod
```
module git.happydns.org/happyDomain/plugins/myplugin
go 1.25
require git.happydns.org/happyDomain v0.0.0
replace git.happydns.org/happyDomain => ../../
```
The `replace` directive points to your local happyDomain checkout, ensuring the plugin is compiled against the exact same types as the server.
A checker has two halves: it **collects** raw data about a target (an observation), then **evaluates** that data against a set of rules to produce a status. Results are stored and displayed in the happyDomain UI alongside the domain or service they concern.
{{< notice style="warning" >}}
A Go plugin and the host process share the same runtime. They **must** be compiled with the same Go toolchain version and the same versions of every shared dependency. Any mismatch produces a hard error at load time.
A `.so` plugin is loaded into the happyDomain process and runs with the same privileges as the server. Treat the plugin directory as a trusted location: happyDomain refuses to load plugins from a directory it cannot trust (see [Security and deployment](#security-and-deployment)).
{{< /notice >}}
---
## Entry point
## What a checker plugin must export
Plugins are built against the **`checker-sdk-go`** module, published separately from the happyDomain core. Throughout this page, `checker` refers to the package `git.happydns.org/checker-sdk-go/checker`.
happyDomain's loader looks for a single exported symbol named `NewCheckerPlugin` with this exact signature:
```go
func NewCheckerPlugin() (*checker.CheckerDefinition, checker.ObservationProvider, error)
```
The two return values describe the two halves of a checker:
- **`*CheckerDefinition`** describes the checker: its identifier, name, version, the observation keys it relies on, the options it accepts, its rules, an optional aggregator, a scheduling interval, and whether it exposes HTML reports or metrics. See the [field table](#checkerdefinition-fields) below.
- **`ObservationProvider`** is the data-collection half. It exposes a `Key()` (the observation key the rules look up) and a `Collect(ctx, opts)` method that returns the raw observation payload. happyDomain serialises that result to JSON and caches it per observation context.
- Return a non-nil `error` if the plugin cannot initialise (a missing environment variable, a broken cgo dependency, …). The host logs the error and skips the file rather than aborting startup.
A single `.so` may export several plugin kinds. The loader runs every known plugin loader against every file, then skips any symbol it does not recognise, so one binary can ship more than one plugin.
---
## Minimal example
This is the smallest plugin that loads. It collects a fixed observation and declares no rules. Adapt it from [`checker-dummy`](https://git.happydns.org/checker-dummy), the reference implementation.
```go
// Command plugin is the happyDomain plugin entrypoint for the dummy checker.
//
// Build with:
// go build -buildmode=plugin -o checker-dummy.so ./plugin
package main
import "git.happydns.org/happyDomain/model"
import (
"context"
func NewTestPlugin() (happydns.TestPlugin, error) {
return &MyPlugin{}, nil
"git.happydns.org/checker-sdk-go/checker"
)
type dummyProvider struct{}
func (dummyProvider) Key() checker.ObservationKey { return "dummy.observation" }
func (dummyProvider) Collect(ctx context.Context, opts checker.CheckerOptions) (any, error) {
return map[string]string{"hello": "world"}, nil
}
// NewCheckerPlugin is the symbol resolved by happyDomain at startup.
func NewCheckerPlugin() (*checker.CheckerDefinition, checker.ObservationProvider, error) {
def := &checker.CheckerDefinition{
ID: "com.example.dummy",
Name: "Dummy checker",
Version: "0.1.0",
ObservationKeys: []checker.ObservationKey{"dummy.observation"},
// Add Rules / Aggregator / Options here in a real plugin.
}
return def, dummyProvider{}, nil
}
```
The constructor is a good place to perform one-time initialisation (open config files, create an HTTP client, …). Return an error if the plugin cannot function.
{{< notice style="warning" >}}
A Go plugin and the host process share the same runtime. They **must** be compiled with the same Go toolchain version and the same versions of every shared dependency. Any mismatch produces a hard error at load time. See [Build constraints](#build-constraints).
{{< /notice >}}
---
## Naming — `PluginEnvName()`
## `CheckerDefinition` fields
Returns one or more short, lowercase identifiers. These names are used to look up the plugin via the API and to key its stored configuration.
```go
func (p *MyPlugin) PluginEnvName() []string {
return []string{"myplugin"}
}
```
Choose names that are unlikely to collide (e.g. `"zonemaster"`, `"matrixim"`) and keep them **stable across versions** because they are persisted alongside user configuration. If two loaded plugins claim the same name, the second one is skipped and a conflict is logged.
---
## Version and availability — `Version()`
Describes the plugin and controls where it appears in the UI:
```go
func (p *MyPlugin) Version() happydns.PluginVersionInfo {
return happydns.PluginVersionInfo{
Name: "My Plugin",
Version: "1.0",
AvailableOn: happydns.PluginAvailability{
ApplyToDomain: true,
ApplyToService: false,
LimitToProviders: nil, // nil or empty = all providers
LimitToServices: []string{"abstract.MatrixIM"},
},
}
}
```
The `*CheckerDefinition` returned by `NewCheckerPlugin` is the description of your checker:
| Field | Type | Description |
|---|---|---|
| `ApplyToDomain` | `bool` | Plugin can be run against a whole domain |
| `ApplyToService` | `bool` | Plugin can be run against a specific DNS service |
| `LimitToProviders` | `[]string` | Restrict to certain DNS provider identifiers (empty = no restriction) |
| `LimitToServices` | `[]string` | Restrict to certain service type identifiers, e.g. `"abstract.MatrixIM"` (empty = no restriction) |
| `ID` | `string` | **Required.** Stable, persistent identifier. Pick a namespaced value (`com.example.dnssec-freshness`, not `dnssec`) and never change it: it keys stored results and user configuration. |
| `Name` | `string` | Human-readable name shown in the UI. |
| `Version` | `string` | Plugin version (e.g. `"1.0.0"`). |
| `Availability` | `CheckerAvailability` | Declares which scopes the checker applies to and any provider/service restrictions (see below). |
| `Options` | `CheckerOptionsDocumentation` | Documents the options the checker accepts, grouped by scope (see below). |
| `Rules` | `[]CheckRule` | The rules evaluated against the collected observation. |
| `Aggregator` | `CheckAggregator` | Optional. Combines the per-rule `CheckState`s into a single summary state. |
| `Interval` | `*CheckIntervalSpec` | Optional scheduling bounds (`Min`, `Max`, `Default` durations). |
| `HasHTMLReport` | `bool` | Set when the provider implements `CheckerHTMLReporter`. |
| `HasMetrics` | `bool` | Set when the provider implements `CheckerMetricsReporter`. |
| `ObservationKeys` | `[]ObservationKey` | The observation keys this checker reads. |
Both `ApplyToDomain` and `ApplyToService` may be `true` simultaneously.
### Availability
---
## Options — `AvailableOptions()`
Options are key/value pairs (`map[string]any`) that configure each test run. They are declared grouped by **scope**, i.e. who sets them and how long they persist:
```go
func (p *MyPlugin) AvailableOptions() happydns.PluginOptionsDocumentation {
return happydns.PluginOptionsDocumentation{
RunOpts: []happydns.PluginOptionDocumentation{ /* … */ },
ServiceOpts: []happydns.PluginOptionDocumentation{ /* … */ },
DomainOpts: []happydns.PluginOptionDocumentation{ /* … */ },
UserOpts: []happydns.PluginOptionDocumentation{ /* … */ },
AdminOpts: []happydns.PluginOptionDocumentation{ /* … */ },
}
}
```
### Option scopes
| Scope | Who sets it | Storage key | Typical use |
|---|---|---|---|
| `RunOpts` | User, at test time | _(transient)_ | Per-invocation parameters |
| `ServiceOpts` | User | plugin + user + domain + service | Service-level configuration |
| `DomainOpts` | User | plugin + user + domain | Domain-level configuration |
| `UserOpts` | User | plugin + user | Personal preferences (e.g. language) |
| `AdminOpts` | Administrator | plugin | Instance-wide settings, shared credentials |
Before `RunTest` is called, happyDomain merges all scoped values from least specific (admin) to most specific (run-time). More-specific values silently override less-specific ones. `RunTest` always receives a single flat map and does not need to know which scope each value came from.
### Option fields
Each option is a `PluginOptionDocumentation` (an alias for `Field`):
`CheckerAvailability` controls where the checker is offered in the UI:
| Field | Type | Description |
|---|---|---|
| `Id` | `string` | **Required.** Key used in the `PluginOptions` map inside `RunTest` |
| `Type` | `string` | Input type: `"string"`, `"select"` |
| `Label` | `string` | Human-readable label shown in the UI |
| `Placeholder` | `string` | Placeholder text for the input field |
| `Default` | `any` | Default value pre-filled in the form |
| `Choices` | `[]string` | Options for `"select"` inputs |
| `Required` | `bool` | Whether the field must be filled before running |
| `Secret` | `bool` | Marks the field as sensitive (e.g. an API key) |
| `Hide` | `bool` | Hides the field from the user entirely |
| `Textarea` | `bool` | Renders a multiline text area |
| `Description` | `string` | Help text displayed below the field |
| `AutoFill` | `string` | Populate the field automatically from context (see below) |
| `ApplyToDomain` | `bool` | Checker can run against a whole domain. |
| `ApplyToZone` | `bool` | Checker can run against a zone. |
| `ApplyToService` | `bool` | Checker can run against a specific service. |
| `LimitToProviders` | `[]string` | Restrict to certain DNS provider identifiers (empty = no restriction). |
| `LimitToServices` | `[]string` | Restrict to certain service type identifiers, e.g. `"abstract.MatrixIM"` (empty = no restriction). |
### Auto-fill
### Options
When `AutoFill` is set, happyDomain populates the field from the test context; the user is not prompted:
Options are declared grouped by **scope**, i.e. who sets them and how long they persist. Each scope is a slice of `CheckerOptionDocumentation`:
| Constant | String value | Populated with |
| Scope | Who sets it | Typical use |
|---|---|---|
| `happydns.AutoFillDomainName` | `"domain_name"` | FQDN of the domain under test, e.g. `"example.com."` |
| `happydns.AutoFillSubdomain` | `"subdomain"` | Subdomain relative to the zone, e.g. `"www"` — service-scoped tests only |
| `happydns.AutoFillServiceType` | `"service_type"` | Service type identifier, e.g. `"abstract.MatrixIM"` — service-scoped tests only |
| `AdminOpts` | Administrator | Instance-wide settings, shared credentials. |
| `UserOpts` | User | Personal preferences (e.g. language). |
| `DomainOpts` | User | Domain-level configuration. |
| `ServiceOpts` | User | Service-level configuration. |
| `RunOpts` | User, at run time | Per-invocation parameters. |
```go
{
Id: "domainName",
Type: "string",
Label: "Domain name",
AutoFill: happydns.AutoFillDomainName,
Required: true,
}
```
happyDomain merges the scoped values from least specific (admin) to most specific (run-time) before calling `Collect`, so the provider receives a single flat `CheckerOptions` map. Each option is a `CheckerOptionField` with fields such as `Id`, `Type`, `Label`, `Default`, `Choices`, `Required`, `Secret`, `Description` and `AutoFill`. Read typed values out of the map with the SDK helpers `checker.GetOption`, `checker.GetIntOption`, `checker.GetBoolOption`, …
{{< notice style="info" >}}
When happyDomain registers an externalisable checker, it automatically appends an `endpoint` admin option, so the administrator can delegate collection to a remote HTTP endpoint instead of running the checker in-process. Leave it empty to run locally.
{{< /notice >}}
---
## Running the check — `RunTest()`
## The `ObservationProvider`
`RunTest` receives the merged option map and a metadata map (reserved for future use), performs the check, and returns a `PluginResult`.
Always assert option values to a concrete type before use — the map holds `any`:
The provider is the data-collection half of the checker:
```go
func (p *MyPlugin) RunTest(opts happydns.PluginOptions, _ map[string]string) (*happydns.PluginResult, error) {
domain, ok := opts["domainName"].(string)
if !ok || domain == "" {
return nil, fmt.Errorf("domainName option is required")
}
// … perform the check …
return &happydns.PluginResult{
Status: happydns.PluginResultStatusOK,
StatusLine: "All good",
Report: myStructuredReport,
}, nil
type ObservationProvider interface {
Key() ObservationKey
Collect(ctx context.Context, opts CheckerOptions) (any, error)
}
```
Return a **non-nil error** only for unexpected failures (network errors, invalid configuration). For expected check failures — the monitored service is down, DNS records are wrong — return a `PluginResult` with an appropriate status and a human-readable `StatusLine`.
- `Key()` returns the observation key this provider fills. It must match one of the `ObservationKeys` declared in the definition.
- `Collect` performs the actual work (a DNS query, an HTTP call, …) and returns any JSON-serialisable value. happyDomain marshals it to JSON and caches it; the rules then read it back.
### Result fields
A provider can optionally implement additional SDK interfaces to extend its behaviour:
| Field | Type | Description |
|---|---|---|
| `Status` | `PluginResultStatus` | Overall result level (see below) |
| `StatusLine` | `string` | Short summary displayed in the UI |
| `Report` | `any` | Any JSON-serialisable value stored as structured diagnostic data |
### Status levels (worst → best)
| Constant | Meaning |
| Interface | Purpose |
|---|---|
| `PluginResultStatusKO` | Check failed |
| `PluginResultStatusWarn` | Check passed with warnings |
| `PluginResultStatusInfo` | Informational, no action required |
| `PluginResultStatusOK` | Check fully passed |
| `CheckerHTMLReporter` | `GetHTMLReport(ctx ReportContext)` renders the stored observation as an HTML document. |
| `CheckerMetricsReporter` | `ExtractMetrics(ctx ReportContext, collectedAt)` produces time-series metrics. |
| `CheckEnabler` | `IsEligible(ctx, opts)` decides, from the target's actual data, whether running the checker is meaningful at all. |
| `DiscoveryPublisher` | `DiscoverEntries(data)` publishes `DiscoveryEntry` records other checkers can consume. |
Rules return `CheckState` values whose `Status` is one of `StatusOK`, `StatusInfo`, `StatusWarn`, `StatusCrit`, `StatusError` or `StatusUnknown`.
### Optional: standalone server
The SDK also provides `checker.Server`, HTTP scaffolding for running a checker as a remote endpoint instead of (or alongside) an in-process plugin. It exposes the routes `/health` and `/collect`, plus `/definition`, `/evaluate` and `/report` when the provider implements the matching optional interfaces. A provider that implements `CheckerInteractive` (`RenderForm` / `ParseForm`) additionally gets a human-facing form on `/check`, usable outside of happyDomain. See the [SDK README](https://git.happydns.org/checker-sdk-go) for details; the in-process plugin path described above does not require any of this.
---
## Building
## Build constraints
```bash
go build -buildmode=plugin -o happydomain-plugin-test-myplugin.so \
git.happydns.org/happyDomain/plugins/myplugin
```
Go's `plugin` package is unforgiving. To load successfully, your plugin must be built with:
Minimal `Makefile`:
- the **same Go toolchain version** as happyDomain itself, including the same patch level;
- the **same versions of every shared dependency** (pin them in your `go.mod`, vendoring the exact versions happyDomain ships);
- `CGO_ENABLED=1`;
- the same `GOOS`/`GOARCH` as the host binary.
```makefile
PLUGIN_NAME=myplugin
TARGET=../happydomain-plugin-test-$(PLUGIN_NAME).so
If any of these do not match, `plugin.Open` fails with a (sometimes cryptic) error like *"plugin was built with a different version of package …"*. The host logs it and skips the file.
all: $(TARGET)
$(TARGET): *.go
go build -buildmode=plugin -o $@ git.happydns.org/happyDomain/plugins/$(PLUGIN_NAME)
```
The prefix `happydomain-plugin-test-` is a convention; happyDomain loads every file in the plugin directories regardless of its name.
Go's `plugin` package only works on **linux**, **darwin** and **freebsd**. On other platforms happyDomain is built without plugin support and the configured plugin directories are ignored, with a warning logged at startup.
---
## Deployment
## Security and deployment
### 1. Copy the `.so` file
### Directory and file permissions
Loading a `.so` file is arbitrary code execution as the happyDomain process, so the loader enforces strict ownership before it touches any file:
- The plugin directory **must not be a symbolic link** — happyDomain refuses to follow one, to prevent it being redirected to an attacker-controlled path.
- The plugin directory **must not be group- or world-writable**. A directory writable by anyone but the owner is treated as a fatal misconfiguration and aborts loading.
- Any individual `.so` file that is **group- or world-writable is skipped** (logged and ignored), even inside a properly locked-down directory.
In practice: keep the directory owned by the happyDomain user, mode `0755`, and the plugin files mode `0644`.
```bash
cp happydomain-plugin-test-myplugin.so /usr/lib/happydomain/plugins/
sudo install -d -m 0755 -o happydomain /var/lib/happydomain/plugins
sudo install -m 0644 -o happydomain checker-dummy.so /var/lib/happydomain/plugins/
```
### 2. Point happyDomain at the directory
`happydomain.conf`:
```
plugins-directories=/usr/lib/happydomain/plugins
```
Environment variable:
### Building the plugin
```bash
HAPPYDOMAIN_PLUGINS_DIRECTORIES=/usr/lib/happydomain/plugins
CGO_ENABLED=1 go build -buildmode=plugin -o checker-dummy.so ./plugin
```
Multiple directories may be listed as a comma-separated value.
### Pointing happyDomain at the directory
### 3. Check the logs
The directory is configured with the **`--plugins-directory`** flag, which **may be repeated** to scan several directories:
On a successful load:
```
Plugin My Plugin loaded (version 1.0)
```bash
happydomain --plugins-directory /var/lib/happydomain/plugins
```
On a name conflict or load error a warning is logged with the filename and reason.
The equivalent environment variable is `HAPPYDOMAIN_PLUGINS_DIRECTORY`.
The loader scans each configured directory and attempts to load every `.so` file it finds. An individual plugin that fails to load — wrong build, missing symbols, a panic in its factory — is logged and skipped without aborting startup; one bad `.so` never prevents the others from loading.
### Restart and check the logs
```bash
sudo systemctl restart happydomain
```
On a successful load, happyDomain logs:
```
Plugin com.example.dummy (/var/lib/happydomain/plugins/checker-dummy.so) loaded
```
---
## Reference implementations
## Licensing
Two plugins are bundled in this directory:
Checker plugins import only `git.happydns.org/checker-sdk-go/checker`, which is licensed under **Apache-2.0**. The SDK is deliberately split out of the AGPL-3.0 happyDomain core as a small, stable public API for third-party checkers.
- **`matrix/`** — queries the Matrix federation tester API. Demonstrates `ApplyToService` with `LimitToServices` and `AdminOpts` for the backend URL.
- **`zonemaster/`** — drives the Zonemaster JSON-RPC API, polls for completion, and maps results to severity levels. Demonstrates `AutoFillDomainName`, `UserOpts` for language selection, and multi-level status mapping.
A plugin built against this SDK is therefore **not** a derivative work of happyDomain, and you may distribute your checker `.so` under any license you choose (MIT, Apache, proprietary, AGPL — whatever fits your needs).
---
## Reference implementation
[`checker-dummy`](https://git.happydns.org/checker-dummy) is the fully working, documented template that this page mirrors. Start from it when writing your own checker.

View file

@ -1,311 +1,235 @@
---
title: "Écrire un plugin happyDomain"
description: "Guide technique pour développer des plugins de test pour happyDomain"
title: "Écrire un plugin de vérification happyDomain"
description: "Guide technique pour développer des plugins checker pour happyDomain"
---
happyDomain prend en charge des **plugins de test** externes — des bibliothèques partagées (fichiers `.so`) qui ajoutent des vérifications de santé sur les domaines ou les services d'une instance en cours d'exécution. Les plugins sont chargés au démarrage sans recompiler le serveur ; l'opérateur dépose simplement un fichier `.so` dans un répertoire configuré.
happyDomain peut être étendu par des **plugins de vérification** (*checkers*) externes : des bibliothèques partagées (fichiers `.so`) qui ajoutent des diagnostics automatisés sur les zones, les domaines, les services ou les utilisateurs. Un plugin checker est chargé dans le processus happyDomain au démarrage. L'opérateur dépose simplement un fichier `.so` dans un répertoire configuré, sans recompiler le serveur.
## Fonctionnement
Un plugin reçoit un ensemble d'options assemblées depuis plusieurs portées de configuration, exécute une vérification (appel HTTP, requête DNS, …) et renvoie un résultat avec un niveau de statut et un rapport détaillé optionnel. Les résultats sont stockés et affichés dans l'interface happyDomain aux côtés du domaine ou du service concerné.
Au démarrage, happyDomain parcourt chaque répertoire listé dans l'option de configuration `plugins-directories`. Pour chaque fichier trouvé, il :
1. Ouvre la bibliothèque partagée.
2. Recherche le symbole exporté `NewTestPlugin`.
3. Appelle `NewTestPlugin()` pour obtenir une valeur de plugin.
4. Enregistre le plugin sous chaque nom renvoyé par `PluginEnvName()`.
Si le fichier n'est pas un plugin Go valide, si `NewTestPlugin` est absent ou s'il retourne une erreur, un avertissement est journalisé et le fichier est ignoré. Le serveur démarre toujours, quels que soient les échecs de chargement individuels.
---
## L'interface `TestPlugin`
Tout plugin doit implémenter quatre méthodes :
```go
type TestPlugin interface {
PluginEnvName() []string
Version() PluginVersionInfo
AvailableOptions() PluginOptionsDocumentation
RunTest(PluginOptions, map[string]string) (*PluginResult, error)
}
```
---
## Structure du projet
Un plugin est un module Go autonome compilé avec `-buildmode=plugin`. Il doit être dans `package main` et exporter exactement un symbole :
```go
func NewTestPlugin() (happydns.TestPlugin, error)
```
Organisation recommandée :
```
myplugin/
├── go.mod
├── Makefile
└── plugin.go # (ou réparti sur plusieurs fichiers .go)
```
### go.mod
```
module git.happydns.org/happyDomain/plugins/myplugin
go 1.25
require git.happydns.org/happyDomain v0.0.0
replace git.happydns.org/happyDomain => ../../
```
La directive `replace` pointe vers votre dépôt local happyDomain, garantissant que le plugin est compilé avec exactement les mêmes types que le serveur.
Un checker comporte deux moitiés. Il **collecte** d'abord des données brutes sur une cible (une observation). Il **évalue** ensuite ces données au regard d'un ensemble de règles afin de produire un statut. Les résultats sont stockés puis affichés dans l'interface de happyDomain, à côté du domaine ou du service concerné.
{{< notice style="warning" >}}
Un plugin Go et le processus hôte partagent le même environnement d'exécution. Ils **doivent** être compilés avec la même version de la chaîne d'outils Go et les mêmes versions de toutes les dépendances partagées. Tout écart provoque une erreur fatale au chargement.
Un plugin `.so` est chargé dans le processus happyDomain et s'exécute avec les mêmes privilèges que le serveur. Le répertoire des plugins doit être considéré comme un emplacement de confiance : happyDomain refuse de charger des plugins depuis un répertoire qui ne l'est pas (voir [Sécurité et déploiement](#sécurité-et-déploiement)).
{{< /notice >}}
---
## Point d'entrée
## Ce qu'un plugin checker doit exporter
Les plugins sont construits avec le module **`checker-sdk-go`**, publié séparément du cœur de happyDomain. Dans cette page, `checker` désigne le paquet `git.happydns.org/checker-sdk-go/checker`.
Le chargeur de happyDomain recherche un unique symbole exporté nommé `NewCheckerPlugin`, avec cette signature exacte :
```go
func NewCheckerPlugin() (*checker.CheckerDefinition, checker.ObservationProvider, error)
```
Les deux valeurs de retour décrivent les deux moitiés d'un checker :
- **`*CheckerDefinition`** décrit le checker : son identifiant, son nom, sa version, les clés d'observation dont il dépend, les options qu'il accepte, ses règles, un agrégateur optionnel, un intervalle de planification, et s'il expose des rapports HTML ou des métriques. Voir le [tableau des champs](#champs-de-checkerdefinition) ci-dessous.
- **`ObservationProvider`** est la moitié chargée de la collecte. Elle expose une méthode `Key()` (la clé d'observation que les règles consultent) et une méthode `Collect(ctx, opts)` qui renvoie la charge utile brute de l'observation. happyDomain sérialise ce résultat en JSON et le met en cache pour chaque contexte d'observation.
- Renvoyez une `error` non nulle si le plugin ne peut pas s'initialiser (variable d'environnement manquante, dépendance cgo cassée, …). L'hôte journalise l'erreur et ignore le fichier, sans interrompre le démarrage.
Un même fichier `.so` peut exporter plusieurs types de plugins. Le chargeur applique chaque chargeur connu à chaque fichier, puis ignore tout symbole qu'il ne reconnaît pas. Un binaire peut donc fournir plusieurs plugins.
---
## Exemple minimal
Voici le plus petit plugin qui se charge. Il collecte une observation fixe et ne déclare aucune règle. On peut l'adapter à partir de [`checker-dummy`](https://git.happydns.org/checker-dummy), l'implémentation de référence.
```go
// Command plugin is the happyDomain plugin entrypoint for the dummy checker.
//
// Build with:
// go build -buildmode=plugin -o checker-dummy.so ./plugin
package main
import "git.happydns.org/happyDomain/model"
import (
"context"
func NewTestPlugin() (happydns.TestPlugin, error) {
return &MyPlugin{}, nil
"git.happydns.org/checker-sdk-go/checker"
)
type dummyProvider struct{}
func (dummyProvider) Key() checker.ObservationKey { return "dummy.observation" }
func (dummyProvider) Collect(ctx context.Context, opts checker.CheckerOptions) (any, error) {
return map[string]string{"hello": "world"}, nil
}
// NewCheckerPlugin is the symbol resolved by happyDomain at startup.
func NewCheckerPlugin() (*checker.CheckerDefinition, checker.ObservationProvider, error) {
def := &checker.CheckerDefinition{
ID: "com.example.dummy",
Name: "Dummy checker",
Version: "0.1.0",
ObservationKeys: []checker.ObservationKey{"dummy.observation"},
// Add Rules / Aggregator / Options here in a real plugin.
}
return def, dummyProvider{}, nil
}
```
Le constructeur est l'endroit idéal pour effectuer une initialisation unique (ouvrir des fichiers de configuration, créer un client HTTP, …). Retournez une erreur si le plugin ne peut pas fonctionner.
{{< notice style="warning" >}}
Un plugin Go et le processus hôte partagent le même *runtime*. Ils **doivent** être compilés avec la même version de la chaîne d'outils Go et les mêmes versions de chaque dépendance partagée. Toute divergence produit une erreur bloquante au chargement. Voir [Contraintes de build](#contraintes-de-build).
{{< /notice >}}
---
## Nommage — `PluginEnvName()`
## Champs de `CheckerDefinition`
Renvoie un ou plusieurs identifiants courts en minuscules. Ces noms sont utilisés pour retrouver le plugin via l'API et pour indexer sa configuration stockée.
```go
func (p *MyPlugin) PluginEnvName() []string {
return []string{"myplugin"}
}
```
Choisissez des noms peu susceptibles d'entrer en conflit (ex. `"zonemaster"`, `"matrixim"`) et gardez-les **stables entre les versions**, car ils sont persistés avec la configuration utilisateur. Si deux plugins chargés revendiquent le même nom, le second est ignoré et un conflit est journalisé.
---
## Version et disponibilité — `Version()`
Décrit le plugin et contrôle l'endroit où il apparaît dans l'interface :
```go
func (p *MyPlugin) Version() happydns.PluginVersionInfo {
return happydns.PluginVersionInfo{
Name: "My Plugin",
Version: "1.0",
AvailableOn: happydns.PluginAvailability{
ApplyToDomain: true,
ApplyToService: false,
LimitToProviders: nil, // nil ou vide = tous les fournisseurs
LimitToServices: []string{"abstract.MatrixIM"},
},
}
}
```
Le `*CheckerDefinition` renvoyé par `NewCheckerPlugin` est la description de votre checker :
| Champ | Type | Description |
|---|---|---|
| `ApplyToDomain` | `bool` | Le plugin peut être exécuté sur un domaine entier |
| `ApplyToService` | `bool` | Le plugin peut être exécuté sur un service DNS spécifique |
| `LimitToProviders` | `[]string` | Restreint à certains identifiants de fournisseurs DNS (vide = aucune restriction) |
| `LimitToServices` | `[]string` | Restreint à certains types de services, ex. `"abstract.MatrixIM"` (vide = aucune restriction) |
| `ID` | `string` | **Requis.** Identifiant stable et persistant. Choisissez une valeur préfixée par un espace de noms (`com.example.dnssec-freshness`, et non `dnssec`) et ne la changez jamais : elle indexe les résultats stockés et la configuration utilisateur. |
| `Name` | `string` | Nom lisible affiché dans l'interface. |
| `Version` | `string` | Version du plugin (par exemple `"1.0.0"`). |
| `Availability` | `CheckerAvailability` | Déclare les portées auxquelles le checker s'applique et d'éventuelles restrictions de fournisseur ou de service (voir ci-dessous). |
| `Options` | `CheckerOptionsDocumentation` | Documente les options acceptées par le checker, regroupées par portée (voir ci-dessous). |
| `Rules` | `[]CheckRule` | Les règles évaluées sur l'observation collectée. |
| `Aggregator` | `CheckAggregator` | Optionnel. Combine les `CheckState` produits par chaque règle en un état de synthèse unique. |
| `Interval` | `*CheckIntervalSpec` | Bornes de planification optionnelles (durées `Min`, `Max`, `Default`). |
| `HasHTMLReport` | `bool` | À activer lorsque le provider implémente `CheckerHTMLReporter`. |
| `HasMetrics` | `bool` | À activer lorsque le provider implémente `CheckerMetricsReporter`. |
| `ObservationKeys` | `[]ObservationKey` | Les clés d'observation que ce checker lit. |
`ApplyToDomain` et `ApplyToService` peuvent être tous les deux `true` simultanément.
### Disponibilité
---
## Options — `AvailableOptions()`
Les options sont des paires clé/valeur (`map[string]any`) qui configurent chaque exécution de test. Elles sont déclarées regroupées par **portée**, c'est-à-dire qui les définit et combien de temps elles persistent :
```go
func (p *MyPlugin) AvailableOptions() happydns.PluginOptionsDocumentation {
return happydns.PluginOptionsDocumentation{
RunOpts: []happydns.PluginOptionDocumentation{ /* … */ },
ServiceOpts: []happydns.PluginOptionDocumentation{ /* … */ },
DomainOpts: []happydns.PluginOptionDocumentation{ /* … */ },
UserOpts: []happydns.PluginOptionDocumentation{ /* … */ },
AdminOpts: []happydns.PluginOptionDocumentation{ /* … */ },
}
}
```
### Portées des options
| Portée | Qui la définit | Clé de stockage | Usage typique |
|---|---|---|---|
| `RunOpts` | L'utilisateur, au moment du test | _(transitoire)_ | Paramètres propres à l'exécution |
| `ServiceOpts` | L'utilisateur | plugin + utilisateur + domaine + service | Configuration au niveau du service |
| `DomainOpts` | L'utilisateur | plugin + utilisateur + domaine | Configuration au niveau du domaine |
| `UserOpts` | L'utilisateur | plugin + utilisateur | Préférences personnelles (ex. langue) |
| `AdminOpts` | L'administrateur | plugin | Paramètres d'instance, identifiants partagés |
Avant l'appel à `RunTest`, happyDomain fusionne toutes les valeurs par portée, de la moins spécifique (admin) à la plus spécifique (exécution). Les valeurs plus spécifiques écrasent silencieusement les moins spécifiques. `RunTest` reçoit toujours une map plate unique et n'a pas besoin de savoir de quelle portée provient chaque valeur.
### Champs d'une option
Chaque option est un `PluginOptionDocumentation` (un alias pour `Field`) :
`CheckerAvailability` contrôle l'endroit où le checker est proposé dans l'interface :
| Champ | Type | Description |
|---|---|---|
| `Id` | `string` | **Obligatoire.** Clé utilisée dans la map `PluginOptions` dans `RunTest` |
| `Type` | `string` | Type de saisie : `"string"`, `"select"` |
| `Label` | `string` | Libellé lisible affiché dans l'interface |
| `Placeholder` | `string` | Texte indicatif du champ de saisie |
| `Default` | `any` | Valeur par défaut pré-remplie dans le formulaire |
| `Choices` | `[]string` | Options pour les saisies de type `"select"` |
| `Required` | `bool` | Indique si le champ doit être rempli avant l'exécution |
| `Secret` | `bool` | Marque le champ comme sensible (ex. une clé API) |
| `Hide` | `bool` | Masque entièrement le champ à l'utilisateur |
| `Textarea` | `bool` | Affiche une zone de texte multiligne |
| `Description` | `string` | Texte d'aide affiché sous le champ |
| `AutoFill` | `string` | Remplit le champ automatiquement depuis le contexte (voir ci-dessous) |
| `ApplyToDomain` | `bool` | Le checker peut s'exécuter sur un domaine entier. |
| `ApplyToZone` | `bool` | Le checker peut s'exécuter sur une zone. |
| `ApplyToService` | `bool` | Le checker peut s'exécuter sur un service précis. |
| `LimitToProviders` | `[]string` | Restreint à certains identifiants de fournisseurs DNS (vide = aucune restriction). |
| `LimitToServices` | `[]string` | Restreint à certains identifiants de types de services, par exemple `"abstract.MatrixIM"` (vide = aucune restriction). |
### Remplissage automatique
### Options
Lorsque `AutoFill` est défini, happyDomain remplit le champ à partir du contexte du test ; l'utilisateur n'est pas sollicité :
Les options sont déclarées par **portée**, c'est-à-dire selon qui les définit et combien de temps elles persistent. Chaque portée est une tranche de `CheckerOptionDocumentation` :
| Constante | Valeur chaîne | Rempli avec |
| Portée | Qui la définit | Usage habituel |
|---|---|---|
| `happydns.AutoFillDomainName` | `"domain_name"` | FQDN du domaine testé, ex. `"example.com."` |
| `happydns.AutoFillSubdomain` | `"subdomain"` | Sous-domaine relatif à la zone, ex. `"www"` — tests à portée service uniquement |
| `happydns.AutoFillServiceType` | `"service_type"` | Identifiant du type de service, ex. `"abstract.MatrixIM"` — tests à portée service uniquement |
| `AdminOpts` | Administrateur | Réglages valables pour toute l'instance, identifiants partagés. |
| `UserOpts` | Utilisateur | Préférences personnelles (par exemple la langue). |
| `DomainOpts` | Utilisateur | Configuration au niveau du domaine. |
| `ServiceOpts` | Utilisateur | Configuration au niveau du service. |
| `RunOpts` | Utilisateur, au moment de l'exécution | Paramètres propres à une invocation. |
```go
{
Id: "domainName",
Type: "string",
Label: "Nom de domaine",
AutoFill: happydns.AutoFillDomainName,
Required: true,
}
```
happyDomain fusionne les valeurs des différentes portées, de la moins spécifique (administrateur) à la plus spécifique (exécution), avant d'appeler `Collect`. Le provider reçoit donc une unique table `CheckerOptions` à plat. Chaque option est un `CheckerOptionField` avec des champs comme `Id`, `Type`, `Label`, `Default`, `Choices`, `Required`, `Secret`, `Description` et `AutoFill`. On lit les valeurs typées de la table à l'aide des fonctions du SDK `checker.GetOption`, `checker.GetIntOption`, `checker.GetBoolOption`, …
{{< notice style="info" >}}
Lorsque happyDomain enregistre un checker externalisable, il ajoute automatiquement une option d'administration `endpoint`. L'administrateur peut ainsi déléguer la collecte à un point d'accès HTTP distant plutôt que de l'exécuter dans le processus. Laissée vide, elle fait fonctionner le checker localement.
{{< /notice >}}
---
## Exécuter la vérification — `RunTest()`
## L'`ObservationProvider`
`RunTest` reçoit la map d'options fusionnée et une map de métadonnées (réservée à un usage futur), effectue la vérification et renvoie un `PluginResult`.
Convertissez toujours les valeurs d'options vers un type concret avant de les utiliser — la map contient des valeurs de type `any` :
Le provider est la moitié du checker chargée de la collecte :
```go
func (p *MyPlugin) RunTest(opts happydns.PluginOptions, _ map[string]string) (*happydns.PluginResult, error) {
domain, ok := opts["domainName"].(string)
if !ok || domain == "" {
return nil, fmt.Errorf("l'option domainName est obligatoire")
}
// … effectuer la vérification …
return &happydns.PluginResult{
Status: happydns.PluginResultStatusOK,
StatusLine: "Tout est bon",
Report: myStructuredReport,
}, nil
type ObservationProvider interface {
Key() ObservationKey
Collect(ctx context.Context, opts CheckerOptions) (any, error)
}
```
Retournez une **erreur non nulle** uniquement pour les échecs inattendus (erreurs réseau, configuration invalide). Pour les échecs de vérification attendus — le service surveillé est indisponible, les enregistrements DNS sont incorrects — retournez un `PluginResult` avec un statut approprié et un `StatusLine` lisible par un humain.
- `Key()` renvoie la clé d'observation que ce provider remplit. Elle doit correspondre à l'une des `ObservationKeys` déclarées dans la définition.
- `Collect` effectue le travail réel (une requête DNS, un appel HTTP, …) et renvoie n'importe quelle valeur sérialisable en JSON. happyDomain la convertit en JSON et la met en cache ; les règles la relisent ensuite.
### Champs du résultat
Un provider peut implémenter des interfaces supplémentaires du SDK pour étendre son comportement :
| Champ | Type | Description |
|---|---|---|
| `Status` | `PluginResultStatus` | Niveau de résultat global (voir ci-dessous) |
| `StatusLine` | `string` | Résumé court affiché dans l'interface |
| `Report` | `any` | Toute valeur sérialisable en JSON, stockée comme données de diagnostic structurées |
### Niveaux de statut (du pire au meilleur)
| Constante | Signification |
| Interface | Rôle |
|---|---|
| `PluginResultStatusKO` | La vérification a échoué |
| `PluginResultStatusWarn` | La vérification a réussi avec des avertissements |
| `PluginResultStatusInfo` | Informatif, aucune action requise |
| `PluginResultStatusOK` | La vérification a entièrement réussi |
| `CheckerHTMLReporter` | `GetHTMLReport(ctx ReportContext)` rend l'observation stockée sous forme de document HTML. |
| `CheckerMetricsReporter` | `ExtractMetrics(ctx ReportContext, collectedAt)` produit des métriques temporelles. |
| `CheckEnabler` | `IsEligible(ctx, opts)` décide, à partir des données réelles de la cible, s'il est pertinent d'exécuter le checker. |
| `DiscoveryPublisher` | `DiscoverEntries(data)` publie des enregistrements `DiscoveryEntry` que d'autres checkers peuvent consommer. |
Les règles renvoient des valeurs `CheckState` dont le `Status` vaut `StatusOK`, `StatusInfo`, `StatusWarn`, `StatusCrit`, `StatusError` ou `StatusUnknown`.
### Optionnel : serveur autonome
Le SDK fournit aussi `checker.Server`, une ossature HTTP pour exécuter un checker comme un point d'accès distant plutôt que (ou en plus) d'un plugin chargé dans le processus. Elle expose les routes `/health` et `/collect`, ainsi que `/definition`, `/evaluate` et `/report` lorsque le provider implémente les interfaces optionnelles correspondantes. Un provider qui implémente `CheckerInteractive` (`RenderForm` / `ParseForm`) dispose en outre d'un formulaire `/check` destiné aux humains, utilisable en dehors de happyDomain. Voir le [README du SDK](https://git.happydns.org/checker-sdk-go) pour les détails ; le mode plugin décrit plus haut n'en a pas besoin.
---
## Compilation
## Contraintes de build
```bash
go build -buildmode=plugin -o happydomain-plugin-test-myplugin.so \
git.happydns.org/happyDomain/plugins/myplugin
```
Le paquet `plugin` de Go est intransigeant. Pour se charger correctement, votre plugin doit être compilé avec :
`Makefile` minimal :
- la **même version de la chaîne d'outils Go** que happyDomain, jusqu'au même niveau de correctif ;
- les **mêmes versions de chaque dépendance partagée** (à figer dans votre `go.mod`, en vendorisant les versions exactes que happyDomain embarque) ;
- `CGO_ENABLED=1` ;
- les mêmes `GOOS` et `GOARCH` que le binaire hôte.
```makefile
PLUGIN_NAME=myplugin
TARGET=../happydomain-plugin-test-$(PLUGIN_NAME).so
Si l'un de ces points ne correspond pas, `plugin.Open` échoue avec une erreur parfois obscure, du type *« plugin was built with a different version of package … »*. L'hôte la journalise et ignore le fichier.
all: $(TARGET)
$(TARGET): *.go
go build -buildmode=plugin -o $@ git.happydns.org/happyDomain/plugins/$(PLUGIN_NAME)
```
Le préfixe `happydomain-plugin-test-` est une convention ; happyDomain charge tous les fichiers présents dans les répertoires de plugins, quel que soit leur nom.
Le paquet `plugin` de Go ne fonctionne que sur **linux**, **darwin** et **freebsd**. Sur les autres plateformes, happyDomain est construit sans prise en charge des plugins et les répertoires configurés sont ignorés, avec un avertissement journalisé au démarrage.
---
## Déploiement
## Sécurité et déploiement
### 1. Copier le fichier `.so`
### Permissions du répertoire et des fichiers
Charger un fichier `.so` revient à exécuter du code arbitraire avec les droits du processus happyDomain. Le chargeur impose donc des règles strictes de propriété avant de toucher au moindre fichier :
- Le répertoire des plugins **ne doit pas être un lien symbolique** : happyDomain refuse de le suivre, pour éviter qu'il soit redirigé vers un chemin contrôlé par un attaquant.
- Le répertoire des plugins **ne doit pas être accessible en écriture au groupe ni à tous**. Un répertoire modifiable par quelqu'un d'autre que son propriétaire est traité comme une erreur de configuration bloquante et interrompt le chargement.
- Tout fichier `.so` **accessible en écriture au groupe ou à tous est ignoré** (journalisé puis écarté), même dans un répertoire par ailleurs verrouillé.
En pratique : conservez le répertoire détenu par l'utilisateur happydomain, en mode `0755`, et les fichiers de plugins en mode `0644`.
```bash
cp happydomain-plugin-test-myplugin.so /usr/lib/happydomain/plugins/
sudo install -d -m 0755 -o happydomain /var/lib/happydomain/plugins
sudo install -m 0644 -o happydomain checker-dummy.so /var/lib/happydomain/plugins/
```
### 2. Indiquer le répertoire à happyDomain
`happydomain.conf` :
```
plugins-directories=/usr/lib/happydomain/plugins
```
Variable d'environnement :
### Construire le plugin
```bash
HAPPYDOMAIN_PLUGINS_DIRECTORIES=/usr/lib/happydomain/plugins
CGO_ENABLED=1 go build -buildmode=plugin -o checker-dummy.so ./plugin
```
Plusieurs répertoires peuvent être listés en les séparant par des virgules.
### Indiquer le répertoire à happyDomain
### 3. Vérifier les journaux
Le répertoire se configure avec l'option **`--plugins-directory`**, qui **peut être répétée** pour analyser plusieurs répertoires :
En cas de chargement réussi :
```
Plugin My Plugin loaded (version 1.0)
```bash
happydomain --plugins-directory /var/lib/happydomain/plugins
```
En cas de conflit de nom ou d'erreur de chargement, un avertissement est journalisé avec le nom du fichier et la raison.
La variable d'environnement équivalente est `HAPPYDOMAIN_PLUGINS_DIRECTORY`.
Le chargeur analyse chaque répertoire configuré et tente de charger tous les fichiers `.so` qu'il y trouve. Un plugin qui échoue au chargement (mauvaise compilation, symboles absents, panique dans sa fabrique) est journalisé puis ignoré, sans interrompre le démarrage : un seul `.so` défectueux n'empêche jamais le chargement des autres.
### Redémarrer et vérifier les journaux
```bash
sudo systemctl restart happydomain
```
En cas de chargement réussi, happyDomain journalise :
```
Plugin com.example.dummy (/var/lib/happydomain/plugins/checker-dummy.so) loaded
```
---
## Implémentations de référence
## Licence
Deux plugins sont fournis dans ce répertoire :
Les plugins checker n'importent que `git.happydns.org/checker-sdk-go/checker`, sous licence **Apache-2.0**. Le SDK a été délibérément détaché du cœur de happyDomain (sous AGPL-3.0) pour offrir une API publique réduite et stable aux checkers tiers.
- **`matrix/`** — interroge l'API de test de fédération Matrix. Illustre `ApplyToService` avec `LimitToServices` et `AdminOpts` pour l'URL du serveur tiers.
- **`zonemaster/`** — pilote l'API JSON-RPC de Zonemaster, attend la fin du test et agrège les résultats par niveau de sévérité. Illustre `AutoFillDomainName`, `UserOpts` pour la sélection de la langue et la gestion de statuts multi-niveaux.
Un plugin construit avec ce SDK n'est donc **pas** une œuvre dérivée de happyDomain. Vous pouvez distribuer votre `.so` sous la licence de votre choix (MIT, Apache, propriétaire ou AGPL, selon vos besoins).
---
## Implémentation de référence
[`checker-dummy`](https://git.happydns.org/checker-dummy) est le modèle complet et documenté dont s'inspire cette page. Partez-en pour écrire votre propre checker.