Skip to content

Migrating from 0.2 to 0.3

dnspatch is v0.x: the configuration format and the plugin API may change between minor versions. This page lists what breaks in 0.3 and how to adapt.

Three things break: the [instance.retriever] table in the configuration, the Retriever and Provider interfaces of plugin, and the required:"true" struct tag of a plugin configuration. Everything else in 0.3 is additive.

Configuration: [instance.retriever] is now an array

An instance takes one or more retrievers, so the table became an array of tables. The old form is rejected with "retriever" must be an array of tables: [[instance.retriever]].

Before:

[[instance]]
domain = "home.example.com"

  [instance.retriever]
  ref = "ipify"

  [[instance.provider]]
  ref = "regru"

After:

[[instance]]
domain = "home.example.com"

  [[instance.retriever]]
  ref = "ipify"

  [[instance.provider]]
  ref = "regru"

Retrievers are polled in order until every address family is filled, so a second retriever is either a fallback or the source of the other family (dual-stack). See examples/.

Configuration: what is new and needs no action

  • type may be set instead of ref in [[instance.retriever]] and [[instance.provider]], which builds the plugin from that table alone. The two are mutually exclusive.
  • Any parameter can read a secret from a file: ${file:PATH}. A literal ${ is still written $${.
  • family = "dual" on ipify, icanhazip, identme and ifconfigco fetches both families with one retriever.
  • dnspatch --check-config validates a configuration without starting the daemon.
  • New plugins: retriever interface; providers beget, dyndns2 (and its ready-made forms dyn, dynu, nicru, noip), rfc2136, yandexcloud.
  • dnspatch --check-config tells providers of one type apart by the parameters that differ between them.

Plugin API: Retriever

GetIPAddress is replaced by GetAddresses, which returns both families.

Before:

func (r *Retriever) GetIPAddress(ctx context.Context) (netip.Addr, error)

After:

func (r *Retriever) GetAddresses(ctx context.Context) (plugin.Addresses, error)

plugin.Addresses is struct{ V4, V6 netip.Addr }. Fill the family you found and leave the other at its zero value; at least one field must be valid. A retriever that only ever knows one family does:

return plugin.Addresses{V4: addr}, nil // or V6: addr

Plugin API: Provider

SetIPAddress is replaced by Update, which receives both families and the record options.

Before:

func (p *Provider) SetIPAddress(ctx context.Context, addr netip.Addr) error

After:

func (p *Provider) Update(ctx context.Context, addrs plugin.Addresses, opts plugin.RecordOptions) error
  • An invalid (zero) V4 or V6 means that family is left untouched: do not delete or change the existing record of that type. The runner sends only the families that changed since the last successful write.
  • Both fields can be valid in one call: write the A and the AAAA record. If the service cannot do both in one request, do them one after another and return an error if either fails.
  • opts.TTL, when positive, overrides the provider's configured TTL. A provider whose service has no TTL ignores it.

The simplest port of a single-family provider keeps the old body and calls it per family:

func (p *Provider) Update(ctx context.Context, addrs plugin.Addresses, opts plugin.RecordOptions) error {
    for _, addr := range []netip.Addr{addrs.V4, addrs.V6} {
        if !addr.IsValid() {
            continue
        }
        if err := p.set(ctx, addr); err != nil {
            return err
        }
    }
    return nil
}

Tests that called SetIPAddress or GetIPAddress must be updated the same way. Run go generate ./... afterwards if the plugin configuration changed.

Plugin API: required is an option of the toml tag

A required parameter is marked with an option of the toml tag, the way encoding/json writes json:"name,omitempty". The separate required:"true" tag is no longer read.

Before:

Token string `toml:"token" required:"true"`

After:

Token string `toml:"token,required"`

Nothing reports the old tag: a plugin built against 0.3 that still says required:"true" compiles, and the parameter silently becomes optional, so a forgotten token reaches the service as an unauthorised request. Search the plugin for required: and convert every match. An unknown or repeated option in the toml tag stops the process at registration.

A parameter that holds a password, key, token or a URL with a login should also carry the new secret option, toml:"token,required,secret": --check-config prints such a value as *** instead of the value itself.