Skip to content

FMQ config update for v1.0 - #576

Open
dborovcanin wants to merge 12 commits into
absmach:mainfrom
dborovcanin:fmq-config-strict
Open

FMQ config update for v1.0#576
dborovcanin wants to merge 12 commits into
absmach:mainfrom
dborovcanin:fmq-config-strict

Conversation

@dborovcanin

Copy link
Copy Markdown
Contributor

What type of PR is this?

What does this do?

Which issue(s) does this PR fix/relate to?

Have you included tests for your changes?

Did you document any new/modified features?

Notes

Configuration decoding used plain yaml.Unmarshal, so an unknown or
misspelled key was silently discarded, and a --config path that did not
exist fell back to built-in defaults. A typo could start a broker with
none of the operator's settings, auth and TLS included, while reporting
healthy.

Decoding now uses yaml.Decoder with KnownFields(true), and a named config
file that does not exist is an error. The previous fallback moves to
LoadOptional, reachable with the new --config-optional flag. Reload uses
Load, so a config file that goes missing under a running broker fails the
reload and retains the live configuration.

Validate rejects two listeners bound to the same address, keeping TCP and
UDP separate since CoAP may reuse a TCP port number.

Strict decoding surfaced four example files still declaring tcp.plain and
websocket.plain, slots renamed to v3/v5 earlier. The blocks were discarded
whole, so each silently opened two plaintext TCP and two plaintext
WebSocket listeners where it declared one of each. The bind check then
caught examples/production.yaml binding the WebSocket v5 default to its
declared WSS port, in a file stating three times that it serves no
plaintext.

server.tcp and server.websocket said nothing about MQTT while sitting
beside server.amqp and server.amqp091. Both move under server.mqtt, with
TCPConfig renamed to MQTTTCPConfig, WebSocketConfig to
MQTTWebSocketConfig, and a new MQTTConfig grouping the two transports.

BREAKING CHANGE: server.tcp and server.websocket become server.mqtt.tcp
and server.mqtt.websocket. Existing configurations now fail at startup
naming the offending key instead of silently losing listener settings.

Signed-off-by: dusan <borovcanindusan1@gmail.com>
A queue's delivery address is not injective. queueDeliveryTopic absorbs
the leading level when it already equals the queue name, so a capture of
m/acme/temp into queue m, a capture of acme/temp into queue m, and an
explicit publish to $queue/m/acme/temp all arrive as
$queue/m/acme/temp. $queue/q/y is likewise both a capture of y and a
capture of q/y. A consumer could not recover where a message came from,
and the address format freezes at v1.

Stamp the origin into a broker-owned x-source-topic property instead of
changing the address. The address keeps its current meaning, so nothing
a consumer subscribes to today moves; what changes is that the origin is
now recoverable. The property is written after the publisher's own
properties are copied in, so a publisher cannot forge it, and it is
reserved, so it is never mistaken for a user property.

The remote path rebuilds its own envelope from an address that has
already been converted and cannot be reversed, so QueueMessage carries
the source topic as its own field. It travels in the existing property
map on the wire and is lifted back out on decode, so no protocol
definition changes.

The collisions are pinned in tests as the contract they now are, rather
than left to be rediscovered.

Signed-off-by: dusan <borovcanindusan1@gmail.com>
(cherry picked from commit f3655c1)
ValidateLocalPrincipals opened every secret file to check its contents,
so validating a production configuration required that machine to hold
the production secrets. That is why config-local-principal.yaml, the one
shipped file exercising two AMQP 0.9.1 listeners and a local-principal
listener, was the only configuration CI could not gate, and why
`fluxmq config validate` could not check a real file on a workstation.

The checks were redundant. broker/localauth loadFingerprint already
reads the file, strips the terminal newline, and rejects embedded CR/LF,
NUL bytes, and secrets under 32 bytes — the same rules, enforced where
the credential material is actually loaded. That runs at startup and on
every reload, so nothing is now unchecked: a broker with a missing or
weak secret still refuses to start.

Configuration validation keeps what is genuinely declarative, including
that a secret file is named and not blank. The shipped local-principal
configuration joins make validate-configs, so all six are gated.

A secret-content failure now surfaces from the local-principal reload
callback rather than from config.Load, so the reload manager's failure
observer no longer fires for it — production already records that case
inside the callback, and firing both would double-count. The observer
keeps its own coverage for the case it exists for: a configuration that
cannot be loaded at all, where the callback is never reached.

Signed-off-by: dusan <borovcanindusan1@gmail.com>
(cherry picked from commit 139020f)
Addresses were checked for being non-empty and unique and nothing else,
so every malformed form was accepted: a port above 65535, a negative
port, a non-numeric port, a bare "1883" with no colon, and plain prose.
The first sign of a typo was a broker that logged a bind failure and
exited.

Decide what the string alone can decide. The address must split into host
and port, the port must be a number in 1 to 65535, and the host, if given,
must not contain whitespace. The host is deliberately not resolved:
validation has to work on a machine that cannot see the deployment's DNS,
so ":1883", "127.0.0.1:1883", "[::1]:1883" and "broker.internal:1883" all
pass.

Port 0 is refused as well. It asks the kernel for an arbitrary free port,
and a broker nobody can be told the address of is not a deployment, so the
message says to choose a fixed port rather than reporting a range error.

The check runs over the same listener set the duplicate-bind check already
walks, so it covers the messaging listeners, admin, health, and the CoAP
bridge. Every failure names the key it came from.

Ported from the v1 configuration work, which carried it against a listener
schema this branch does not adopt.

Signed-off-by: dusan <borovcanindusan1@gmail.com>
storage.sync_writes reads as "fsync all storage", but it only reaches the
broker key-value store that holds retained messages and sessions. The
queue append-only log is a separate engine whose acknowledgement policy is
not configurable at all: ordinary appends are buffered and only one
internal path calls AppendAndSync. An operator setting sync_writes: true
on a durable-queue deployment would reasonably think they had bought queue
durability, and they had not.

Rename it to storage.badger_sync_writes so the key names its engine.
Renaming is free now and breaking after the 1.0 freeze, and it stops the
ambiguity growing when a queue-side durability key lands beside it. Strict
decoding means an old file fails with "storage.sync_writes: unknown field"
rather than silently losing the setting.

The doc comments on the runtime type and the reference documentation now
state which engine the key reaches, and that queue durability is not it.
The schema test pins the new name and asserts the old one is gone.

Also adds v1.md, the stable-core hardening plan written alongside the v1
configuration work, so the release scope is tracked in the repository
rather than in a working tree.

Ported from the v1 configuration work.

Signed-off-by: dusan <borovcanindusan1@gmail.com>
Listeners were changed so that an omitted key takes the default and a
written 0 means unlimited or no deadline. Everything else kept the older
convention where 0 silently selected the default, so the same literal
meant opposite things depending on the section: max_connections: 0 gave an
uncapped listener, capture_workers: 0 gave four workers.

The rule is now one rule everywhere: a value the operator writes is never
replaced by a different one. Where zero is a coherent setting it is
honoured, as it is for listener limits and timeouts. Where it is not, it
is refused with a message that says to omit the key instead.

capture_workers, capture_queue_depth, capture_drain_timeout,
identity_cache_size, identity_cache_ttl, and hooks.timeout have no
coherent zero — no capture lanes, a cache holding nothing, or a blocking
hook with no deadline are not settings anyone wants — so they become
optional and reject a written zero. Omitting them still takes the built-in
default, which is unchanged, so no existing behaviour moves.

The reference documentation now lists the real default for each of them
rather than 0, and says a written zero is rejected.

Ported from the v1 configuration work.

Signed-off-by: dusan <borovcanindusan1@gmail.com>
Strict decoding turns a stale documentation example into a broker that
refuses to start, so the examples are now part of the contract rather
than prose beside it. Five yaml blocks no longer loaded:

- configuration/server.md declared server.mqtt.websocket.plain. The page
  documenting the listener rename kept the slot the rename removed.
- messaging/consumer-groups.md documented five queue_manager keys that
  have never existed. Only auto_commit_interval is configurable; the
  rest are queue/consumer.DefaultConfig values with no YAML surface, so
  the section now says which are fixed and at what value.
- configuration/security.md granted an exact publish target without
  cluster.enabled: false, which the same page's prose calls a startup
  error.
- deployment/internal-amqp-local-principals.md shows the removed flat
  auth form deliberately, and reference/configuration-reference.md
  quotes one section in isolation. Both are marked as blocks that must
  not load.

TestDocumentedConfigsLoad closes the gap that let these through:
TestShippedConfigsDecodeStrictly covers examples/ and deployments/ but
never looked at documentation. It guards 27 blocks across docs/ and
README.md, treats a block as broker configuration when any top-level key
is a config key so manifests and payload samples need no annotation, and
takes an explicit <!-- fluxmq:config-skip: reason --> marker for the
blocks that must fail.

Signed-off-by: dusan <borovcanindusan1@gmail.com>
The repository carried two independently written v1 plans. v1.md
estimated 54-81 engineer-days; the working-tree roadmap estimated 31-44.
The gap was scope rather than disagreement, and the union of the two is
larger than either: 67-100 days to the tag.

ROADMAP.md is now the single plan and V1-READINESS.md the evidence
behind it, both tracked so they cannot follow plan.md and the three
design documents that were deleted before them.

Every finding imported from v1.md was re-verified against this branch
first. Confirmed and scheduled: the admin API and Connect queue service
have no authentication (P0-9), DLQ movement and replication both fail
open (P0-10), etcd peer traffic is cleartext (P0-11), the AMQP 1.0
handshake is unbounded, and images are published before they are
scanned. One was stale and rewritten: readiness already checks the
broker, storage, and peer reachability, so the work is extending it
rather than adding it. Two were already delivered on this branch: the
queue delivery address is settled by types.PropSourceTopic, and topic
matching is a trie.

The config branch stays parked rather than merged; its listener model is
superseded by server.mqtt, and the three commits still worth salvaging
are named in the progress log.

Signed-off-by: dusan <borovcanindusan1@gmail.com>
Four defects and two stale documents, from review of this branch.

Trailing YAML documents were silently dropped. The decoder ran once and
never checked for a second document, so a `---` above an auth or TLS
section started the broker on defaults while the file plainly showed
otherwise — the same failure strict decoding exists to end. Decoding now
continues past the first document and rejects anything that is not EOF,
naming the line.

Moved listener sections now report where they went. server.tcp and
server.websocket were removed in favour of server.mqtt.*, and the
decoder alone said only "field tcp not found", which tells an operator
that something broke but not what to write. The load path names the
replacement, reusing the mechanism that already rejects the flat auth
keys. This makes the cutover legible; it does not make it survivable,
and the deployments consuming the published image still have to be
sequenced.

Duplicate-bind validation now matches the listeners startup opens. It
counted server.health_addr even when health_enabled was false, which
refused a legitimate port reuse, and omitted the amqp091 internal and
service aliases, which let a genuine collision through to fail at bind
time instead.

Documentation: the CLI reference still described the missing-file
fallback this branch removed and never mentioned --config-optional, and
both hook pages told operators that an explicit `timeout: 0` selects the
500ms default when it is now rejected at load. Copying either one
prevented startup.

The queue delivery contract is qualified rather than overstated. The
source topic travels in a broker-stamped property, and MQTT 3.1.1 cannot
encode properties, so a 3.1.1 consumer of a captured message receives
the queue identity and nothing about the origin. Making the address
injective does not fix it: a capture of acme/temp into queue m renders
exactly as an explicit publish to $queue/m/acme/temp, so separating them
would need a marker level imposed on every protocol to serve the one
that cannot read properties. Recorded as an open decision.

Signed-off-by: dusan <borovcanindusan1@gmail.com>
A 3.1.1 consumer could subscribe to a classic queue, receive messages,
and never settle them, because settlement needs identifiers the protocol
cannot carry. The broker now settles those deliveries on PUBACK, which
resolves the open contract question this document raised: origin
recovery for a captured message still needs MQTT 5.0 or AMQP, and the
support matrix says so rather than claiming both MQTT versions are
equivalent.

The implementation is on fmq-mqtt-queue-ack, which targets main rather
than stacking on this branch: it depends on nothing here, and this
branch cannot merge until magistrala is pinned off the latest image tag.

Opens one item: a cross-node takeover rebuilds inflight messages without
their properties, so a settled delivery redelivers after a node move.
Closing that needs an additive proto/cluster/v1 field, which is cheaper
before the tag than after.

Signed-off-by: dusan <borovcanindusan1@gmail.com>
Two validations refused files and deployments that are fine, both found
by running the reviewer's own reproduction probes against the branch.

A trailing document separator is not a dropped document. The strict
decoder rejected anything after the first `---`, which caught the case it
was written for — a second document carrying auth or TLS settings that
would be silently ignored — but also caught a file that merely ends in a
separator, or one followed by comments. yaml.v3 reports those as a null
document rather than as the end of the stream. An empty trailing
document now passes, and a file may end in as many separators as its
templating emits.

Duplicate-bind detection treated any wildcard as colliding with any host
on the same port, so 0.0.0.0:1883 and [::1]:1883 were refused although
both bind: an IPv4 wildcard does not accept IPv6 connections. Wildcards
now collide only with the families they accept, with "::" treated as
dual-stack because that is the Linux default and the case that really
collides. The error also names both addresses; it previously reported
one side twice, so a conflict involving a wildcard printed the address
the operator had not written.

Signed-off-by: dusan <borovcanindusan1@gmail.com>
The MQTT 3.1.1 queue settlement work landed on main as absmach#580 while this
branch waits on the magistrala pin, so the entry describing it as
pending, and the next-session list still naming 1.2 as unstarted, no
longer matched the tree.

Signed-off-by: dusan <borovcanindusan1@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant