From d019de1814fdb558f91d29a2323c030779df73a1 Mon Sep 17 00:00:00 2001 From: bzp2010 Date: Mon, 13 Jul 2026 03:01:26 +0800 Subject: [PATCH 1/3] feat(core): inject managed by label --- apps/cli/src/command/helper.ts | 6 ++ apps/cli/src/command/typing.d.ts | 1 + apps/cli/src/server/schema.ts | 1 + libs/backend-api7/src/index.ts | 4 + libs/backend-apisix-standalone/src/index.ts | 4 + libs/backend-apisix/src/index.ts | 4 + libs/sdk/src/backend/index.ts | 11 ++- libs/sdk/src/backend/utils.spec.ts | 81 +++++++++++++++++++++ libs/sdk/src/backend/utils.ts | 34 +++++++++ libs/sdk/tsconfig.json | 9 ++- libs/sdk/tsconfig.spec.json | 6 +- 11 files changed, 158 insertions(+), 3 deletions(-) create mode 100644 libs/sdk/src/backend/utils.spec.ts diff --git a/apps/cli/src/command/helper.ts b/apps/cli/src/command/helper.ts index 0862d07c..8ea2ed8b 100644 --- a/apps/cli/src/command/helper.ts +++ b/apps/cli/src/command/helper.ts @@ -207,6 +207,12 @@ export class BackendCommand< ) .env('ADC_TLS_SKIP_VERIFY') .default(false), + ) + .addOption( + new Option( + '--no-managed-by-label', + 'disable injecting the "managed-by=adc" label into synced resources', + ), ); } } diff --git a/apps/cli/src/command/typing.d.ts b/apps/cli/src/command/typing.d.ts index 636b7400..f6c1d738 100644 --- a/apps/cli/src/command/typing.d.ts +++ b/apps/cli/src/command/typing.d.ts @@ -16,6 +16,7 @@ export type BackendOptions = { labelSelector?: Record; includeResourceType?: Array; excludeResourceType?: Array; + managedByLabel?: boolean; } & GlobalOptions; export interface KVConfiguration { diff --git a/apps/cli/src/server/schema.ts b/apps/cli/src/server/schema.ts index ce37d8d6..ec6772b4 100644 --- a/apps/cli/src/server/schema.ts +++ b/apps/cli/src/server/schema.ts @@ -12,6 +12,7 @@ const SyncTask = z.strictObject({ labelSelector: z.record(z.string(), z.string()).optional(), cacheKey: z.string(), bypassCache: z.boolean().optional().default(false), + managedByLabel: z.boolean().optional().default(false), }), config: z.looseObject({}), }); diff --git a/libs/backend-api7/src/index.ts b/libs/backend-api7/src/index.ts index dd1db4c1..03386111 100644 --- a/libs/backend-api7/src/index.ts +++ b/libs/backend-api7/src/index.ts @@ -204,6 +204,10 @@ export class BackendAPI7 implements ADCSDK.Backend { events: Array, opts: ADCSDK.BackendSyncOptions = { exitOnFailure: true }, ) { + events = ADCSDK.backend.injectManagedByLabel( + events, + this.opts.managedByLabel ?? true, + ); return forkJoin([ from(this.version()), from(this.getGatewayGroupId()), diff --git a/libs/backend-apisix-standalone/src/index.ts b/libs/backend-apisix-standalone/src/index.ts index af385f7c..6bebdc66 100644 --- a/libs/backend-apisix-standalone/src/index.ts +++ b/libs/backend-apisix-standalone/src/index.ts @@ -155,6 +155,10 @@ export class BackendAPISIXStandalone implements ADCSDK.Backend { events: Array, opts: ADCSDK.BackendSyncOptions = { exitOnFailure: true }, ): Observable { + events = ADCSDK.backend.injectManagedByLabel( + events, + this.opts.managedByLabel ?? true, + ); return from(this.version()).pipe( switchMap((version) => { return new Operator({ diff --git a/libs/backend-apisix/src/index.ts b/libs/backend-apisix/src/index.ts index d89af8b9..3f4adfa3 100644 --- a/libs/backend-apisix/src/index.ts +++ b/libs/backend-apisix/src/index.ts @@ -87,6 +87,10 @@ export class BackendAPISIX implements ADCSDK.Backend { events: Array, opts: ADCSDK.BackendSyncOptions = { exitOnFailure: true }, ): Observable { + events = ADCSDK.backend.injectManagedByLabel( + events, + this.opts.managedByLabel ?? true, + ); return forkJoin([from(this.version()), from(this.defaultValue())]).pipe( switchMap(([version]) => { return new Operator({ diff --git a/libs/sdk/src/backend/index.ts b/libs/sdk/src/backend/index.ts index 634f618e..16a1f31a 100644 --- a/libs/sdk/src/backend/index.ts +++ b/libs/sdk/src/backend/index.ts @@ -5,7 +5,12 @@ import { Observable, Subscription } from 'rxjs'; import { SemVer } from 'semver'; import * as ADCSDK from '..'; -import { BackendEventSource } from './utils'; +import { + BackendEventSource, + MANAGED_BY_LABEL_KEY, + MANAGED_BY_LABEL_VALUE, + injectManagedByLabel, +} from './utils'; export interface BackendOptions { server: string; @@ -21,6 +26,7 @@ export interface BackendOptions { labelSelector?: Record; includeResourceType?: Array; excludeResourceType?: Array; + managedByLabel?: boolean; cacheKey: string; httpAgent: httpAgent; @@ -121,4 +127,7 @@ export interface Backend { export default Backend; export const backend = { BackendEventSource, + injectManagedByLabel, + MANAGED_BY_LABEL_KEY, + MANAGED_BY_LABEL_VALUE, }; diff --git a/libs/sdk/src/backend/utils.spec.ts b/libs/sdk/src/backend/utils.spec.ts new file mode 100644 index 00000000..0efd0508 --- /dev/null +++ b/libs/sdk/src/backend/utils.spec.ts @@ -0,0 +1,81 @@ +import { EventType, ResourceType } from '../core'; +import type { Event } from '../core'; +import { + MANAGED_BY_LABEL_KEY, + MANAGED_BY_LABEL_VALUE, + injectManagedByLabel, +} from './utils'; + +const buildEvent = (overrides: Partial): Event => ({ + type: EventType.CREATE, + resourceId: 'id', + resourceName: 'name', + resourceType: ResourceType.ROUTE, + newValue: { name: 'name' } as Event['newValue'], + ...overrides, +}); + +describe('injectManagedByLabel', () => { + it('merges managed-by=adc into newValue.labels on CREATE events', () => { + const event = buildEvent({ type: EventType.CREATE }); + const [result] = injectManagedByLabel([event]); + expect(result.newValue?.labels).toEqual({ + [MANAGED_BY_LABEL_KEY]: MANAGED_BY_LABEL_VALUE, + }); + }); + + it('merges managed-by=adc into newValue.labels on UPDATE events without dropping existing labels', () => { + const event = buildEvent({ + type: EventType.UPDATE, + newValue: { name: 'name', labels: { team: 'a' } } as Event['newValue'], + }); + const [result] = injectManagedByLabel([event]); + expect(result.newValue?.labels).toEqual({ + team: 'a', + [MANAGED_BY_LABEL_KEY]: MANAGED_BY_LABEL_VALUE, + }); + }); + + it('does not modify DELETE events', () => { + const event = buildEvent({ + type: EventType.DELETE, + newValue: undefined, + oldValue: { name: 'name' } as Event['oldValue'], + }); + const [result] = injectManagedByLabel([event]); + expect(result.newValue).toBeUndefined(); + }); + + it('skips resource types without a labels field', () => { + const globalRuleEvent = buildEvent({ + resourceType: ResourceType.GLOBAL_RULE, + newValue: { 'limit-count': { count: 1 } } as unknown as Event['newValue'], + }); + const pluginMetadataEvent = buildEvent({ + resourceType: ResourceType.PLUGIN_METADATA, + newValue: { log_format: {} } as unknown as Event['newValue'], + }); + const [result1, result2] = injectManagedByLabel([ + globalRuleEvent, + pluginMetadataEvent, + ]); + expect(result1.newValue).not.toHaveProperty('labels'); + expect(result2.newValue).not.toHaveProperty('labels'); + }); + + it('returns events unchanged when disabled', () => { + const event = buildEvent({ type: EventType.CREATE }); + const [result] = injectManagedByLabel([event], false); + expect(result.newValue?.labels).toBeUndefined(); + }); + + it('does not touch the event.diff patch', () => { + const diff = [{ path: ['name'], type: 'MODIFIED' as const }]; + const event = buildEvent({ + type: EventType.UPDATE, + diff: diff as unknown as Event['diff'], + }); + const [result] = injectManagedByLabel([event]); + expect(result.diff).toBe(diff); + }); +}); diff --git a/libs/sdk/src/backend/utils.ts b/libs/sdk/src/backend/utils.ts index 24ddcb9a..0fcd8dbb 100644 --- a/libs/sdk/src/backend/utils.ts +++ b/libs/sdk/src/backend/utils.ts @@ -4,6 +4,40 @@ import { Subject } from 'rxjs'; import * as ADCSDK from '..'; +export const MANAGED_BY_LABEL_KEY = 'managed-by'; +export const MANAGED_BY_LABEL_VALUE = 'adc'; + +// Must run after the differ has already produced event.diff — mutating labels +// here never feeds back into the diff patch, otherwise every resource would +// show a spurious label change on the first sync (remote doesn't have it yet). +export const injectManagedByLabel = ( + events: Array, + enabled = true, +): Array => { + if (!enabled) return events; + // GLOBAL_RULE / PLUGIN_METADATA have no `labels` field in their schema, skip them. + // Built lazily (not at module scope) to avoid resolving ADCSDK.ResourceType + // before the circular `..` barrel import has finished initializing. + const unlabelableResourceTypes = new Set([ + ADCSDK.ResourceType.GLOBAL_RULE, + ADCSDK.ResourceType.PLUGIN_METADATA, + ]); + events.forEach((event) => { + if ( + (event.type !== ADCSDK.EventType.CREATE && + event.type !== ADCSDK.EventType.UPDATE) || + !event.newValue || + unlabelableResourceTypes.has(event.resourceType) + ) + return; + (event.newValue as { labels?: ADCSDK.Labels }).labels = { + ...(event.newValue as { labels?: ADCSDK.Labels }).labels, + [MANAGED_BY_LABEL_KEY]: MANAGED_BY_LABEL_VALUE, + }; + }); + return events; +}; + export class BackendEventSource { protected subject!: Subject; private _innerSubject: Subject<{ diff --git a/libs/sdk/tsconfig.json b/libs/sdk/tsconfig.json index dc7dc215..62ebbd94 100644 --- a/libs/sdk/tsconfig.json +++ b/libs/sdk/tsconfig.json @@ -2,5 +2,12 @@ "extends": "../../tsconfig.base.json", "files": [], "include": [], - "references": [] + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] } diff --git a/libs/sdk/tsconfig.spec.json b/libs/sdk/tsconfig.spec.json index aa4a5fa4..779b9d2f 100644 --- a/libs/sdk/tsconfig.spec.json +++ b/libs/sdk/tsconfig.spec.json @@ -30,5 +30,9 @@ "src/**/*.spec.jsx", "src/**/*.d.ts" ], - "references": [] + "references": [ + { + "path": "./tsconfig.lib.json" + } + ] } From c3e27a86cba557064c8b4ab2ead7c99d46a845b4 Mon Sep 17 00:00:00 2001 From: bzp2010 Date: Mon, 13 Jul 2026 12:00:42 +0800 Subject: [PATCH 2/3] move to inject directly --- apps/cli/src/command/diff.command.ts | 1 + apps/cli/src/command/ingress-sync.command.ts | 1 + apps/cli/src/command/lint.command.ts | 5 +- apps/cli/src/command/sync.command.ts | 1 + apps/cli/src/command/utils.spec.ts | 27 +++++++ apps/cli/src/command/utils.ts | 6 ++ apps/cli/src/command/validate.command.ts | 1 + apps/cli/src/server/schema.ts | 1 - apps/cli/src/tasks/load_local.ts | 15 +++- libs/backend-api7/src/index.ts | 4 - libs/backend-apisix-standalone/src/index.ts | 4 - libs/backend-apisix/src/index.ts | 4 - libs/sdk/src/backend/index.ts | 11 +-- libs/sdk/src/backend/utils.spec.ts | 81 -------------------- libs/sdk/src/backend/utils.ts | 34 -------- 15 files changed, 55 insertions(+), 141 deletions(-) delete mode 100644 libs/sdk/src/backend/utils.spec.ts diff --git a/apps/cli/src/command/diff.command.ts b/apps/cli/src/command/diff.command.ts index 788102e7..e06651d9 100644 --- a/apps/cli/src/command/diff.command.ts +++ b/apps/cli/src/command/diff.command.ts @@ -69,6 +69,7 @@ export const DiffCommand = new BackendCommand( opts.labelSelector, opts.includeResourceType, opts.excludeResourceType, + opts.managedByLabel, ), opts.lint ? LintTask() : { task: () => undefined }, !opts.remoteStateFile diff --git a/apps/cli/src/command/ingress-sync.command.ts b/apps/cli/src/command/ingress-sync.command.ts index 27992830..9344f22d 100644 --- a/apps/cli/src/command/ingress-sync.command.ts +++ b/apps/cli/src/command/ingress-sync.command.ts @@ -31,6 +31,7 @@ export const IngressSyncCommand = new BackendCommand('sync') opts.labelSelector, opts.includeResourceType, opts.excludeResourceType, + opts.managedByLabel, ), LintTask(), !opts.remoteStateFile diff --git a/apps/cli/src/command/lint.command.ts b/apps/cli/src/command/lint.command.ts index ca7f1d16..54582ea3 100644 --- a/apps/cli/src/command/lint.command.ts +++ b/apps/cli/src/command/lint.command.ts @@ -28,7 +28,10 @@ export const LintCommand = new BaseCommand('lint') const opts = LintCommand.optsWithGlobals(); const tasks = new Listr( - [LoadLocalConfigurationTask(opts.file, {}), LintTask()], + [ + LoadLocalConfigurationTask(opts.file, {}, undefined, undefined, false), + LintTask(), + ], { renderer: SignaleRenderer, rendererOptions: { verbose: opts.verbose }, diff --git a/apps/cli/src/command/sync.command.ts b/apps/cli/src/command/sync.command.ts index 2ed80eca..4a417ccd 100644 --- a/apps/cli/src/command/sync.command.ts +++ b/apps/cli/src/command/sync.command.ts @@ -85,6 +85,7 @@ export const SyncCommand = new BackendCommand( opts.labelSelector, opts.includeResourceType, opts.excludeResourceType, + opts.managedByLabel, ), opts.lint ? LintTask() : { task: () => undefined }, !opts.remoteStateFile diff --git a/apps/cli/src/command/utils.spec.ts b/apps/cli/src/command/utils.spec.ts index 5e3e5c1c..2a9636e3 100644 --- a/apps/cli/src/command/utils.spec.ts +++ b/apps/cli/src/command/utils.spec.ts @@ -1,6 +1,8 @@ import * as ADCSDK from '@api7/adc-sdk'; import { + MANAGED_BY_LABEL_KEY, + MANAGED_BY_LABEL_VALUE, fillLabels, recursiveRemoveIdField, recursiveReplaceEnvVars, @@ -254,6 +256,31 @@ describe('CLI utils', () => { }); }); + it('should label local resources as managed by ADC, the same way fillLabels does for --label-selector', () => { + expect( + fillLabels( + { + services: [{ name: 'Test Service' }], + consumers: [{ username: 'alice', labels: { team: 'a' } }], + } as ADCSDK.Configuration, + { [MANAGED_BY_LABEL_KEY]: MANAGED_BY_LABEL_VALUE }, + ), + ).toEqual({ + services: [ + { + name: 'Test Service', + labels: { [MANAGED_BY_LABEL_KEY]: MANAGED_BY_LABEL_VALUE }, + }, + ], + consumers: [ + { + username: 'alice', + labels: { team: 'a', [MANAGED_BY_LABEL_KEY]: MANAGED_BY_LABEL_VALUE }, + }, + ], + }); + }); + describe('Environment Variables', () => { it('mock config', () => { const config: ADCSDK.Configuration = { diff --git a/apps/cli/src/command/utils.ts b/apps/cli/src/command/utils.ts index fca4b9b8..a797c38b 100644 --- a/apps/cli/src/command/utils.ts +++ b/apps/cli/src/command/utils.ts @@ -200,6 +200,12 @@ export const fillLabels = ( return configuration; }; +// Injected into local resources the same way --label-selector's fillLabels() +// works, so it flows into every CREATE/UPDATE payload naturally without any +// special-casing in the differ or the backend. +export const MANAGED_BY_LABEL_KEY = 'managed-by'; +export const MANAGED_BY_LABEL_VALUE = 'adc'; + export const filterResourceType = ( config: ADCSDK.Configuration = {}, includes?: Array, diff --git a/apps/cli/src/command/validate.command.ts b/apps/cli/src/command/validate.command.ts index 4e9c3b3f..65bc9857 100644 --- a/apps/cli/src/command/validate.command.ts +++ b/apps/cli/src/command/validate.command.ts @@ -56,6 +56,7 @@ export const ValidateCommand = new BackendCommand( opts.labelSelector, opts.includeResourceType, opts.excludeResourceType, + opts.managedByLabel, ), opts.lint ? LintTask() : { task: () => undefined }, DiffResourceTask(), diff --git a/apps/cli/src/server/schema.ts b/apps/cli/src/server/schema.ts index ec6772b4..ce37d8d6 100644 --- a/apps/cli/src/server/schema.ts +++ b/apps/cli/src/server/schema.ts @@ -12,7 +12,6 @@ const SyncTask = z.strictObject({ labelSelector: z.record(z.string(), z.string()).optional(), cacheKey: z.string(), bypassCache: z.boolean().optional().default(false), - managedByLabel: z.boolean().optional().default(false), }), config: z.looseObject({}), }); diff --git a/apps/cli/src/tasks/load_local.ts b/apps/cli/src/tasks/load_local.ts index 93ece411..9294e33d 100644 --- a/apps/cli/src/tasks/load_local.ts +++ b/apps/cli/src/tasks/load_local.ts @@ -7,8 +7,9 @@ import { ListrTask } from 'listr2'; import path from 'node:path'; import type { TaskContext } from '../command/diff.command'; - import { + MANAGED_BY_LABEL_KEY, + MANAGED_BY_LABEL_VALUE, fillLabels, filterResourceType, mergeKVConfigurations, @@ -22,6 +23,7 @@ export const LoadLocalConfigurationTask = ( labelSelector?: ADCSDK.BackendOptions['labelSelector'], includeResourceType?: Array, excludeResourceType?: Array, + managedByLabel = true, ): ListrTask => ({ title: `Load local configuration`, task: async (ctx, task) => { @@ -87,7 +89,8 @@ export const LoadLocalConfigurationTask = ( { title: 'Filter configuration resource type', enabled: () => - (includeResourceType?.length ?? 0) > 0 || (excludeResourceType?.length ?? 0) > 0, + (includeResourceType?.length ?? 0) > 0 || + (excludeResourceType?.length ?? 0) > 0, task: () => { ctx.local = filterResourceType( ctx.local, @@ -104,6 +107,14 @@ export const LoadLocalConfigurationTask = ( fillLabels(ctx.local, labelSelector); }, }, + { + enabled: managedByLabel, + task: async () => { + fillLabels(ctx.local, { + [MANAGED_BY_LABEL_KEY]: MANAGED_BY_LABEL_VALUE, + }); + }, + }, ], { ctx: subCtx, diff --git a/libs/backend-api7/src/index.ts b/libs/backend-api7/src/index.ts index 03386111..dd1db4c1 100644 --- a/libs/backend-api7/src/index.ts +++ b/libs/backend-api7/src/index.ts @@ -204,10 +204,6 @@ export class BackendAPI7 implements ADCSDK.Backend { events: Array, opts: ADCSDK.BackendSyncOptions = { exitOnFailure: true }, ) { - events = ADCSDK.backend.injectManagedByLabel( - events, - this.opts.managedByLabel ?? true, - ); return forkJoin([ from(this.version()), from(this.getGatewayGroupId()), diff --git a/libs/backend-apisix-standalone/src/index.ts b/libs/backend-apisix-standalone/src/index.ts index 6bebdc66..af385f7c 100644 --- a/libs/backend-apisix-standalone/src/index.ts +++ b/libs/backend-apisix-standalone/src/index.ts @@ -155,10 +155,6 @@ export class BackendAPISIXStandalone implements ADCSDK.Backend { events: Array, opts: ADCSDK.BackendSyncOptions = { exitOnFailure: true }, ): Observable { - events = ADCSDK.backend.injectManagedByLabel( - events, - this.opts.managedByLabel ?? true, - ); return from(this.version()).pipe( switchMap((version) => { return new Operator({ diff --git a/libs/backend-apisix/src/index.ts b/libs/backend-apisix/src/index.ts index 3f4adfa3..d89af8b9 100644 --- a/libs/backend-apisix/src/index.ts +++ b/libs/backend-apisix/src/index.ts @@ -87,10 +87,6 @@ export class BackendAPISIX implements ADCSDK.Backend { events: Array, opts: ADCSDK.BackendSyncOptions = { exitOnFailure: true }, ): Observable { - events = ADCSDK.backend.injectManagedByLabel( - events, - this.opts.managedByLabel ?? true, - ); return forkJoin([from(this.version()), from(this.defaultValue())]).pipe( switchMap(([version]) => { return new Operator({ diff --git a/libs/sdk/src/backend/index.ts b/libs/sdk/src/backend/index.ts index 16a1f31a..634f618e 100644 --- a/libs/sdk/src/backend/index.ts +++ b/libs/sdk/src/backend/index.ts @@ -5,12 +5,7 @@ import { Observable, Subscription } from 'rxjs'; import { SemVer } from 'semver'; import * as ADCSDK from '..'; -import { - BackendEventSource, - MANAGED_BY_LABEL_KEY, - MANAGED_BY_LABEL_VALUE, - injectManagedByLabel, -} from './utils'; +import { BackendEventSource } from './utils'; export interface BackendOptions { server: string; @@ -26,7 +21,6 @@ export interface BackendOptions { labelSelector?: Record; includeResourceType?: Array; excludeResourceType?: Array; - managedByLabel?: boolean; cacheKey: string; httpAgent: httpAgent; @@ -127,7 +121,4 @@ export interface Backend { export default Backend; export const backend = { BackendEventSource, - injectManagedByLabel, - MANAGED_BY_LABEL_KEY, - MANAGED_BY_LABEL_VALUE, }; diff --git a/libs/sdk/src/backend/utils.spec.ts b/libs/sdk/src/backend/utils.spec.ts deleted file mode 100644 index 0efd0508..00000000 --- a/libs/sdk/src/backend/utils.spec.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { EventType, ResourceType } from '../core'; -import type { Event } from '../core'; -import { - MANAGED_BY_LABEL_KEY, - MANAGED_BY_LABEL_VALUE, - injectManagedByLabel, -} from './utils'; - -const buildEvent = (overrides: Partial): Event => ({ - type: EventType.CREATE, - resourceId: 'id', - resourceName: 'name', - resourceType: ResourceType.ROUTE, - newValue: { name: 'name' } as Event['newValue'], - ...overrides, -}); - -describe('injectManagedByLabel', () => { - it('merges managed-by=adc into newValue.labels on CREATE events', () => { - const event = buildEvent({ type: EventType.CREATE }); - const [result] = injectManagedByLabel([event]); - expect(result.newValue?.labels).toEqual({ - [MANAGED_BY_LABEL_KEY]: MANAGED_BY_LABEL_VALUE, - }); - }); - - it('merges managed-by=adc into newValue.labels on UPDATE events without dropping existing labels', () => { - const event = buildEvent({ - type: EventType.UPDATE, - newValue: { name: 'name', labels: { team: 'a' } } as Event['newValue'], - }); - const [result] = injectManagedByLabel([event]); - expect(result.newValue?.labels).toEqual({ - team: 'a', - [MANAGED_BY_LABEL_KEY]: MANAGED_BY_LABEL_VALUE, - }); - }); - - it('does not modify DELETE events', () => { - const event = buildEvent({ - type: EventType.DELETE, - newValue: undefined, - oldValue: { name: 'name' } as Event['oldValue'], - }); - const [result] = injectManagedByLabel([event]); - expect(result.newValue).toBeUndefined(); - }); - - it('skips resource types without a labels field', () => { - const globalRuleEvent = buildEvent({ - resourceType: ResourceType.GLOBAL_RULE, - newValue: { 'limit-count': { count: 1 } } as unknown as Event['newValue'], - }); - const pluginMetadataEvent = buildEvent({ - resourceType: ResourceType.PLUGIN_METADATA, - newValue: { log_format: {} } as unknown as Event['newValue'], - }); - const [result1, result2] = injectManagedByLabel([ - globalRuleEvent, - pluginMetadataEvent, - ]); - expect(result1.newValue).not.toHaveProperty('labels'); - expect(result2.newValue).not.toHaveProperty('labels'); - }); - - it('returns events unchanged when disabled', () => { - const event = buildEvent({ type: EventType.CREATE }); - const [result] = injectManagedByLabel([event], false); - expect(result.newValue?.labels).toBeUndefined(); - }); - - it('does not touch the event.diff patch', () => { - const diff = [{ path: ['name'], type: 'MODIFIED' as const }]; - const event = buildEvent({ - type: EventType.UPDATE, - diff: diff as unknown as Event['diff'], - }); - const [result] = injectManagedByLabel([event]); - expect(result.diff).toBe(diff); - }); -}); diff --git a/libs/sdk/src/backend/utils.ts b/libs/sdk/src/backend/utils.ts index 0fcd8dbb..24ddcb9a 100644 --- a/libs/sdk/src/backend/utils.ts +++ b/libs/sdk/src/backend/utils.ts @@ -4,40 +4,6 @@ import { Subject } from 'rxjs'; import * as ADCSDK from '..'; -export const MANAGED_BY_LABEL_KEY = 'managed-by'; -export const MANAGED_BY_LABEL_VALUE = 'adc'; - -// Must run after the differ has already produced event.diff — mutating labels -// here never feeds back into the diff patch, otherwise every resource would -// show a spurious label change on the first sync (remote doesn't have it yet). -export const injectManagedByLabel = ( - events: Array, - enabled = true, -): Array => { - if (!enabled) return events; - // GLOBAL_RULE / PLUGIN_METADATA have no `labels` field in their schema, skip them. - // Built lazily (not at module scope) to avoid resolving ADCSDK.ResourceType - // before the circular `..` barrel import has finished initializing. - const unlabelableResourceTypes = new Set([ - ADCSDK.ResourceType.GLOBAL_RULE, - ADCSDK.ResourceType.PLUGIN_METADATA, - ]); - events.forEach((event) => { - if ( - (event.type !== ADCSDK.EventType.CREATE && - event.type !== ADCSDK.EventType.UPDATE) || - !event.newValue || - unlabelableResourceTypes.has(event.resourceType) - ) - return; - (event.newValue as { labels?: ADCSDK.Labels }).labels = { - ...(event.newValue as { labels?: ADCSDK.Labels }).labels, - [MANAGED_BY_LABEL_KEY]: MANAGED_BY_LABEL_VALUE, - }; - }); - return events; -}; - export class BackendEventSource { protected subject!: Subject; private _innerSubject: Subject<{ From 18c2c15635aeadfd4d23ecdd763261b7d0b02081 Mon Sep 17 00:00:00 2001 From: bzp2010 Date: Mon, 13 Jul 2026 12:25:27 +0800 Subject: [PATCH 3/3] fix comment --- apps/cli/src/command/utils.spec.ts | 91 +++++++++++++++++++++++++++++- 1 file changed, 90 insertions(+), 1 deletion(-) diff --git a/apps/cli/src/command/utils.spec.ts b/apps/cli/src/command/utils.spec.ts index 2a9636e3..ccd7d1fa 100644 --- a/apps/cli/src/command/utils.spec.ts +++ b/apps/cli/src/command/utils.spec.ts @@ -260,7 +260,22 @@ describe('CLI utils', () => { expect( fillLabels( { - services: [{ name: 'Test Service' }], + services: [ + { + name: 'Test Service', + routes: [ + { + name: 'Test Nested Route', + uris: ['/test-nested'], + }, + ], + stream_routes: [ + { + name: 'Test Nested Stream Route', + }, + ], + }, + ], consumers: [{ username: 'alice', labels: { team: 'a' } }], } as ADCSDK.Configuration, { [MANAGED_BY_LABEL_KEY]: MANAGED_BY_LABEL_VALUE }, @@ -270,6 +285,19 @@ describe('CLI utils', () => { { name: 'Test Service', labels: { [MANAGED_BY_LABEL_KEY]: MANAGED_BY_LABEL_VALUE }, + routes: [ + { + name: 'Test Nested Route', + uris: ['/test-nested'], + labels: { [MANAGED_BY_LABEL_KEY]: MANAGED_BY_LABEL_VALUE }, + }, + ], + stream_routes: [ + { + name: 'Test Nested Stream Route', + labels: { [MANAGED_BY_LABEL_KEY]: MANAGED_BY_LABEL_VALUE }, + }, + ], }, ], consumers: [ @@ -281,6 +309,67 @@ describe('CLI utils', () => { }); }); + it('should overwrite an existing managed-by label with a different value, keeping other labels', () => { + expect( + fillLabels( + { + services: [ + { + name: 'Test Service', + labels: { team: 'a', [MANAGED_BY_LABEL_KEY]: 'something-else' }, + routes: [ + { + name: 'Test Nested Route', + uris: ['/test-nested'], + labels: { [MANAGED_BY_LABEL_KEY]: 'something-else' }, + }, + ], + stream_routes: [ + { + name: 'Test Nested Stream Route', + labels: { [MANAGED_BY_LABEL_KEY]: 'something-else' }, + }, + ], + }, + ], + consumers: [ + { + username: 'alice', + labels: { [MANAGED_BY_LABEL_KEY]: 'something-else' }, + }, + ], + } as ADCSDK.Configuration, + { [MANAGED_BY_LABEL_KEY]: MANAGED_BY_LABEL_VALUE }, + ), + ).toEqual({ + services: [ + { + name: 'Test Service', + labels: { team: 'a', [MANAGED_BY_LABEL_KEY]: MANAGED_BY_LABEL_VALUE }, + routes: [ + { + name: 'Test Nested Route', + uris: ['/test-nested'], + labels: { [MANAGED_BY_LABEL_KEY]: MANAGED_BY_LABEL_VALUE }, + }, + ], + stream_routes: [ + { + name: 'Test Nested Stream Route', + labels: { [MANAGED_BY_LABEL_KEY]: MANAGED_BY_LABEL_VALUE }, + }, + ], + }, + ], + consumers: [ + { + username: 'alice', + labels: { [MANAGED_BY_LABEL_KEY]: MANAGED_BY_LABEL_VALUE }, + }, + ], + }); + }); + describe('Environment Variables', () => { it('mock config', () => { const config: ADCSDK.Configuration = {