diff --git a/changelog/pending/20221031--sdk-nodejs--delegates-alias-computation-to-engine-for-node-sdk.yaml b/changelog/pending/20221031--sdk-nodejs--delegates-alias-computation-to-engine-for-node-sdk.yaml new file mode 100644 index 000000000000..05d37f8cfae9 --- /dev/null +++ b/changelog/pending/20221031--sdk-nodejs--delegates-alias-computation-to-engine-for-node-sdk.yaml @@ -0,0 +1,4 @@ +changes: +- type: feat + scope: sdk/nodejs + description: Delegates alias computation to engine for Node SDK diff --git a/sdk/nodejs/resource.ts b/sdk/nodejs/resource.ts index 26a3447e7839..2957883c5022 100644 --- a/sdk/nodejs/resource.ts +++ b/sdk/nodejs/resource.ts @@ -17,7 +17,6 @@ import { Input, Inputs, interpolate, Output, output } from "./output"; import { getResource, readResource, registerResource, registerResourceOutputs } from "./runtime/resource"; import { getProject, getStack } from "./runtime/settings"; import { getStackResource } from "./runtime/state"; -import * as stacklib from "./runtime/stack"; import { unknownValue } from "./runtime/rpc"; import * as utils from "./utils"; import * as log from "./log"; @@ -215,7 +214,7 @@ export abstract class Resource { * @internal */ // eslint-disable-next-line @typescript-eslint/naming-convention,no-underscore-dangle,id-blacklist,id-match - private readonly __name?: string; + readonly __name?: string; /** * The set of providers to use for child resources. Keyed by package name (e.g. "aws"). @@ -333,9 +332,6 @@ export abstract class Resource { opts.protect = parent.__protect; } - // Update aliases to include the full set of aliases implied by the child and parent aliases. - opts.aliases = allAliases(opts.aliases || [], name, t, parent, parent.__name!); - this.__providers = parent.__providers; } diff --git a/sdk/nodejs/runtime/resource.ts b/sdk/nodejs/runtime/resource.ts index 8c5711dc0297..c1786ec2ef02 100644 --- a/sdk/nodejs/runtime/resource.ts +++ b/sdk/nodejs/runtime/resource.ts @@ -20,6 +20,8 @@ import * as utils from "../utils"; import { getAllResources, Input, Inputs, Output, output } from "../output"; import { ResolvedResource } from "../queryable"; import { + Alias, + allAliases, ComponentResource, ComponentResourceOptions, createUrn, @@ -53,6 +55,7 @@ import { getStack, isDryRun, isLegacyApplyEnabled, + monitorSupportsAliasSpecs, rpcKeepAlive, serialize, terminateRpcs, @@ -60,6 +63,7 @@ import { const gstruct = require("google-protobuf/google/protobuf/struct_pb.js"); const resproto = require("../proto/resource_pb.js"); +const aliasproto = require("../proto/alias_pb.js"); interface ResourceResolverOperation { // A resolver for a resource's URN. @@ -84,9 +88,11 @@ interface ResourceResolverOperation { // all be URNs of custom resources, not component resources. propertyToDirectDependencyURNs: Map>; // A list of aliases applied to this resource. - aliases: URN[]; + aliases: (Alias | URN)[]; // An ID to import, if any. import: ID | undefined; + // Any important feature support from the monitor. + monitorSupports: Map; } /** @@ -273,6 +279,28 @@ export function readResource(res: Resource, parent: Resource | undefined, t: str }), label); } +function mapAliasesForRequest(aliases: (string | Alias)[] | undefined) { + if (aliases === undefined) { + return []; + } + + return aliases.map(a => { + const newAlias = new aliasproto.Alias(); + if (typeof a === "string") { + newAlias.setUrn(a); + } else { + const newAliasSpec = new aliasproto.Alias.Spec(); + newAliasSpec.setName(a.name); + newAliasSpec.setType(a.type); + newAliasSpec.setStack(a.stack); + newAliasSpec.setProject(a.project); + newAliasSpec.setParenturn(a.parent); + newAlias.setSpec(newAliasSpec); + } + return newAlias; + }); +} + /** * registerResource registers a new resource object with a given type t and name. It returns the auto-generated * URN and the ID that will resolve after the deployment has completed. All properties will be initialized to property @@ -284,7 +312,7 @@ export function registerResource(res: Resource, parent: Resource | undefined, t: log.debug(`Registering resource: t=${t}, name=${name}, custom=${custom}, remote=${remote}`); const monitor = getMonitor(); - const resopAsync = prepareResource(label, res, parent, custom, remote, props, opts); + const resopAsync = prepareResource(label, res, parent, custom, remote, props, opts, t, name); // In order to present a useful stack trace if an error does occur, we preallocate potential // errors here. V8 captures a stack trace at the moment an Error is created and this stack @@ -311,7 +339,11 @@ export function registerResource(res: Resource, parent: Resource | undefined, t: req.setAcceptsecrets(true); req.setAcceptresources(!utils.disableResourceReferences); req.setAdditionalsecretoutputsList((opts).additionalSecretOutputs || []); - req.setAliasurnsList(resop.aliases); + if (resop.monitorSupports.get("aliasSpecs")) { + req.setAliasesList(mapAliasesForRequest(resop.aliases)); + } else { + req.setAliasurnsList(resop.aliases); + } req.setImportid(resop.import || ""); req.setSupportspartialvalues(true); req.setRemote(remote); @@ -424,8 +456,11 @@ export function registerResource(res: Resource, parent: Resource | undefined, t: * properties. */ async function prepareResource(label: string, res: Resource, parent: Resource | undefined, custom: boolean, remote: boolean, - props: Inputs, opts: ResourceOptions): Promise { - + props: Inputs, opts: ResourceOptions, type: string, name: string): Promise; +async function prepareResource(label: string, res: Resource, parent: Resource | undefined, custom: boolean, remote: boolean, + props: Inputs, opts: ResourceOptions): Promise; +async function prepareResource(label: string, res: Resource, parent: Resource | undefined, custom: boolean, remote: boolean, + props: Inputs, opts: ResourceOptions, type?: string, name?: string): Promise { // add an entry to the rpc queue while we prepare the request. // automation api inline programs that don't have stack exports can exit quickly. If we don't do this, // sometimes they will exit right after `prepareResource` is called as a part of register resource, but before the @@ -581,12 +616,20 @@ async function prepareResource(label: string, res: Resource, parent: Resource | propertyToDirectDependencyURNs.set(propertyName, urns); } + const supportsAliasSpecs = await monitorSupportsAliasSpecs(); + const monitorSupports = new Map(); + monitorSupports.set("aliasSpecs", supportsAliasSpecs); + let computedAliases = opts.aliases; + if (!supportsAliasSpecs && parent) { + computedAliases = await allAliases(opts.aliases || [], name!, type!, parent, parent.__name!); + } + // Wait for all aliases. Note that we use `res.__aliases` instead of `opts.aliases` as the former has been processed // in the Resource constructor prior to calling `registerResource` - both adding new inherited aliases and // simplifying aliases down to URNs. const aliases = []; - const uniqueAliases = new Set(); - for (const alias of (res.__aliases || [])) { + const uniqueAliases = new Set(); + for (const alias of (computedAliases || [])) { const aliasVal = await output(alias).promise(); if (!uniqueAliases.has(aliasVal)) { uniqueAliases.add(aliasVal); @@ -606,6 +649,7 @@ async function prepareResource(label: string, res: Resource, parent: Resource | propertyToDirectDependencyURNs: propertyToDirectDependencyURNs, aliases: aliases, import: importID, + monitorSupports, }; } finally { diff --git a/sdk/nodejs/runtime/settings.ts b/sdk/nodejs/runtime/settings.ts index 520fdd6b3e43..3a906e92fab5 100644 --- a/sdk/nodejs/runtime/settings.ts +++ b/sdk/nodejs/runtime/settings.ts @@ -516,6 +516,14 @@ export async function monitorSupportsOutputValues(): Promise { */ export async function monitorSupportsDeletedWith(): Promise { return monitorSupportsFeature("deletedWith"); + +/** + * monitorSupportsAliasSpecs returns a promise that when resolved tells you if the resource monitor we are + * connected to is able to support alias specs across its RPC interface. When it does, we marshal aliases + * in a special way. + */ +export async function monitorSupportsAliasSpecs(): Promise { + return monitorSupportsFeature("aliasSpecs"); } // sxsRandomIdentifier is a module level global that is transfered to process.env. diff --git a/sdk/nodejs/tests/runtime/langhost/cases/072.large_alias_lineage_chains/index.js b/sdk/nodejs/tests/runtime/langhost/cases/072.large_alias_lineage_chains/index.js index 23cdb785836a..ac595b163a58 100644 --- a/sdk/nodejs/tests/runtime/langhost/cases/072.large_alias_lineage_chains/index.js +++ b/sdk/nodejs/tests/runtime/langhost/cases/072.large_alias_lineage_chains/index.js @@ -18,10 +18,39 @@ class MyResource extends pulumi.CustomResource { } } +class MyOtherResource extends pulumi.CustomResource { + constructor(name, aliases, parent) { + super( + "test:index:MyOtherResource", + name, + {}, + { + aliases: aliases, + parent + } + ); + } +} + const resource1Aliases = Array.from(new Array(1000).keys()).map(key => `my-alias-name-${key}`); const resource1 = new MyResource("testResource1", resource1Aliases); resource1.__aliases.map(alias => alias.apply(aliasName => assert(resource1Aliases.includes(aliasName)))); +assert.equal(resource1.__aliases.length, 1000); const resource2Aliases = Array.from(new Array(1000).keys()).map(key => `my-other-alias-name-${key}`); const resource2 = new MyResource("testResource2", resource2Aliases, resource1) resource2.__aliases.map(alias => alias.apply(aliasName => assert(resource2Aliases.includes(aliasName)))); +assert.equal(resource2.__aliases.length, 1000); + +const resource3Aliases = Array.from(new Array(1000).keys()).map(key => { + return { + name: `my-alias-${key}`, + stack: "my-stack", + project: "my-project", + type: "test:index:MyOtherResource", + } +}); +const resource3 = new MyOtherResource("testResource2", resource3Aliases, resource2) +assert.equal(resource3.__aliases.length, 1000); +// We want to ensure that the parent's type is included in computed aliases from the engine +resource3.__aliases[0].apply(aliasName => assert(aliasName.includes("test:index:MyResource"))); diff --git a/sdk/nodejs/tests/runtime/langhost/run.spec.ts b/sdk/nodejs/tests/runtime/langhost/run.spec.ts index 72013bfade05..4d17679f0dba 100644 --- a/sdk/nodejs/tests/runtime/langhost/run.spec.ts +++ b/sdk/nodejs/tests/runtime/langhost/run.spec.ts @@ -1249,14 +1249,13 @@ describe("rpc", () => { return { urn: makeUrn(t, name), id: undefined, props: undefined }; }, }, - /** Skipping this test case as it requires limiting the alias multiplication which occurs */ - // "large_alias_lineage_chains": { - // program: path.join(base, "072.large_alias_lineage_chains"), - // expectResourceCount: 1, - // registerResource: (ctx: any, dryrun: boolean, t: string, name: string, res: any, ...args: any) => { - // return { urn: makeUrn(t, name), id: undefined, props: undefined }; - // }, - // } + "large_alias_lineage_chains": { + program: path.join(base, "072.large_alias_lineage_chains"), + expectResourceCount: 3, + registerResource: (ctx: any, dryrun: boolean, t: string, name: string, res: any, ...args: any) => { + return { urn: makeUrn(t, name), id: undefined, props: undefined }; + }, + }, }; for (const casename of Object.keys(cases)) { @@ -1418,7 +1417,7 @@ describe("rpc", () => { // SupportsFeature callback (call: any, callback: any) => { const resp = new resproto.SupportsFeatureResponse(); - resp.setHassupport(false); + resp.setHassupport(true); callback(undefined, resp); }, );