Skip to content

feat(differ): add feature gate for v4#498

Merged
bzp2010 merged 1 commit into
mainfrom
bzp/feat-differ-v4-feature-gate
Jun 23, 2026
Merged

feat(differ): add feature gate for v4#498
bzp2010 merged 1 commit into
mainfrom
bzp/feat-differ-v4-feature-gate

Conversation

@bzp2010

@bzp2010 bzp2010 commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

Description

Since we need to fix #489 and release a patch version for it, we need to prevent any potential implementation errors in v4 from affecting the patch.
Therefore, this PR introduces a feature gate for differ v4. In version 0.27.1, its behavior will be as follows: differ v3 will be enabled by default, and differ v4 will be enabled when the environment variable ENABLE_DIFFER_V4 is set to true or 1. In other words, it is disabled by default and must be explicitly enabled.

In future minor releases, this behavior will change to being enabled by default and disabled only when explicitly set. Differ v3 will be completely removed after several releases.

Checklist

  • I have explained the need for this PR and the problem it solves
  • I have explained the changes or the new features added to this PR
  • I have added tests corresponding to this change
  • I have updated the documentation to reflect this change
  • I have verified that this change is backward compatible

Summary by CodeRabbit

  • New Features

    • Added a new configuration diff engine that detects and tracks changes between local and remote configurations across multiple resource types, including services, SSL, consumers, routes, upstreams, and credentials.
    • Introduced runtime configuration to select between diff engine versions.
  • Tests

    • Expanded test coverage to validate functionality across multiple engine versions.

@bzp2010 bzp2010 requested review from LiteSun, guoqqqi and juzhiyuan June 23, 2026 08:03
@bzp2010 bzp2010 self-assigned this Jun 23, 2026
@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a full DifferV3 implementation to libs/differ/src/differv3.ts (705 lines) that compares local vs. remote ADCSDK.InternalConfiguration objects and emits sorted ADCSDK.Event arrays. Updates libs/differ/src/index.ts to export both DifferV3 and DifferV4 and select Differ via an ENABLE_DIFFER_V4 environment variable. Parameterizes all six existing test suites to run against both implementations.

Changes

DifferV3 Implementation and Test Coverage

Layer / File(s) Summary
DifferV3 helper utilities
libs/differ/src/differv3.ts
Defines the deterministic event-ordering order map (keyed by ${resourceType}.${eventType}), ADC_DIFFER_DEBUG flag, mergeDefault() for recursive default injection with prototype-pollution guards, diffPlugins() for plugin-level default merging and change detection, and postprocessSubEvent() for adjusting sub-event resourceId and injecting parentId.
diffResource() core diffing logic
libs/differ/src/differv3.ts
Matches remote vs. local resources by ID; emits DELETE events (with recursive sub-events for nested types like routes, credentials) for resources absent locally; emits CREATE events for resources absent remotely; normalizes SSL comparisons by stripping certificate private keys; merges schema defaults; diffs plugins separately and excludes them from main-object comparison; computes UPDATE or ONLY_SUB_EVENTS via objectDiff.
DifferV3 class and diff() orchestration
libs/differ/src/differv3.ts
Defines the DifferV3 class with constructor (transactionId, defaultValue, optional logger) and static diff(), which instantiates the differ with randomUUID, calls diffResource() for all supported resource types, unwraps nested sub-events from parents, sorts the flat event list using order, and returns the result with optional debug logging.
Module feature gate and exports
libs/differ/src/index.ts
Adds ENABLE_DIFFER_V4 boolean (true when env var is 'true' or '1'), exports DifferV3 and DifferV4 as named exports, and sets Differ to DifferV4 when gated, otherwise DifferV3.
Parameterized test suites for V3 and V4
libs/differ/src/test/*.spec.ts
Updates basic, consumer, custom-id, service-upstream, upstream, and usecase test specs to import DifferV3/DifferV4 and use describe.each so all existing assertions run against both implementations.

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant DifferV3
  participant diffResource
  participant diffPlugins
  participant mergeDefault
  participant postprocessSubEvent

  Caller->>DifferV3: diff(local, remote, defaultValue)
  DifferV3->>DifferV3: new DifferV3({ transactionId: randomUUID(), defaultValue })
  loop each ADCSDK.ResourceType
    DifferV3->>diffResource: diffResource(resourceType, local[type], remote[type])
    diffResource->>mergeDefault: merge defaults into local resource
    mergeDefault-->>diffResource: merged local object
    diffResource->>diffPlugins: diff plugins separately
    diffPlugins-->>diffResource: pluginsChanged + normalizedPlugins
    diffResource->>postprocessSubEvent: adjust sub-event resourceId + inject parentId
    postprocessSubEvent-->>diffResource: updated sub-event
    diffResource-->>DifferV3: DELETE / CREATE / UPDATE / ONLY_SUB_EVENTS events
  end
  DifferV3->>DifferV3: unwrap subEvents, sort by order map
  DifferV3-->>Caller: Array<ADCSDK.Event>
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 error, 1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Security Check ❌ Error Critical security vulnerabilities exist: DifferV3 logs full ADC configurations (containing consumer credentials, plugin configs with OAuth secrets, API keys, SSL private keys) without redaction at... Implement payload redaction before debug logging: strip sensitive fields (passwords, tokens, API keys, secrets) from configuration objects via a redactor function or custom MarshalJSON methods before passing to logger.debug().
E2e Test Quality Review ⚠️ Warning PR lacks E2E test coverage and feature gate tests; DifferV3 has unresolved code review issues (sensitive data logging, input mutations, missing GLOBAL_RULE defaults, incomplete event payloads); exi... Add tests for ENABLE_DIFFER_V4 env var feature gate; resolve code review comments on debug logging, input mutations, and event payloads; implement E2E tests validating actual diff behavior.
Linked Issues check ❓ Inconclusive The PR objective is to add a feature gate for DifferV4 to avoid v4 issues affecting a patch release. Issue #489 is about a global_rules bug in the standalone backend, unrelated to the differ feature gate itself. Clarify the relationship between the feature gate and issue #489. If the gate is a prerequisite to safely integrate v4, provide evidence or rationale. If unrelated, consider linking a v4-specific issue instead.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(differ): add feature gate for v4' accurately summarizes the main change—adding a feature gate to conditionally enable/disable DifferV4 at runtime.
Out of Scope Changes check ✅ Passed All changes are in scope: DifferV3 implementation, feature gate logic, and test parameterization. No unrelated refactoring or unscoped modifications detected.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch bzp/feat-differ-v4-feature-gate

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@libs/differ/src/differv3.ts`:
- Around line 93-102: The differ.logger?.debug call at this location logs
sensitive payloads (local, remote, defaultValue) that may contain auth keys,
OAuth secrets, credentials, and database connection strings without redaction.
Create or use an existing redactor function to sanitize these payloads before
passing them to the logger.debug call, removing or masking any sensitive fields.
Apply the same redaction pattern to all other debug logging calls in the file at
lines 226-229, 415-422, 483-495, and 586-596 where full configs, events, and
plugin configurations are being logged.
- Around line 251-259: The localItem retrieved from localIdMap is a caller-owned
object that should not be mutated by the normalization process. Clone localItem
using cloneDeep (similar to how remoteItem is already cloned at the beginning of
the remote forEach block) before calling unset operations on it to prevent
removing IDs and metadata that will be needed for subsequent syncs. Apply this
same pattern to all locations where localItem is retrieved and normalized,
including the areas around lines 316-323 and 535-540 as noted in the comment.
- Around line 131-147: The diffResource call for ADCSDK.ResourceType.GLOBAL_RULE
is missing plugin default value merging, which causes unchanged local rules to
appear as UPDATE operations when APISIX returns schema defaults. For both the
local and remote global_rules mappings in this diff block, merge the plugin
defaults from defaultValue.plugins[pluginName] into each pluginConfig before
they are passed to diffResource. Apply this same fix to the other similar
global_rules diff locations mentioned at lines 348-353 and 442-449 to ensure
defaults are consistently merged across all global_rule comparisons.
- Around line 263-298: The DELETE event being constructed includes nested
resources in the oldValue property (like routes, stream_routes, upstreams,
consumers, or credentials) which are also being sent as subEvents, causing
inconsistency with UPDATE events and duplicate field transmission. Strip these
nested resource properties from the oldValue when constructing the DELETE event
so that nested fields only appear in subEvents. Apply the same fix to CREATE
events in the location mentioned in the comment (around lines 541-575) where
newValue is set, ensuring both CREATE and DELETE events exclude their nested
resource properties from the parent payload.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4991379d-8e5c-4d13-a618-f2e669941e66

📥 Commits

Reviewing files that changed from the base of the PR and between ecb770a and a50acb5.

📒 Files selected for processing (8)
  • libs/differ/src/differv3.ts
  • libs/differ/src/index.ts
  • libs/differ/src/test/basic.spec.ts
  • libs/differ/src/test/consumer.spec.ts
  • libs/differ/src/test/custom-id.spec.ts
  • libs/differ/src/test/service-upstream.spec.ts
  • libs/differ/src/test/upstream.spec.ts
  • libs/differ/src/test/usecase.spec.ts

Comment thread libs/differ/src/differv3.ts
Comment on lines +131 to +147
...differ.diffResource(
ADCSDK.ResourceType.GLOBAL_RULE,
Object.entries(local?.global_rules ?? {}).map(
([pluginName, pluginConfig]) => [
pluginName,
pluginName,
pluginConfig as Record<string, unknown>,
],
) ?? [],
Object.entries(remote?.global_rules ?? {}).map(
([pluginName, pluginConfig]) => [
pluginName,
pluginName,
pluginConfig as Record<string, unknown>,
],
) ?? [],
),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Merge plugin defaults for GLOBAL_RULE configs.

global_rules are diffed as one resource per plugin name, but they bypass defaultValue.plugins[pluginName]. If APISIX returns schema defaults inside a global-rule plugin config, unchanged local rules can emit UPDATE on every sync.

Proposed direction
-      const defaultValue =
-        this.defaultValue?.core?.[resourceTypeForDefault] ?? {};
+      const defaultValue =
+        resourceType === ADCSDK.ResourceType.GLOBAL_RULE
+          ? this.defaultValue?.plugins?.[remoteName] ?? {}
+          : this.defaultValue?.core?.[resourceTypeForDefault] ?? {};
       const mergedLocalItem = this.mergeDefault(
         localItem,
         cloneDeep(defaultValue),
       );

Also applies to: 348-353, 442-449

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@libs/differ/src/differv3.ts` around lines 131 - 147, The diffResource call
for ADCSDK.ResourceType.GLOBAL_RULE is missing plugin default value merging,
which causes unchanged local rules to appear as UPDATE operations when APISIX
returns schema defaults. For both the local and remote global_rules mappings in
this diff block, merge the plugin defaults from defaultValue.plugins[pluginName]
into each pluginConfig before they are passed to diffResource. Apply this same
fix to the other similar global_rules diff locations mentioned at lines 348-353
and 442-449 to ensure defaults are consistently merged across all global_rule
comparisons.

Comment thread libs/differ/src/differv3.ts
Comment thread libs/differ/src/differv3.ts
@bzp2010 bzp2010 merged commit 289a33b into main Jun 23, 2026
32 checks passed
@bzp2010 bzp2010 deleted the bzp/feat-differ-v4-feature-gate branch June 23, 2026 09:32
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.

bug(backend-apisix-standalone): global_rules read path drops plugins → duplicate global_rule on resync → APISIX 400

2 participants