Capstan.Config (Capstan v0.1.0)

Copy Markdown View Source

Option validation and the fail-closed server-precondition gate.

Two clearly separated responsibilities:

  • validate/1 is pure — it normalises the user's start_link options and refuses a mis-shaped or unsafe configuration before any socket is opened.
  • check_preconditions/1 issues one COM_QUERY on an already-authenticated socket and refuses to start unless the source's binlog is configured for lossless row-based CDC.

Server preconditions (ADR-0002)

check_preconditions/1 reads five global variables in a single query and refuses with a DISTINCT reason per violation — degraded row decoding silently guesses column identity, so the gate fails closed rather than proceed:

  • binlog_format must be ROW — else :binlog_format_not_row
  • binlog_row_image must be FULL — else :binlog_row_image_not_full
  • binlog_row_metadata must be FULL — else :binlog_row_metadata_not_full
  • binlog_row_value_options must be empty ("" = full JSON, not PARTIAL_JSON) — else :binlog_row_value_options_not_empty
  • gtid_mode must be ON — else :gtid_mode_not_on

MySQL simple-query results are all text strings, so every value is compared as text against the expected literal and never coerced to a typed term — an empty binlog_row_value_options arrives as "", not nil or 0.

TLS verification posture (ADR-0002)

ssl defaults true. Peer verification is an explicit operator choice, never a silent default: with TLS on, ssl_opts must carry EITHER a cacertfile/cacerts OR an explicit verify:. Given neither, validate/1 fails closed with :tls_verification_unspecified rather than let OTP's verify_peer default select a posture nobody chose. This mirrors the guard Capstan.Protocol.Handshake applies at connect time, moved to config time so a bad TLS configuration is refused before any socket is opened.

Authenticated TLS against MySQL's auto-generated certificate. A cacertfile alone drives verify: :verify_peer, but MySQL's auto-generated server certificate is self-signed with CN …Auto_Generated_Server_Certificate and no SAN, so verify_peer's hostname check fails against an address such as 127.0.0.1. An operator taking the authenticated route must ALSO pass server_name_indication: :disable in ssl_opts — the chain is verified, the hostname is not (VERIFY_CA semantics). validate/1 does NOT inject this: that would silently weaken every cacertfile user, so the operator supplies it deliberately.

Summary

Types

A value-free precondition-gate refusal (ADR-0002).

t()

The normalised configuration validate/1 returns on success.

A value-free option-validation refusal.

Functions

Reads the five server preconditions over socket and returns :ok iff all pass.

Validates raw start_link options into a normalised config map, or returns a value-free error.

Types

precondition_error()

@type precondition_error() ::
  :binlog_format_not_row
  | :binlog_row_image_not_full
  | :binlog_row_metadata_not_full
  | :binlog_row_value_options_not_empty
  | :gtid_mode_not_on
  | :precondition_query_failed

A value-free precondition-gate refusal (ADR-0002).

t()

@type t() :: %{
  connection: keyword(),
  server_id: pos_integer(),
  max_command_retries: non_neg_integer()
}

The normalised configuration validate/1 returns on success.

validation_error()

@type validation_error() ::
  :config_invalid | :server_id_required | :tls_verification_unspecified

A value-free option-validation refusal.

Functions

check_preconditions(socket)

@spec check_preconditions(Capstan.Protocol.Packet.socket()) ::
  :ok
  | {:error,
     precondition_error()
     | {:query_error, non_neg_integer()}
     | {:transport, term()}}

Reads the five server preconditions over socket and returns :ok iff all pass.

Issues ONE COM_QUERY on the already-authenticated socket and compares each value as text (ADR-0002). A wrong variable refuses with its distinct reason; a server or transport error is surfaced fail-closed, never swallowed into a spurious :ok.

validate(opts)

@spec validate(keyword()) :: {:ok, t()} | {:error, validation_error()}

Validates raw start_link options into a normalised config map, or returns a value-free error.

Refuses: :server_id_required (missing or non-positive server_id), :tls_verification_unspecified (ssl: true with no CA source and no explicit verify:), and :config_invalid (any other missing or mis-shaped option). Defaults applied: ssl true, ssl_opts [], auth_plugins [:caching_sha2_password], password "", database nil, max_command_retries 5.