CASSSIDECAR-429: Add RFC 6902-inspired JSON Patch support to ConfigurationManager#369
CASSSIDECAR-429: Add RFC 6902-inspired JSON Patch support to ConfigurationManager#369pauloricardomg wants to merge 4 commits into
Conversation
…ationManager Implement patch operations using RFC 6902-inspired semantics: paths reference the effective configuration structure but mutations target the overlay. Supported operations: add, remove, replace, test. Key behaviors: - Paths use JSON Pointer syntax for both top-level and nested cassandraYaml keys - Nested path mutations copy the entire top-level key from effective config into the overlay (copy-siblings strategy), ensuring the overlay is self-contained at top-level key granularity - test operations assert against effective config without mutating - replace fails if path is absent from effective config (typo protection) - remove fails if path only exists in base template (not in overlay) - All operations validated atomically before any mutations are applied - JVM option key format and conflicting boolean option validation - Nested paths into array-valued keys are rejected gracefully
| ? currentOverlay.configuration() | ||
| : new CassandraConfigurationOverlay(null, null); | ||
| CassandraConfigurationOverlay updatedOverlay = | ||
| ConfigurationPatchApplier.apply(parsedOps, effectiveConfig.configuration(), currentOverlayConfig); |
There was a problem hiding this comment.
If incoming patch request has a conflicting jvm option against base yaml, this ConfigurationPatchApplier.apply only checks against overlay and doesn't detect conflict. storeOverlay call below stores it. Then baseSnapshot.overlay call below detects conflict with the base, prints warning and returns config without the conflicting option to the caller. The same thing happens in subsequent calls as well.
Also, returned config should be same as stored config, which is not true in this case.
There was a problem hiding this comment.
Good to have test case for the same
There was a problem hiding this comment.
Good catch! Addressed in 17fb889.
apply() now checks boolean JVM opt conflicts against the effective config (base + overlay), not just the overlay, and rejects the patch before storeOverlay is called. Stored and returned configs no longer diverge, and the warn-and-drop path in baseSnapshot.overlay() is no longer hit.
Tests: ConfigurationManagerTest.testPatchConflictingBooleanJvmOptRejected (rejected, nothing stored) and testPatchReturnedConfigMatchesStoredConfig (returned == re-read).
| "^-(D[a-zA-Z][a-zA-Z0-9._-]*|X[a-z][a-zA-Z0-9]*|XX:[+-]?[a-zA-Z][a-zA-Z0-9_]*)$"); | ||
|
|
||
| // XX flags that accept arbitrary commands as values | ||
| static final Set<String> BLOCKED_XX_FLAGS = Set.of( |
There was a problem hiding this comment.
There are more options which can take paths etc e.g. -XX:ErrorFile, -XX:HeapDumpPath, -XX:LogFile, -Xloggc, and -D-style system properties. All absolute paths are allowed. Good to have allowlist instead of blocklist to be specific about which options are allowed and which paths are allowed.
There was a problem hiding this comment.
Addressed in 5c67693 by expanding the blocklist to cover path-bearing/file-writing options (-XX:ErrorFile, -XX:HeapDumpPath, -XX:LogFile, -XX:FlightRecorderOptions, -XX:StartFlightRecording, -Xloggc, -Xlog, -Xbootclasspath) alongside the existing command-executing -XX:OnError/-XX:OnOutOfMemoryError.
I honestly don't know the full set of valid JVM configs, and an allowlist would mean enumerating every legitimate Cassandra JVM option (all the -Xms/-Xmx/-Xss/-XX GC and heap tuning flags plus the whole -Dcassandra.* / -Dcom.sun.management.* / -Djava.* system-property surface), which would make the list massive and hard to keep current. This isn't a security issue in practice: the patch endpoint requires admin credentials, so we're just removing the obviously insecure options and can add more as we find them. If you feel strongly about an allowlist, I'm happy to switch to one.
| { | ||
| if (actual instanceof JsonObject && expected instanceof Map) | ||
| { | ||
| return actual.equals(new JsonObject((Map<String, Object>) expected)); |
There was a problem hiding this comment.
As we anyhow converting expected to JsonObject, can we expect the caller to pass both actual and expected as JsonObject, so we don't have to worry if passed params are not exactly matching instanceof expectations?
There was a problem hiding this comment.
Addressed in d973ec4. The patch handler builds each operation's value via JsonObject.getValue() (from context.body().asJsonArray()), which returns Vert.x JSON types - JsonObject for objects, JsonArray for arrays, or scalars - the same types resolveValue returns for the effective config. So both sides are already comparable and the TEST precondition is now a plain Objects.equals with no instanceof/conversion. Added coverage for object- and array-valued TEST operations.
|
Well structured code ! |
…ffective config Previously ConfigurationPatchApplier only checked boolean JVM option conflicts against the overlay, so a patch adding e.g. -XX:-UseG1GC while the base template already set -XX:+UseG1GC was not rejected. The overlay was stored, then baseSnapshot.overlay() detected the conflict on read, logged a warning, and returned the effective config without the option - leaving the stored overlay and the returned config inconsistent, and repeating on every subsequent call. Conflict detection now runs against a projection of the effective options after the patch is applied, rejecting the patch before anything is stored. Adds coverage in ConfigurationPatchApplierTest and ConfigurationManagerTest, including a stored-equals-returned assertion.
…xtraJvmOpts The extraJvmOpts value pattern permits absolute paths because Cassandra system properties legitimately need them, so JVM options that write to arbitrary filesystem paths must be blocked by key. Expands the blocklist (renamed BLOCKED_XX_FLAGS -> BLOCKED_JVM_OPTS) to cover -XX:ErrorFile, -XX:HeapDumpPath, -XX:LogFile, -XX:FlightRecorderOptions, -XX:StartFlightRecording, -Xloggc, -Xlog and -Xbootclasspath alongside the existing command-executing -XX:OnError / -XX:OnOutOfMemoryError.
…Applier Removes valueEquals(), which used instanceof checks to bridge a JsonObject actual against a Map expected. The patch request handler builds each operation's value via JsonObject.getValue(), which returns Vert.x JSON types (JsonObject for objects, JsonArray for arrays, or scalars) - the same types resolveValue returns for the effective config. Both sides are therefore already comparable, so the TEST precondition is now a plain Objects.equals with no conversion. Adds coverage for object- and array-valued TEST operations.
Summary
Implements patch operations for the Configuration Manager using RFC 6902-inspired semantics. Paths reference the effective configuration structure (base template + overlay merged) but mutations target the overlay only. Supported operations:
add,remove,replace,test.moveandcopyare intentionally excluded as they introduce ambiguous semantics in the two-tier configuration model.Configuration Model
The configuration uses a two-tier model:
cassandra.yamlprovided by the operator, read-only via this API.ConfigurationProvider.Patch operations target paths in the effective config (what the client sees from GET) but only mutate the overlay. The base template is never modified by this API.
Overview
Patch Operations
addremovereplacetestCopy-Siblings Strategy (nested paths)
When a patch targets a nested path within a top-level
cassandraYamlkey, the entire top-level key value is copied from the effective config into the overlay before applying the leaf change. This ensures the overlay is always self-contained at the top-level key granularity, preventing orphan fragments if the base template later removes the parent structure.Example:
Template:
{"memtable": {"configurations": {"trie": {"class_name": "TrieMemtable", "max_shard_count": 4}, "skiplist": {"class_name": "SkipListMemtable"}}}}Patch:
replace /configuration/cassandraYaml/memtable/configurations/trie/max_shard_countwith value8Resulting overlay (entire
memtablekey stored, not just the leaf):{"memtable": {"configurations": {"trie": {"class_name": "TrieMemtable", "max_shard_count": 8}, "skiplist": {"class_name": "SkipListMemtable"}}}}The trade-off is that sibling fields are "pinned" in the overlay. If the base template later modifies
skiplist, the overlay's copy takes precedence. Operators canremovethe sibling path from the overlay to re-sync with the template.Main Classes
ConfigurationPatchOperationOpenum (ADD, REMOVE, REPLACE, TEST) + path + valueConfigurationPatchValidatorConfigurationPatchApplierConfigurationPatchExceptionConfigurationConflictExceptionConfigurationManager.patchConfigurationValidation Flow
ConfigurationPatchValidator): path prefix, segment parsing, value presence, duplicate mutation pathsConfigurationManager): caller'sexpectedHashvs current effective config hashConfigurationPatchApplierphase 1): per-op checks against effective config and overlayConfigurationPatchApplierphase 2): only if all preconditions passAdditional Validations
-D,-X, or-XX:-XX:+Fooand-XX:-Foocannot coexist (remove first, then add)extraJvmOptspaths must be flat: no nested segments allowedTest Overview
ConfigurationPatchValidatorTestConfigurationPatchApplierTestConfigurationManagerTest