Skip to content

Commit eebc858

Browse files
committed
Adding information to json output
1 parent ef9983c commit eebc858

5 files changed

Lines changed: 309 additions & 29 deletions

File tree

src/cli/compare-command.ts

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -195,13 +195,16 @@ async function runCompare(
195195
logLevel: parseLogLevel(globalOpts.logLevel),
196196
};
197197

198-
logger.info('Starting comparison...');
199-
logger.info(
200-
` Source: ${sourceContext.serviceName} (${sourceContext.resourceGroup})`,
201-
);
202-
logger.info(
203-
` Target: ${targetContext.serviceName} (${targetContext.resourceGroup})`,
204-
);
198+
const shouldLogInfo = format !== 'json';
199+
if (shouldLogInfo) {
200+
logger.info('Starting comparison...');
201+
logger.info(
202+
` Source: ${sourceContext.serviceName} (${sourceContext.resourceGroup})`,
203+
);
204+
logger.info(
205+
` Target: ${targetContext.serviceName} (${targetContext.resourceGroup})`,
206+
);
207+
}
205208

206209
const result = await compareApimInstances(config);
207210

@@ -299,9 +302,12 @@ async function runLocalCompare(
299302
? await loadOverrideConfig(options.overrides)
300303
: undefined;
301304

302-
logger.info('Starting local artifact comparison...');
303-
logger.info(` Source: ${options.source}${overrides ? ' (with overrides)' : ''}`);
304-
logger.info(` Target: ${options.target}`);
305+
const shouldLogInfo = format !== 'json';
306+
if (shouldLogInfo) {
307+
logger.info('Starting local artifact comparison...');
308+
logger.info(` Source: ${options.source}${overrides ? ' (with overrides)' : ''}`);
309+
logger.info(` Target: ${options.target}`);
310+
}
305311

306312
const artifactStore = new ArtifactStore();
307313

src/lib/resource-uri.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,3 +110,28 @@ export function buildResourceLabel(descriptor: ResourceDescriptor): string {
110110
// armPathSuffix has no leading slash, so the result is already relative
111111
return formatTemplatePath(metadata.armPathSuffix, descriptor.nameParts);
112112
}
113+
114+
/**
115+
* Returns the relative ARM path for an APIM resource id, excluding the APIM
116+
* service prefix.
117+
*
118+
* Example:
119+
* /subscriptions/.../providers/Microsoft.ApiManagement/service/my-apim/namedValues/value1
120+
* → namedValues/value1
121+
*/
122+
export const getRelativeResourceId = (
123+
resourceId: string,
124+
serviceName: string,
125+
): string | undefined => {
126+
if (resourceId.length === 0 || serviceName.length === 0) {
127+
return undefined;
128+
}
129+
130+
const relativePrefix = `/providers/Microsoft.ApiManagement/service/${serviceName}/`;
131+
const prefixIndex = resourceId.indexOf(relativePrefix);
132+
if (prefixIndex < 0) {
133+
return undefined;
134+
}
135+
136+
return resourceId.substring(prefixIndex + relativePrefix.length).split('?')[0];
137+
};

src/services/compare-service.ts

Lines changed: 51 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,14 @@ import { RESOURCE_TYPE_METADATA, ResourceType } from '../models/resource-types.j
2424
import { ApimServiceContext, ResourceDescriptor } from '../models/types.js';
2525
import { CompareConfig, OverrideConfig } from '../models/config.js';
2626
import { LogLevel } from '../lib/logger.js';
27+
import { getRelativeResourceId } from '../lib/resource-uri.js';
2728
import { loadLocalArtifacts } from './local-artifact-loader.js';
2829

30+
const resolveRelativeResourceId = getRelativeResourceId;
31+
2932
export interface CompareResult {
33+
sourceResourceId?: string;
34+
targetResourceId?: string;
3035
totalDifferences: number;
3136
differences: ComparisonDifference[];
3237
}
@@ -36,25 +41,22 @@ export interface ComparisonDifference {
3641
resourceType: string;
3742
resourceName: string;
3843
displayName?: string;
44+
relativeResourceId?: string;
3945
diffType: 'missing' | 'extra' | 'property-diff';
4046
diffs?: ResourceDiff[];
4147
}
42-
43-
// Built-in resources to exclude from comparison
44-
const EXCLUDE_GROUPS = new Set(['administrators', 'developers', 'guests']);
45-
const EXCLUDE_PRODUCTS = new Set(['starter', 'unlimited']);
46-
const EXCLUDE_SUBSCRIPTIONS = new Set(['master']);
47-
const EXCLUDE_APIS = new Set(['echo-api']);
48-
4948
/**
5049
* Compares two APIM instances and returns differences
5150
*/
5251
export async function compareApimInstances(
5352
config: CompareConfig,
5453
): Promise<CompareResult> {
55-
logger.info(
56-
`Comparing ${config.source.serviceName}${config.target.serviceName}`,
57-
);
54+
const shouldLogInfo = config.format !== 'json';
55+
if (shouldLogInfo) {
56+
logger.info(
57+
`Comparing ${config.source.serviceName}${config.target.serviceName}`,
58+
);
59+
}
5860

5961
const normalizeContext: NormalizeContext = {
6062
sourceServiceName: config.source.serviceName,
@@ -83,14 +85,14 @@ export async function compareApimInstances(
8385
{ type: ResourceType.Gateway },
8486
{ type: ResourceType.VersionSet },
8587
{ type: ResourceType.Backend },
86-
{ type: ResourceType.Group, exclude: EXCLUDE_GROUPS },
88+
{ type: ResourceType.Group },
8789
{ type: ResourceType.PolicyFragment },
8890
{ type: ResourceType.GlobalSchema },
8991
{ type: ResourceType.Logger, skipLoggerCreds: true },
9092
{ type: ResourceType.Diagnostic },
9193
{ type: ResourceType.ServicePolicy },
92-
{ type: ResourceType.Product, exclude: EXCLUDE_PRODUCTS },
93-
{ type: ResourceType.Subscription, exclude: EXCLUDE_SUBSCRIPTIONS },
94+
{ type: ResourceType.Product },
95+
{ type: ResourceType.Subscription },
9496
// Note: Workspace types not yet defined in ResourceType enum
9597
// { type: ResourceType.Workspace },
9698
{ type: ResourceType.Documentation },
@@ -121,7 +123,6 @@ export async function compareApimInstances(
121123
normalizeContext,
122124
config.source,
123125
config.target,
124-
EXCLUDE_APIS,
125126
);
126127
differences.push(...apiDiffs);
127128

@@ -137,7 +138,7 @@ export async function compareApimInstances(
137138
if (typeof id !== 'string') return '';
138139
return extractResourceName(id);
139140
})
140-
.filter((name) => name && !EXCLUDE_APIS.has(name));
141+
.filter(Boolean);
141142

142143
// Compare API children
143144
for (const apiName of apiNames) {
@@ -225,7 +226,7 @@ export async function compareApimInstances(
225226
if (typeof id !== 'string') return '';
226227
return extractResourceName(id);
227228
})
228-
.filter((name) => name && !EXCLUDE_PRODUCTS.has(name));
229+
.filter(Boolean);
229230

230231
for (const productName of productNames) {
231232
const productChildTypes: readonly ResourceType[] = [
@@ -319,6 +320,8 @@ export async function compareApimInstances(
319320
).length;
320321

321322
return {
323+
sourceResourceId: config.source.baseUrl,
324+
targetResourceId: config.target.baseUrl,
322325
totalDifferences,
323326
differences,
324327
};
@@ -639,12 +642,21 @@ function compareResourceLists(
639642
if (!targetMap.has(name)) {
640643
const sourceResource = sourceMap.get(name)!;
641644
const displayName = getComparisonDisplayName(sourceResource);
645+
const sourceResourceId =
646+
typeof sourceResource.id === 'string' ? sourceResource.id : undefined;
647+
const relativeResourceId = resolveRelativeResourceId(
648+
sourceResourceId ?? '',
649+
normalizeContext.sourceServiceName,
650+
);
642651

643652
differences.push({
644653
resourceType: typeLabel,
645654
resourceName: name,
646655
diffType: 'missing',
647656
...(displayName === undefined ? {} : { displayName }),
657+
...(relativeResourceId === undefined
658+
? {}
659+
: { relativeResourceId }),
648660
instance: 'source',
649661
});
650662
}
@@ -655,12 +667,21 @@ function compareResourceLists(
655667
if (!sourceMap.has(name)) {
656668
const targetResource = targetMap.get(name)!;
657669
const displayName = getComparisonDisplayName(targetResource);
670+
const targetResourceId =
671+
typeof targetResource.id === 'string' ? targetResource.id : undefined;
672+
const relativeResourceId = resolveRelativeResourceId(
673+
targetResourceId ?? '',
674+
normalizeContext.targetServiceName,
675+
);
658676

659677
differences.push({
660678
resourceType: typeLabel,
661679
resourceName: name,
662680
diffType: 'extra',
663681
...(displayName === undefined ? {} : { displayName }),
682+
...(relativeResourceId === undefined
683+
? {}
684+
: { relativeResourceId }),
664685
instance: 'target',
665686
});
666687
}
@@ -675,6 +696,13 @@ function compareResourceLists(
675696
const displayName =
676697
getComparisonDisplayName(sourceResource) ??
677698
getComparisonDisplayName(targetResource);
699+
const sourceResourceId =
700+
typeof sourceResource.id === 'string' ? sourceResource.id : undefined;
701+
const targetResourceId =
702+
typeof targetResource.id === 'string' ? targetResource.id : undefined;
703+
const relativeResourceId =
704+
resolveRelativeResourceId(sourceResourceId ?? '', normalizeContext.sourceServiceName) ??
705+
resolveRelativeResourceId(targetResourceId ?? '', normalizeContext.targetServiceName);
678706

679707
const sourceNorm = normalizeResource(sourceResource, normalizeContext);
680708
const targetNorm = normalizeResource(targetResource, normalizeContext);
@@ -722,6 +750,9 @@ function compareResourceLists(
722750
resourceName: name,
723751
diffType: 'property-diff',
724752
...(displayName === undefined ? {} : { displayName }),
753+
...(relativeResourceId === undefined
754+
? {}
755+
: { relativeResourceId }),
725756
diffs,
726757
});
727758
}
@@ -831,7 +862,10 @@ export async function compareLocalArtifacts(
831862
_format: 'text' | 'json' | 'table' = 'text',
832863
_logLevel?: LogLevel,
833864
): Promise<CompareResult> {
834-
logger.info(`Comparing local artifacts: ${sourceDir}${targetDir}`);
865+
const shouldLogInfo = _format !== 'json';
866+
if (shouldLogInfo) {
867+
logger.info(`Comparing local artifacts: ${sourceDir}${targetDir}`);
868+
}
835869

836870
// Load artifacts from both directories
837871
const sourceArtifacts = await loadLocalArtifacts(

tests/unit/lib/resource-uri.test.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT license.
33
import { describe, it, expect } from 'vitest';
4-
import { buildArmUri, parseArmUri, buildResourceLabel } from '../../../src/lib/resource-uri.js';
4+
import { buildArmUri, parseArmUri, buildResourceLabel, getRelativeResourceId } from '../../../src/lib/resource-uri.js';
55
import { ApimServiceContext, ResourceDescriptor } from '../../../src/models/types.js';
66
import { ResourceType } from '../../../src/models/resource-types.js';
77

@@ -260,6 +260,31 @@ describe('buildResourceLabel', () => {
260260
expect(label).toBe('apis/petstore/operations/get-user/policies/policy');
261261
});
262262

263+
264+
describe('getRelativeResourceId', () => {
265+
it('should strip the APIM service prefix from a named value resource id', () => {
266+
const resourceId =
267+
'/subscriptions/sub-123/resourceGroups/rg-test/providers/Microsoft.ApiManagement/service/my-apim/namedValues/mySecret';
268+
269+
expect(getRelativeResourceId(resourceId, 'my-apim')).toBe(
270+
'namedValues/mySecret',
271+
);
272+
});
273+
274+
it('should strip query parameters from the relative resource id', () => {
275+
const resourceId =
276+
'/subscriptions/sub-123/resourceGroups/rg-test/providers/Microsoft.ApiManagement/service/my-apim/apis/petstore?api-version=2024-05-01';
277+
278+
expect(getRelativeResourceId(resourceId, 'my-apim')).toBe('apis/petstore');
279+
});
280+
281+
it('should return undefined when the service name does not match', () => {
282+
const resourceId =
283+
'/subscriptions/sub-123/resourceGroups/rg-test/providers/Microsoft.ApiManagement/service/my-apim/apis/petstore';
284+
285+
expect(getRelativeResourceId(resourceId, 'other-apim')).toBeUndefined();
286+
});
287+
});
263288
it('should format product-child resource (ProductApi)', () => {
264289
const descriptor: ResourceDescriptor = {
265290
type: ResourceType.ProductApi,

0 commit comments

Comments
 (0)