Skip to content

Commit 2756791

Browse files
Alexey-ZheltovAlexander Zaslonov
authored andcommitted
fix(gateway): read desired apis from apis.json for delete-unmatched
computeGatewayApiDeleteActions compared deployed [gateway,api] descriptors against localSet, which only holds aggregate GatewayApi [gateway] keys (api names live in apis.json content, not the path). Every deployed assignment therefore missed the set and was deleted regardless of desired state - e.g. an API present in the gateway's apis.json was both PUT and DELETEd. Read the desired API set per gateway via store.readAssociation and delete only deployed assignments absent from it. Update tests to use aggregate local descriptors + readAssociation.
1 parent 065d9be commit 2756791

2 files changed

Lines changed: 57 additions & 16 deletions

File tree

‎src/services/delete-unmatched-service.ts‎

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -190,10 +190,10 @@ export async function computeDeleteActions(
190190
// apis.json are never touched.
191191
const gatewayApiDeletes = await computeGatewayApiDeleteActions(
192192
client,
193+
store,
193194
context,
194195
config,
195-
localDescriptors,
196-
localSet
196+
localDescriptors
197197
);
198198
// Run association removals first (children before parents).
199199
deleteDescriptors.unshift(...gatewayApiDeletes);
@@ -204,22 +204,24 @@ export async function computeDeleteActions(
204204
/**
205205
* Reconcile per-gateway API assignments (ResourceType.GatewayApi).
206206
*
207-
* Only gateways that appear as a parent of at least one local GatewayApi
208-
* artifact are considered, so a workspace that does not track gateway
209-
* associations is left completely untouched. For each such gateway the deployed
210-
* assignments are listed and any assignment missing from the local artifacts is
211-
* queued for deletion (i.e. the API is un-assigned from that gateway).
207+
* Only gateways that appear as a local GatewayApi artifact are considered, so a
208+
* workspace that does not track gateway associations is left completely
209+
* untouched. The desired API set for each gateway is read from its
210+
* `gateways/{gw}/apis.json` (the artifact store surfaces GatewayApi only as an
211+
* aggregate `nameParts = [gateway]` descriptor, with the API names living in the
212+
* file content), then any deployed assignment not in that desired set is queued
213+
* for deletion (i.e. the API is un-assigned from that gateway).
212214
*/
213215
async function computeGatewayApiDeleteActions(
214216
client: IApimClient,
217+
store: IArtifactStore,
215218
context: ApimServiceContext,
216219
config: PublishConfig,
217-
localDescriptors: ResourceDescriptor[],
218-
localSet: Set<string>
220+
localDescriptors: ResourceDescriptor[]
219221
): Promise<ResourceDescriptor[]> {
220222
const { envMapping } = config;
221223

222-
// Distinct gateway names that own at least one local GatewayApi artifact.
224+
// Distinct gateway names that own a local GatewayApi artifact.
223225
const gatewayNames = new Set<string>();
224226
for (const descriptor of localDescriptors) {
225227
if (descriptor.type === ResourceType.GatewayApi) {
@@ -242,6 +244,18 @@ async function computeGatewayApiDeleteActions(
242244
nameParts: [gatewayName],
243245
};
244246

247+
// Desired API set (canonical names) from the gateway's apis.json artifact.
248+
let desiredApis: Set<string>;
249+
try {
250+
const entries = await store.readAssociation(config.sourceDir, gatewayDescriptor, 'apis');
251+
desiredApis = new Set(entries.map((entry) => entry.name));
252+
} catch (error) {
253+
logger.debug(
254+
`[delete-unmatched] Skipping gateway "${gatewayName}" API reconciliation (cannot read desired apis): ${(error as Error).message}`
255+
);
256+
continue;
257+
}
258+
245259
try {
246260
for await (const apiJson of client.listResources(
247261
context,
@@ -258,16 +272,19 @@ async function computeGatewayApiDeleteActions(
258272
nameParts: [gatewayName, apiName],
259273
};
260274

275+
// Compare the deployed API against the desired set using canonical names
276+
// so env-affixed deployments still match the un-affixed artifacts.
277+
let canonicalApiName = apiName;
261278
if (envMapping !== undefined) {
262279
const canonicalDescriptor = toCanonicalDescriptor(deployedDescriptor, envMapping);
263280
if (canonicalDescriptor === null) {
264281
// Belongs to another environment — do not touch.
265282
continue;
266283
}
267-
if (!localSet.has(getResourceKey(canonicalDescriptor))) {
268-
deletes.push(deployedDescriptor);
269-
}
270-
} else if (!localSet.has(getResourceKey(deployedDescriptor))) {
284+
canonicalApiName = getNamePart(canonicalDescriptor.nameParts, 1);
285+
}
286+
287+
if (!desiredApis.has(canonicalApiName)) {
271288
deletes.push(deployedDescriptor);
272289
}
273290
}

‎tests/unit/services/delete-unmatched-service.test.ts‎

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -235,13 +235,15 @@ describe('delete-unmatched-service', () => {
235235
[ResourceType.GatewayApi, [{ name: 'api-keep' }, { name: 'api-stale' }]],
236236
]);
237237

238-
// Local artifacts track the managed gateway with only api-keep assigned.
238+
// The store surfaces GatewayApi as an aggregate descriptor (nameParts =
239+
// [gateway]); the desired API names live in apis.json (readAssociation).
239240
const localDescriptors: ResourceDescriptor[] = [
240-
{ type: ResourceType.GatewayApi, nameParts: ['managed', 'api-keep'] },
241+
{ type: ResourceType.GatewayApi, nameParts: ['managed'] },
241242
];
242243

243244
const client = createMockClient(apimResources);
244245
const store = createMockStore(localDescriptors);
246+
store.readAssociation = vi.fn().mockResolvedValue([{ name: 'api-keep' }]);
245247

246248
const result = await computeDeleteActions(client, store, testContext, testConfig);
247249

@@ -253,6 +255,28 @@ describe('delete-unmatched-service', () => {
253255
});
254256
});
255257

258+
it('should keep a desired gateway API even when it is also being published', async () => {
259+
const apimResources = new Map<ResourceType, Record<string, unknown>[]>([
260+
[ResourceType.GatewayApi, [{ name: 'webapitest' }]],
261+
]);
262+
263+
const localDescriptors: ResourceDescriptor[] = [
264+
{ type: ResourceType.GatewayApi, nameParts: ['shgw-UAE-01'] },
265+
];
266+
267+
const client = createMockClient(apimResources);
268+
const store = createMockStore(localDescriptors);
269+
store.readAssociation = vi.fn().mockResolvedValue([
270+
{ name: 'customermanagementservice' },
271+
{ name: 'swagger-petstore' },
272+
{ name: 'webapitest' },
273+
]);
274+
275+
const result = await computeDeleteActions(client, store, testContext, testConfig);
276+
277+
expect(result.filter((d) => d.type === ResourceType.GatewayApi)).toHaveLength(0);
278+
});
279+
256280
it('should not touch any gateway assignments when artifacts track no gateways', async () => {
257281
const apimResources = new Map<ResourceType, Record<string, unknown>[]>([
258282
[ResourceType.GatewayApi, [{ name: 'api-a' }, { name: 'api-b' }]],

0 commit comments

Comments
 (0)