Skip to content

Commit 56907ba

Browse files
authored
Merge pull request #145 from Azure/fix/publish-overrides
Resolve named value reference, schema ref, and override issues in publish
2 parents 518ecff + 4335e63 commit 56907ba

9 files changed

Lines changed: 541 additions & 95 deletions

File tree

docs/guides/environment-overrides.md

Lines changed: 108 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,89 @@ loggers:
204204
isBuffered: true
205205
```
206206
207+
> **Note:** When you provide a raw `instrumentationKey` value (instead of a `{{namedValue}}` reference), APIM will automatically create a named value to store the credential securely.
208+
209+
### Logger credentials with auto-generated named values
210+
211+
When APIM creates a logger (e.g., for Application Insights), it auto-generates a named value to store the instrumentation key. These auto-generated named values have 24-character hex IDs (e.g., `66f48e1226dab62c0823e4f8`) and are normally skipped during publish because APIM recreates them automatically.
212+
213+
However, when publishing to a **fresh environment**, APIM cannot recreate these named values because the logger doesn't exist yet. To handle this, provide an override for the auto-generated named value:
214+
215+
```yaml
216+
namedValues:
217+
# Override the auto-generated named value with the production instrumentation key.
218+
# Use the 24-char hex ID from the extracted artifact filename.
219+
- name: 66f48e1226dab62c0823e4f8
220+
properties:
221+
value: "prod-instrumentation-key-value"
222+
223+
loggers:
224+
- name: appinsights-logger
225+
properties:
226+
loggerType: applicationInsights
227+
resourceId: "/subscriptions/<sub-id>/resourceGroups/<rg>/providers/microsoft.insights/components/prod-appinsights"
228+
isBuffered: true
229+
```
230+
231+
Alternatively, you can override the logger credentials directly to bypass the named value reference entirely:
232+
233+
```yaml
234+
loggers:
235+
- name: appinsights-logger
236+
properties:
237+
loggerType: applicationInsights
238+
resourceId: "/subscriptions/<sub-id>/resourceGroups/<rg>/providers/microsoft.insights/components/prod-appinsights"
239+
credentials:
240+
instrumentationKey: "prod-instrumentation-key-value"
241+
isBuffered: true
242+
```
243+
244+
### Subscriptions
245+
246+
```yaml
247+
subscriptions:
248+
- name: my-subscription
249+
properties:
250+
displayName: "My Subscription (Production)"
251+
scope: "/apis/petstore-api"
252+
```
253+
254+
> **Note:** The built-in `master` subscription is automatically skipped during publish.
255+
> Product-scoped subscriptions auto-generated by APIM are also skipped to avoid subscription limit errors.
256+
257+
### Products
258+
259+
```yaml
260+
products:
261+
- name: starter-product
262+
properties:
263+
displayName: "Starter (Production)"
264+
subscriptionRequired: true
265+
approvalRequired: false
266+
subscriptionsLimit: 10
267+
```
268+
269+
### Gateways
270+
271+
```yaml
272+
gateways:
273+
- name: on-prem-gateway
274+
properties:
275+
locationData:
276+
name: "Production datacenter"
277+
city: "Seattle"
278+
countryOrRegion: "US"
279+
```
280+
281+
### Policy fragments
282+
283+
```yaml
284+
policyFragments:
285+
- name: rate-limit-fragment
286+
properties:
287+
description: "Production rate limiting policy"
288+
```
289+
207290
### Service-level policies
208291

209292
```yaml
@@ -213,21 +296,26 @@ policies:
213296
format: rawxml
214297
```
215298

216-
### All other resource types
299+
### Version sets, groups, and tags
217300

218-
Overrides are also supported for: `gateways`, `versionSets`, `groups`, `subscriptions`, `products`, `tags`, `policyFragments`, and `workspaces`. Each uses the same `name` + `properties` format:
301+
Overrides are also supported for `versionSets`, `groups`, `tags`, and `workspaces`. Each uses the same `name` + `properties` format:
219302

220303
```yaml
221-
gateways:
222-
- name: on-prem-gateway
304+
versionSets:
305+
- name: petstore-versions
223306
properties:
224-
locationData:
225-
name: "On-premises datacenter"
307+
displayName: "Petstore API Versions"
308+
versioningScheme: Segment
226309
227-
products:
228-
- name: starter-product
310+
groups:
311+
- name: partner-developers
229312
properties:
230-
displayName: "Starter (Production)"
313+
displayName: "Partner Developers (Production)"
314+
315+
tags:
316+
- name: public-api
317+
properties:
318+
displayName: "Public API"
231319
```
232320

233321
## Override rules
@@ -360,6 +448,17 @@ If you add a new backend in dev but forget to add it to your override files, pub
360448

361449
When using Key Vault references, the APIM managed identity needs access to the Key Vault. A common failure mode: overrides reference a Key Vault but APIM lacks the `Key Vault Secrets User` role on that vault.
362450

451+
### Gotcha: Auto-generated named values for loggers
452+
453+
When you create a logger in APIM (e.g., for Application Insights), APIM auto-generates a named value to store the credential. These have 24-character hex names (e.g., `<24-char-hex-id>`). During extract, these are captured as artifacts. During publish:
454+
455+
- **Same environment:** Auto-generated named values are skipped (APIM already has them).
456+
- **Fresh environment:** The logger fails because the named value doesn't exist yet. Provide an override with the target environment's credential value, or override the logger's `credentials` directly.
457+
458+
### Gotcha: Redacted secrets
459+
460+
Extracted secret named values have their `value` replaced with `*** REDACTED ***`. If you publish these without providing an override with a real value or Key Vault reference, they will be skipped with a warning. Always provide overrides for secret named values when publishing to a different environment.
461+
363462
### Dry-run validation
364463

365464
Use `--dry-run` to preview publish behavior with overrides:

src/services/api-publisher.ts

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -462,34 +462,46 @@ async function reconcileOperationsAfterSpecImport(
462462
}
463463
}
464464

465-
/** Strip source schema refs from request/response representations before PATCH. */
465+
/**
466+
* Strip source schema refs from operation parameters and representations
467+
* before PATCH. APIM assigns new schema IDs on spec import, so stale IDs
468+
* from the source instance would cause "Operation references schema that
469+
* does not exist" validation errors.
470+
*/
466471
function stripRepresentationSchemaRefs(patchProps: Record<string, unknown>): void {
467472
const SCHEMA_REF_FIELDS = ['schemaId', 'typeName'];
468473

469-
function stripFromRepresentations(representations: unknown): void {
470-
if (!Array.isArray(representations)) return;
471-
for (const rep of representations) {
472-
if (rep && typeof rep === 'object') {
474+
function stripFromItems(items: unknown): void {
475+
if (!Array.isArray(items)) return;
476+
for (const item of items) {
477+
if (item && typeof item === 'object') {
473478
for (const field of SCHEMA_REF_FIELDS) {
474-
delete (rep as Record<string, unknown>)[field];
479+
delete (item as Record<string, unknown>)[field];
475480
}
476481
}
477482
}
478483
}
479484

480-
// Strip schema refs from request.representations.
485+
// Strip schema refs from top-level templateParameters.
486+
stripFromItems(patchProps.templateParameters);
487+
488+
// Strip schema refs from request parameters and representations.
481489
const request = patchProps.request;
482490
if (request && typeof request === 'object') {
483491
const req = request as Record<string, unknown>;
484-
stripFromRepresentations(req.representations);
492+
stripFromItems(req.representations);
493+
stripFromItems(req.queryParameters);
494+
stripFromItems(req.headers);
485495
}
486496

487-
// Strip schema refs from responses[].representations.
497+
// Strip schema refs from responses[].representations and headers.
488498
const responses = patchProps.responses;
489499
if (Array.isArray(responses)) {
490500
for (const response of responses) {
491501
if (response && typeof response === 'object') {
492-
stripFromRepresentations((response as Record<string, unknown>).representations);
502+
const resp = response as Record<string, unknown>;
503+
stripFromItems(resp.representations);
504+
stripFromItems(resp.headers);
493505
}
494506
}
495507
}

src/services/publish-service.ts

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import { IApimClient } from '../clients/iapim-client.js';
1010
import { IArtifactStore } from '../clients/iartifact-store.js';
11-
import { PublishConfig } from '../models/config.js';
11+
import { PublishConfig, OverrideConfig } from '../models/config.js';
1212
import { ApimServiceContext, ResourceDescriptor } from '../models/types.js';
1313
import { ResourceType } from '../models/resource-types.js';
1414
import { getResourceTier } from '../lib/dependency-graph.js';
@@ -257,7 +257,7 @@ async function executePuts(
257257
// Pool backends reference individual Backend resources and must be
258258
// published after the backends they aggregate (see pool backend
259259
// ordering comments in splitPoolBackends).
260-
const { namedValues, otherTier1 } = splitNamedValues(nonWorkspaceTier1);
260+
const { namedValues, otherTier1 } = splitNamedValues(nonWorkspaceTier1, config.overrides);
261261

262262
if (namedValues.length > 0) {
263263
logger.debug(`Publishing ${namedValues.length} named value(s) first (wave 1 of tier 1)`);
@@ -378,19 +378,22 @@ async function publishAndOutput(
378378
* Auto-generated NamedValues (24-char hex IDs like Logger credentials) are
379379
* filtered out because APIM recreates them when the referencing Logger is
380380
* published. Publishing them would create orphan duplicates.
381+
*
382+
* However, if the user has provided an explicit override for an auto-generated
383+
* named value, it is published normally. This supports publishing to fresh
384+
* environments where APIM cannot recreate the named value automatically.
381385
*/
382386
function splitNamedValues(
383-
descriptors: ResourceDescriptor[]
387+
descriptors: ResourceDescriptor[],
388+
overrides?: OverrideConfig
384389
): { namedValues: ResourceDescriptor[]; otherTier1: ResourceDescriptor[] } {
385390
const namedValues: ResourceDescriptor[] = [];
386391
const otherTier1: ResourceDescriptor[] = [];
387392

388393
for (const descriptor of descriptors) {
389394
if (descriptor.type === ResourceType.NamedValue) {
390-
// Skip auto-generated named values (24-char hex IDs) - APIM recreates
391-
// these when publishing loggers with credential references
392395
const name = getNamePart(descriptor.nameParts, 0);
393-
if (isAutoGeneratedId(name)) {
396+
if (isAutoGeneratedId(name) && !hasNamedValueOverride(name, overrides)) {
394397
logger.debug(`Skipping auto-generated named value: ${name}`);
395398
continue;
396399
}
@@ -403,6 +406,18 @@ function splitNamedValues(
403406
return { namedValues, otherTier1 };
404407
}
405408

409+
/**
410+
* Check whether a named value has an explicit override entry.
411+
* Uses case-insensitive matching to align with override-merger behavior.
412+
*/
413+
function hasNamedValueOverride(name: string, overrides?: OverrideConfig): boolean {
414+
if (!overrides?.namedValues) return false;
415+
const lowerName = name.toLowerCase();
416+
return Object.keys(overrides.namedValues).some(
417+
(key) => key.toLowerCase() === lowerName
418+
);
419+
}
420+
406421
/**
407422
* Separates Backend descriptors that are pool backends (properties.type === "Pool")
408423
* from all other descriptors in the supplied list.

src/services/resource-extractor.ts

Lines changed: 0 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -88,10 +88,6 @@ export async function extractResourceType(
8888
};
8989

9090
try {
91-
const loggerTokenMap =
92-
type === ResourceType.Logger
93-
? await loadNamedValueDisplayNameMap(client, context)
94-
: undefined;
9591

9692
const resources = client.listResources(context, type, parent);
9793

@@ -124,10 +120,6 @@ export async function extractResourceType(
124120
}
125121
}
126122

127-
if (type === ResourceType.Logger && loggerTokenMap && loggerTokenMap.size > 0) {
128-
json = normalizeLoggerCredentialPlaceholders(json, loggerTokenMap);
129-
}
130-
131123
// Apply secret redaction
132124
const safeJson = redactSecrets(descriptor, json);
133125

@@ -166,68 +158,6 @@ export async function extractResourceType(
166158
return result;
167159
}
168160

169-
async function loadNamedValueDisplayNameMap(
170-
client: IApimClient,
171-
context: ApimServiceContext
172-
): Promise<Map<string, string>> {
173-
const map = new Map<string, string>();
174-
175-
for await (const namedValue of client.listResources(context, ResourceType.NamedValue)) {
176-
const name = namedValue.name;
177-
const properties = namedValue.properties as Record<string, unknown> | undefined;
178-
const displayName = properties?.displayName;
179-
180-
if (typeof name === 'string' && typeof displayName === 'string' && displayName.length > 0) {
181-
map.set(displayName, name);
182-
}
183-
}
184-
185-
return map;
186-
}
187-
188-
function normalizeLoggerCredentialPlaceholders(
189-
json: Record<string, unknown>,
190-
displayNameToName: Map<string, string>
191-
): Record<string, unknown> {
192-
const properties = json.properties as Record<string, unknown> | undefined;
193-
const credentials = properties?.credentials;
194-
195-
if (!properties || credentials === undefined) {
196-
return json;
197-
}
198-
199-
const normalizeValue = (value: unknown): unknown => {
200-
if (typeof value === 'string') {
201-
return value.replace(/\{\{([^}]+)\}\}/g, (match, tokenName: string) => {
202-
const mappedName = displayNameToName.get(tokenName);
203-
return mappedName ? `{{${mappedName}}}` : match;
204-
});
205-
}
206-
207-
if (Array.isArray(value)) {
208-
return value.map(normalizeValue);
209-
}
210-
211-
if (value && typeof value === 'object') {
212-
const out: Record<string, unknown> = {};
213-
for (const [key, child] of Object.entries(value as Record<string, unknown>)) {
214-
out[key] = normalizeValue(child);
215-
}
216-
return out;
217-
}
218-
219-
return value;
220-
};
221-
222-
return {
223-
...json,
224-
properties: {
225-
...properties,
226-
credentials: normalizeValue(credentials),
227-
},
228-
};
229-
}
230-
231161
/**
232162
* Extract a single resource by descriptor and write to artifact store.
233163
*/

0 commit comments

Comments
 (0)