// Copyright 2016-2018, Pulumi Corporation. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import * as url from "url"; import { ResourceError } from "./errors"; import * as log from "./log"; import { Input, Inputs, interpolate, Output, output } from "./output"; import { getResource, pkgFromType, readResource, registerResource, registerResourceOutputs, SourcePosition, } from "./runtime/resource"; import { unknownValue } from "./runtime/rpc"; import { getProject, getStack } from "./runtime/settings"; import { getStackResource } from "./runtime/state"; import * as utils from "./utils"; export type ID = string; // a provider-assigned ID. export type URN = string; // an automatically generated logical URN, used to stably identify resources. /** * createUrn computes a URN from the combination of a resource name, resource type, optional parent, * optional project and optional stack. */ export function createUrn( name: Input<string>, type: Input<string>, parent?: Resource | Input<URN>, project?: string, stack?: string, ): Output<string> { let parentPrefix: Output<string>; if (parent) { let parentUrn: Output<string>; if (Resource.isInstance(parent)) { parentUrn = parent.urn; } else { parentUrn = output(parent); } parentPrefix = parentUrn.apply((parentUrnString) => { const prefix = parentUrnString.substring(0, parentUrnString.lastIndexOf("::")) + "$"; if (prefix.endsWith("::pulumi:pulumi:Stack$")) { // Don't prefix the stack type as a parent type return `urn:pulumi:${stack || getStack()}::${project || getProject()}::`; } return prefix; }); } else { parentPrefix = output(`urn:pulumi:${stack || getStack()}::${project || getProject()}::`); } return interpolate`${parentPrefix}${type}::${name}`; } /** * inheritedChildAlias computes the alias that should be applied to a child based on an alias applied to it's parent. * This may involve changing the name of the resource in cases where the resource has a named derived from the name of * the parent, and the parent name changed. */ function inheritedChildAlias( childName: string, parentName: string, parentAlias: Input<string>, childType: string, ): Output<string> { // If the child name has the parent name as a prefix, then we make the assumption that it was // constructed from the convention of using `{name}-details` as the name of the child resource. To // ensure this is aliased correctly, we must then also replace the parent aliases name in the prefix of // the child resource name. // // For example: // * name: "newapp-function" // * opts.parent.__name: "newapp" // * parentAlias: "urn:pulumi:stackname::projectname::awsx:ec2:Vpc::app" // * parentAliasName: "app" // * aliasName: "app-function" // * childAlias: "urn:pulumi:stackname::projectname::aws:s3/bucket:Bucket::app-function" let aliasName = output(childName); if (childName.startsWith(parentName)) { aliasName = output(parentAlias).apply((parentAliasUrn) => { const parentAliasName = parentAliasUrn.substring(parentAliasUrn.lastIndexOf("::") + 2); return parentAliasName + childName.substring(parentName.length); }); } return createUrn(aliasName, childType, parentAlias); } /** * Extracts the type and name from a URN. */ function urnTypeAndName(urn: URN) { const parts = urn.split("::"); const typeParts = parts[2].split("$"); return { name: parts[3], type: typeParts[typeParts.length - 1], }; } /** * allAliases computes the full set of aliases for a child resource given a set of aliases applied to the child and * parent resources. This includes the child resource's own aliases, as well as aliases inherited from the parent. * If there are N child aliases, and M parent aliases, there will be (M+1)*(N+1)-1 total aliases, * or, as calculated in the logic below, N+(M*(1+N)). */ export function allAliases( childAliases: Input<URN | Alias>[], childName: string, childType: string, parent: Resource, parentName: string, ): Output<URN>[] { const aliases: Output<URN>[] = []; for (const childAlias of childAliases) { aliases.push(collapseAliasToUrn(childAlias, childName, childType, parent)); } for (const parentAlias of parent.__aliases || []) { // For each parent alias, add an alias that uses that base child name and the parent alias aliases.push(inheritedChildAlias(childName, parentName, parentAlias, childType)); // Also add an alias for each child alias and the parent alias for (const childAlias of childAliases) { const inheritedAlias = collapseAliasToUrn(childAlias, childName, childType, parent).apply( (childAliasURN) => { const { name: aliasedChildName, type: aliasedChildType } = urnTypeAndName(childAliasURN); return inheritedChildAlias(aliasedChildName, parentName, parentAlias, aliasedChildType); }, ); aliases.push(inheritedAlias); } } return aliases; } /** * Resource represents a class whose CRUD operations are implemented by a provider plugin. */ export abstract class Resource { /** * A regexp for use with sourcePosition. */ private static sourcePositionRegExp = /Error:\s*\n\s*at new Resource \(.*\)\n\s*at new \S*Resource \(.*\)\n(\s*at new \S* \(.*\)\n)?[^(]*\((?<file>.*):(?<line>[0-9]+):(?<col>[0-9]+)\)\n/; /** * A private field to help with RTTI that works in SxS scenarios. * @internal */ // eslint-disable-next-line @typescript-eslint/naming-convention,no-underscore-dangle,id-blacklist,id-match public readonly __pulumiResource: boolean = true; /** * The optional parent of this resource. * @internal */ // eslint-disable-next-line @typescript-eslint/naming-convention,no-underscore-dangle,id-blacklist,id-match public readonly __parentResource: Resource | undefined; /** * The child resources of this resource. We use these (only from a ComponentResource) to allow * code to dependOn a ComponentResource and have that effectively mean that it is depending on * all the CustomResource children of that component. * * Important! We only walk through ComponentResources. They're the only resources that serve * as an aggregation of other primitive (i.e. custom) resources. While a custom resource can be * a parent of other resources, we don't want to ever depend on those child resource. If we do, * it's simple to end up in a situation where we end up depending on a child resource that has a * data cycle dependency due to the data passed into it. * * An example of how this would be bad is: * * ```ts * var c1 = new CustomResource("c1"); * var c2 = new CustomResource("c2", { parentId: c1.id }, { parent: c1 }); * var c3 = new CustomResource("c3", { parentId: c1.id }, { parent: c1 }); * ``` * * The problem here is that 'c2' has a data dependency on 'c1'. If it tries to wait on 'c1' it * will walk to the children and wait on them. This will mean it will wait on 'c3'. But 'c3' * will be waiting in the same manner on 'c2', and a cycle forms. * * This normally does not happen with ComponentResources as they do not have any data flowing * into them. The only way you would be able to have a problem is if you had this sort of coding * pattern: * * ```ts * var c1 = new ComponentResource("c1"); * var c2 = new CustomResource("c2", { parentId: c1.urn }, { parent: c1 }); * var c3 = new CustomResource("c3", { parentId: c1.urn }, { parent: c1 }); * ``` * * However, this would be pretty nonsensical as there is zero need for a custom resource to ever * need to reference the urn of a component resource. So it's acceptable if that sort of * pattern failed in practice. * * @internal */ // eslint-disable-next-line @typescript-eslint/naming-convention,no-underscore-dangle,id-blacklist,id-match public __childResources: Set<Resource> | undefined; /** * urn is the stable logical URN used to distinctly address a resource, both before and after * deployments. */ public readonly urn!: Output<URN>; /** * When set to true, protect ensures this resource cannot be deleted. * @internal */ // eslint-disable-next-line @typescript-eslint/naming-convention,no-underscore-dangle,id-blacklist,id-match private readonly __protect: boolean; /** * A collection of transformations to apply as part of resource registration. * * Note: This is marked optional only because older versions of this library may not have had * this property, and marking optional forces consumers of the property to defensively handle * cases where they are passed "old" resources. * @internal */ // eslint-disable-next-line @typescript-eslint/naming-convention,no-underscore-dangle,id-blacklist,id-match __transformations?: ResourceTransformation[]; /** * A list of aliases applied to this resource. * * Note: This is marked optional only because older versions of this library may not have had * this property, and marking optional forces consumers of the property to defensively handle * cases where they are passed "old" resources. * @internal */ // eslint-disable-next-line @typescript-eslint/naming-convention,no-underscore-dangle,id-blacklist,id-match readonly __aliases?: Input<URN>[]; /** * The name assigned to the resource at construction. * * Note: This is marked optional only because older versions of this library may not have had * this property, and marking optional forces consumers of the property to defensively handle * cases where they are passed "old" resources. * @internal */ // eslint-disable-next-line @typescript-eslint/naming-convention,no-underscore-dangle,id-blacklist,id-match readonly __name?: string; /** * The set of providers to use for child resources. Keyed by package name (e.g. "aws"). * @internal */ // eslint-disable-next-line @typescript-eslint/naming-convention,no-underscore-dangle,id-blacklist,id-match private readonly __providers: Record<string, ProviderResource>; /** * The specified provider or provider determined from the parent for custom or remote resources. * It is passed along in the `Call` gRPC request for resource method calls (when set) so that the * call goes to the same provider as the resource. * @internal */ // Note: This is deliberately not named `__provider` as that conflicts with the property // used by the `dynamic.Resource` class. // eslint-disable-next-line @typescript-eslint/naming-convention,no-underscore-dangle,id-blacklist,id-match readonly __prov?: ProviderResource; /** * The specified provider version. * @internal */ // eslint-disable-next-line @typescript-eslint/naming-convention,no-underscore-dangle,id-blacklist,id-match readonly __version?: string; /** * The specified provider download URL. * @internal */ // eslint-disable-next-line @typescript-eslint/naming-convention,no-underscore-dangle,id-blacklist,id-match readonly __pluginDownloadURL?: string; /** * Private field containing the type ID for this object. Useful for implementing `isInstance` on * classes that inherit from `Resource`. * @internal */ // eslint-disable-next-line @typescript-eslint/naming-convention,no-underscore-dangle,id-blacklist,id-match public readonly __pulumiType: string; public static isInstance(obj: any): obj is Resource { return utils.isInstance<Resource>(obj, "__pulumiResource"); } /** * sourcePosition returns the source position of the user code that instantiated this resource. * * This is somewhat brittle in that it expects a call stack of the form: * - Resource class constructor * - abstract Resource subclass constructor * - concrete Resource subclass constructor * - user code * * This stack reflects the expected class hierarchy of "cloud resource / component resource < customresource/componentresource < resource". * * For example, consider the AWS S3 Bucket resource. When user code instantiates a Bucket, the stack will look like * this: * * new Resource (/path/to/resource.ts:123:45) * new CustomResource (/path/to/resource.ts:678:90) * new Bucket (/path/to/bucket.ts:987:65) * <user code> (/path/to/index.ts:4:3) * * Because Node can only give us the stack trace as text, we parse out the source position using a regex that * matches traces of this form (see stackTraceRegExp above). */ private static sourcePosition(): SourcePosition | undefined { const stackObj: any = {}; Error.captureStackTrace(stackObj, Resource.sourcePosition); // Parse out the source position of the user code. If any part of the match is missing, return undefined. const { file, line, col } = Resource.sourcePositionRegExp.exec(stackObj.stack)?.groups || {}; if (!file || !line || !col) { return undefined; } // Parse the line and column numbers. If either fails to parse, return undefined. // // Note: this really shouldn't happen given the regex; this is just a bit of defensive coding. const lineNum = parseInt(line, 10); const colNum = parseInt(col, 10); if (Number.isNaN(lineNum) || Number.isNaN(colNum)) { return undefined; } return { uri: url.pathToFileURL(file).toString(), line: lineNum, column: colNum, }; } /** * Returns the provider for the given module member, if one exists. */ public getProvider(moduleMember: string): ProviderResource | undefined { const pkg = pkgFromType(moduleMember); if (pkg === undefined) { return undefined; } return this.__providers[pkg]; } /** * Creates and registers a new resource object. [t] is the fully qualified type token and * [name] is the "name" part to use in creating a stable and globally unique URN for the object. * dependsOn is an optional list of other resources that this resource depends on, controlling * the order in which we perform resource operations. * * @param t The type of the resource. * @param name The _unique_ name of the resource. * @param custom True to indicate that this is a custom resource, managed by a plugin. * @param props The arguments to use to populate the new resource. * @param opts A bag of options that control this resource's behavior. * @param remote True if this is a remote component resource. * @param dependency True if this is a synthetic resource used internally for dependency tracking. */ constructor( t: string, name: string, custom: boolean, props: Inputs = {}, opts: ResourceOptions = {}, remote: boolean = false, dependency: boolean = false, ) { this.__pulumiType = t; if (dependency) { this.__protect = false; this.__providers = {}; return; } if (opts.parent && !Resource.isInstance(opts.parent)) { throw new Error(`Resource parent is not a valid Resource: ${opts.parent}`); } if (!t) { throw new ResourceError("Missing resource type argument", opts.parent); } if (!name) { throw new ResourceError("Missing resource name argument (for URN creation)", opts.parent); } // Before anything else - if there are transformations registered, invoke them in order to transform the properties and // options assigned to this resource. const parent = opts.parent || getStackResource(); this.__transformations = [...(opts.transformations || []), ...(parent?.__transformations || [])]; for (const transformation of this.__transformations) { const tres = transformation({ resource: this, type: t, name, props, opts }); if (tres) { if (tres.opts.parent !== opts.parent) { // This is currently not allowed because the parent tree is needed to establish what // transformation to apply in the first place, and to compute inheritance of other // resource options in the Resource constructor before transformations are run (so // modifying it here would only even partially take affect). It's theoretically // possible this restriction could be lifted in the future, but for now just // disallow re-parenting resources in transformations to be safe. throw new Error("Transformations cannot currently be used to change the `parent` of a resource."); } props = tres.props; opts = tres.opts; } } this.__name = name; // Make a shallow clone of opts to ensure we don't modify the value passed in. opts = Object.assign({}, opts); // Check the parent type if one exists and fill in any default options. this.__providers = {}; if (parent) { this.__parentResource = parent; this.__parentResource.__childResources = this.__parentResource.__childResources || new Set(); this.__parentResource.__childResources.add(this); if (opts.protect === undefined) { opts.protect = parent.__protect; } this.__providers = parent.__providers; } // providers is found by combining (in ascending order of priority) // 1. provider // 2. self_providers // 3. opts.providers this.__providers = { ...this.__providers, ...convertToProvidersMap((<ComponentResourceOptions>opts).providers), ...convertToProvidersMap(opts.provider ? [opts.provider] : {}), }; const pkg = pkgFromType(t); // provider is the first option that does not return none // 1. opts.provider // 2. a matching provider in opts.providers // 3. a matching provider inherited from opts.parent if ((custom || remote) && opts.provider === undefined) { const parentProvider = parent?.getProvider(t); if (pkg && pkg in this.__providers) { opts.provider = this.__providers[pkg]; } else if (parentProvider) { opts.provider = parentProvider; } } // Custom and remote resources have a backing provider. If this is a custom or // remote resource and a provider has been specified that has the same package // as the resource's package, save it in `__prov`. // If the provider's package isn't the same as the resource's package, don't // save it in `__prov` because the user specified `provider: someProvider` as // shorthand for `providers: [someProvider]`, which is a provider intended for // the component's children and not for this resource itself. // `__prov` is passed along in `Call` gRPC requests for resource method calls // (when set) so that the call goes to the same provider as the resource. if ((custom || remote) && opts.provider) { if (pkg && pkg === opts.provider.getPackage()) { this.__prov = opts.provider; } } this.__protect = !!opts.protect; this.__version = opts.version; this.__pluginDownloadURL = opts.pluginDownloadURL; // Collapse any `Alias`es down to URNs. We have to wait until this point to do so because we do not know the // default `name` and `type` to apply until we are inside the resource constructor. this.__aliases = []; if (opts.aliases) { for (const alias of opts.aliases) { this.__aliases.push(collapseAliasToUrn(alias, name, t, parent)); } } const sourcePosition = Resource.sourcePosition(); if (opts.urn) { // This is a resource that already exists. Read its state from the engine. getResource(this, parent, props, custom, opts.urn); } else if (opts.id) { // If this is a custom resource that already exists, read its state from the provider. if (!custom) { throw new ResourceError( "Cannot read an existing resource unless it has a custom provider", opts.parent, ); } readResource(this, parent, t, name, props, opts, sourcePosition); } else { // Kick off the resource registration. If we are actually performing a deployment, this // resource's properties will be resolved asynchronously after the operation completes, so // that dependent computations resolve normally. If we are just planning, on the other // hand, values will never resolve. registerResource( this, parent, t, name, custom, remote, (urn) => new DependencyResource(urn), props, opts, sourcePosition, ); } } } function convertToProvidersMap(providers: Record<string, ProviderResource> | ProviderResource[] | undefined) { if (!providers) { return {}; } if (!Array.isArray(providers)) { return providers; } const result: Record<string, ProviderResource> = {}; for (const provider of providers) { result[provider.getPackage()] = provider; } return result; } (<any>Resource).doNotCapture = true; /** * Constant to represent the 'root stack' resource for a Pulumi application. The purpose of this is * solely to make it easy to write an [Alias] like so: * * `aliases: [{ parent: rootStackResource }]`. * * This indicates that the prior name for a resource was created based on it being parented directly * by the stack itself and no other resources. Note: this is equivalent to: * * `aliases: [{ parent: undefined }]` * * However, the former form is preferable as it is more self-descriptive, while the latter may look * a bit confusing and may incorrectly look like something that could be removed without changing * semantics. */ export const rootStackResource: Resource = undefined!; /** * Alias is a partial description of prior named used for a resource. It can be processed in the * context of a resource creation to determine what the full aliased URN would be. * * Note there is a semantic difference between properties being absent from this type and properties * having the `undefined` value. Specifically, there is a difference between: * * ```ts * { name: "foo", parent: undefined } // and * { name: "foo" } * ``` * * The presence of a property indicates if its value should be used. If absent, then the value is * not used. So, in the above while `alias.parent` is `undefined` for both, the first alias means * "the original urn had no parent" while the second alias means "use the current parent". * * Note: to indicate that a resource was previously parented by the root stack, it is recommended * that you use: * * `aliases: [{ parent: pulumi.rootStackResource }]` * * This form is self-descriptive and makes the intent clearer than using: * * `aliases: [{ parent: undefined }]` */ export interface Alias { /** * The previous name of the resource. If not provided, the current name of the resource is * used. */ name?: Input<string>; /** * The previous type of the resource. If not provided, the current type of the resource is used. */ type?: Input<string>; /** * The previous parent of the resource. If not provided (i.e. `{ name: "foo" }`), the current * parent of the resource is used (`opts.parent` if provided, else the implicit stack resource * parent). * * To specify no original parent, use `{ parent: pulumi.rootStackResource }`. */ parent?: Resource | Input<URN>; /** * The previous stack of the resource. If not provided, defaults to `pulumi.getStack()`. */ stack?: Input<string>; /** * The previous project of the resource. If not provided, defaults to `pulumi.getProject()`. */ project?: Input<string>; } /** * Converts an alias into a URN given a set of default data for the missing * values. */ function collapseAliasToUrn( alias: Input<Alias | string>, defaultName: string, defaultType: string, defaultParent: Resource | undefined, ): Output<URN> { return output(alias).apply((a) => { if (typeof a === "string") { return output(a); } const name = a.hasOwnProperty("name") ? a.name : defaultName; const type = a.hasOwnProperty("type") ? a.type : defaultType; const parent = a.hasOwnProperty("parent") ? a.parent : defaultParent; const project = a.hasOwnProperty("project") ? a.project : getProject(); const stack = a.hasOwnProperty("stack") ? a.stack : getStack(); if (name === undefined) { throw new Error("No valid 'name' passed in for alias."); } if (type === undefined) { throw new Error("No valid 'type' passed in for alias."); } return createUrn(name, type, parent, project, stack); }); } /** * ResourceOptions is a bag of optional settings that control a resource's behavior. */ export interface ResourceOptions { // !!! IMPORTANT !!! If you add a new field to this type, make sure to add test that verifies that // mergeOptions works properly for it. Also be sure to update the logic in callbacks.ts that marshals to // and from this type to the wire protocol. /** * An optional existing ID to load, rather than create. */ id?: Input<ID>; /** * An optional parent resource to which this resource belongs. */ parent?: Resource; /** * An optional additional explicit dependencies on other resources. */ dependsOn?: Input<Input<Resource>[]> | Input<Resource>; /** * When set to true, protect ensures this resource cannot be deleted. */ protect?: boolean; /** * Ignore changes to any of the specified properties. */ ignoreChanges?: string[]; /** * Changes to any of these property paths will force a replacement. If this list includes `"*"`, changes to any * properties will force a replacement. Initialization errors from previous deployments will require replacement * instead of update only if `"*"` is passed. */ replaceOnChanges?: string[]; /** * An optional version, corresponding to the version of the provider plugin that should be used when operating on * this resource. This version overrides the version information inferred from the current package and should * rarely be used. */ version?: string; /** * An optional list of aliases to treat this resource as matching. */ aliases?: Input<URN | Alias>[]; /** * An optional provider to use for this resource's CRUD operations. If no provider is supplied, * the default provider for the resource's package will be used. The default provider is pulled * from the parent's provider bag (see also ComponentResourceOptions.providers). * * If this is a [ComponentResourceOptions] do not provide both [provider] and [providers] */ provider?: ProviderResource; /** * An optional customTimeouts configuration block. */ customTimeouts?: CustomTimeouts; /** * Optional list of transformations to apply to this resource during construction. The * transformations are applied in order, and are applied prior to transformation applied to * parents walking from the resource up to the stack. */ transformations?: ResourceTransformation[]; /** * Optional list of transforms to apply to this resource during construction. The * transforms are applied in order, and are applied prior to transforms applied to * parents walking from the resource up to the stack. * * This property is experimental. */ transforms?: ResourceTransform[]; /** * The URN of a previously-registered resource of this type to read from the engine. */ urn?: URN; /** * An option to specify the URL from which to download this resources * associated plugin. This version overrides the URL information inferred * from the current package and should rarely be used. */ pluginDownloadURL?: string; /** * If set to True, the providers Delete method will not be called for this resource. */ retainOnDelete?: boolean; /** * If set, the providers Delete method will not be called for this resource * if specified is being deleted as well. */ deletedWith?: Resource; // !!! IMPORTANT !!! If you add a new field to this type, make sure to add test that verifies // that mergeOptions works properly for it. } export interface CustomTimeouts { /** * The optional create timeout represented as a string e.g. 5m, 40s, 1d. */ create?: string; /** * The optional update timeout represented as a string e.g. 5m, 40s, 1d. */ update?: string; /** * The optional delete timeout represented as a string e.g. 5m, 40s, 1d. */ delete?: string; } /** * ResourceTransformation is the callback signature for the `transformations` resource option. A * transformation is passed the same set of inputs provided to the `Resource` constructor, and can * optionally return back alternate values for the `props` and/or `opts` prior to the resource * actually being created. The effect will be as though those props and opts were passed in place * of the original call to the `Resource` constructor. If the transformation returns undefined, * this indicates that the resource will not be transformed. */ export type ResourceTransformation = (args: ResourceTransformationArgs) => ResourceTransformationResult | undefined; /** * ResourceTransform is the callback signature for the `transforms` resource option. A * transform is passed the same set of inputs provided to the `Resource` constructor, and can * optionally return back alternate values for the `props` and/or `opts` prior to the resource * actually being created. The effect will be as though those props and opts were passed in place * of the original call to the `Resource` constructor. If the transform returns undefined, * this indicates that the resource will not be transformed. */ export type ResourceTransform = ( args: ResourceTransformArgs, ) => Promise<ResourceTransformResult | undefined> | ResourceTransformResult | undefined; /** * ResourceTransformArgs is the argument bag passed to a resource transform. */ export interface ResourceTransformArgs { /** * If the resource is a custom or component resource. */ custom: boolean; /** * The type of the Resource. */ type: string; /** * The name of the Resource. */ name: string; /** * The original properties passed to the Resource constructor. */ props: Inputs; /** * The original resource options passed to the Resource constructor. */ opts: ResourceOptions; } /** * ResourceTransformResult is the result that must be returned by a resource transformation * callback. It includes new values to use for the `props` and `opts` of the `Resource` in place of * the originally provided values. */ export interface ResourceTransformResult { /** * The new properties to use in place of the original `props` */ props: Inputs; /** * The new resource options to use in place of the original `opts` */ opts: ResourceOptions; } /** * ResourceTransformationArgs is the argument bag passed to a resource transformation. */ export interface ResourceTransformationArgs { /** * The Resource instance that is being transformed. */ resource: Resource; /** * The type of the Resource. */ type: string; /** * The name of the Resource. */ name: string; /** * The original properties passed to the Resource constructor. */ props: Inputs; /** * The original resource options passed to the Resource constructor. */ opts: ResourceOptions; } /** * ResourceTransformationResult is the result that must be returned by a resource transformation * callback. It includes new values to use for the `props` and `opts` of the `Resource` in place of * the originally provided values. */ export interface ResourceTransformationResult { /** * The new properties to use in place of the original `props` */ props: Inputs; /** * The new resource options to use in place of the original `opts` */ opts: ResourceOptions; } /** * CustomResourceOptions is a bag of optional settings that control a custom resource's behavior. */ export interface CustomResourceOptions extends ResourceOptions { // !!! IMPORTANT !!! If you add a new field to this type, make sure to add test that verifies // that mergeOptions works properly for it. /** * When set to true, deleteBeforeReplace indicates that this resource should be deleted before its replacement * is created when replacement is necessary. */ deleteBeforeReplace?: boolean; /** * The names of outputs for this resource that should be treated as secrets. This augments the list that * the resource provider and pulumi engine already determine based on inputs to your resource. It can be used * to mark certain ouputs as a secrets on a per resource basis. */ additionalSecretOutputs?: string[]; /** * When provided with a resource ID, import indicates that this resource's provider should import its state from * the cloud resource with the given ID. The inputs to the resource's constructor must align with the resource's * current state. Once a resource has been imported, the import property must be removed from the resource's * options. */ import?: ID; // !!! IMPORTANT !!! If you add a new field to this type, make sure to add test that verifies // that mergeOptions works properly for it. } /** * ComponentResourceOptions is a bag of optional settings that control a component resource's behavior. */ export interface ComponentResourceOptions extends ResourceOptions { // !!! IMPORTANT !!! If you add a new field to this type, make sure to add test that verifies // that mergeOptions works properly for it. /** * An optional set of providers to use for child resources. Either keyed by package name (e.g. * "aws"), or just provided as an array. In the latter case, the package name will be retrieved * from the provider itself. * * Note: only a list should be used. Mapping keys are not respected. */ providers?: Record<string, ProviderResource> | ProviderResource[]; // !!! IMPORTANT !!! If you add a new field to this type, make sure to add test that verifies // that mergeOptions works properly for it. } /** * CustomResource is a resource whose create, read, update, and delete (CRUD) operations are managed * by performing external operations on some physical entity. The engine understands how to diff * and perform partial updates of them, and these CRUD operations are implemented in a dynamically * loaded plugin for the defining package. */ export abstract class CustomResource extends Resource { /** * A private field to help with RTTI that works in SxS scenarios. * @internal */ // eslint-disable-next-line @typescript-eslint/naming-convention,no-underscore-dangle,id-blacklist,id-match public readonly __pulumiCustomResource: boolean; /** * id is the provider-assigned unique ID for this managed resource. It is set during * deployments and may be missing (undefined) during planning phases. */ public readonly id!: Output<ID>; /** * Returns true if the given object is an instance of CustomResource. This is designed to work even when * multiple copies of the Pulumi SDK have been loaded into the same process. */ public static isInstance(obj: any): obj is CustomResource { return utils.isInstance<CustomResource>(obj, "__pulumiCustomResource"); } /** * Creates and registers a new managed resource. t is the fully qualified type token and name * is the "name" part to use in creating a stable and globally unique URN for the object. * dependsOn is an optional list of other resources that this resource depends on, controlling * the order in which we perform resource operations. Creating an instance does not necessarily * perform a create on the physical entity which it represents, and instead, this is dependent * upon the diffing of the new goal state compared to the current known resource state. * * @param t The type of the resource. * @param name The _unique_ name of the resource. * @param props The arguments to use to populate the new resource. * @param opts A bag of options that control this resource's behavior. * @param dependency True if this is a synthetic resource used internally for dependency tracking. */ constructor(t: string, name: string, props?: Inputs, opts: CustomResourceOptions = {}, dependency = false) { if ((<ComponentResourceOptions>opts).providers) { throw new ResourceError( "Do not supply 'providers' option to a CustomResource. Did you mean 'provider' instead?", opts.parent, ); } super(t, name, true, props, opts, false, dependency); this.__pulumiCustomResource = true; } } (<any>CustomResource).doNotCapture = true; /** * ProviderResource is a resource that implements CRUD operations for other custom resources. These resources are * managed similarly to other resources, including the usual diffing and update semantics. */ export abstract class ProviderResource extends CustomResource { /** @internal */ private readonly pkg: string; /** @internal */ // eslint-disable-next-line @typescript-eslint/naming-convention,no-underscore-dangle,id-blacklist,id-match public __registrationId?: string; public static async register(provider: ProviderResource | undefined): Promise<string | undefined> { if (provider === undefined) { return undefined; } if (!provider.__registrationId) { const providerURN = await provider.urn.promise(); const providerID = (await provider.id.promise()) || unknownValue; provider.__registrationId = `${providerURN}::${providerID}`; } return provider.__registrationId; } /** * Creates and registers a new provider resource for a particular package. * * @param pkg The package associated with this provider. * @param name The _unique_ name of the provider. * @param props The configuration to use for this provider. * @param opts A bag of options that control this provider's behavior. * @param dependency True if this is a synthetic resource used internally for dependency tracking. */ constructor(pkg: string, name: string, props?: Inputs, opts: ResourceOptions = {}, dependency: boolean = false) { super(`pulumi:providers:${pkg}`, name, props, opts, dependency); this.pkg = pkg; } /** @internal */ public getPackage() { return this.pkg; } } /** * ComponentResource is a resource that aggregates one or more other child resources into a higher * level abstraction. The component resource itself is a resource, but does not require custom CRUD * operations for provisioning. */ export class ComponentResource<TData = any> extends Resource { /** * A private field to help with RTTI that works in SxS scenarios. * @internal */ // eslint-disable-next-line @typescript-eslint/naming-convention,no-underscore-dangle,id-blacklist,id-match public readonly __pulumiComponentResource = true; /** @internal */ // eslint-disable-next-line @typescript-eslint/naming-convention,no-underscore-dangle,id-blacklist,id-match public readonly __data: Promise<TData>; /** @internal */ // eslint-disable-next-line @typescript-eslint/naming-convention,no-underscore-dangle,id-blacklist,id-match private __registered = false; /** @internal */ // eslint-disable-next-line @typescript-eslint/naming-convention,no-underscore-dangle,id-blacklist,id-match public readonly __remote: boolean; /** * Returns true if the given object is an instance of CustomResource. This is designed to work even when * multiple copies of the Pulumi SDK have been loaded into the same process. */ public static isInstance(obj: any): obj is ComponentResource { return utils.isInstance<ComponentResource>(obj, "__pulumiComponentResource"); } /** * Creates and registers a new component resource. [type] is the fully qualified type token and * [name] is the "name" part to use in creating a stable and globally unique URN for the object. * [opts.parent] is the optional parent for this component, and [opts.dependsOn] is an optional * list of other resources that this resource depends on, controlling the order in which we * perform resource operations. * * @param type The type of the resource. * @param name The _unique_ name of the resource. * @param args Information passed to [initialize] method. * @param opts A bag of options that control this resource's behavior. * @param remote True if this is a remote component resource. */ constructor( type: string, name: string, args: Inputs = {}, opts: ComponentResourceOptions = {}, remote: boolean = false, ) { // Explicitly ignore the props passed in. We allow them for back compat reasons. However, // we explicitly do not want to pass them along to the engine. The ComponentResource acts // only as a container for other resources. Another way to think about this is that a normal // 'custom resource' corresponds to real piece of cloud infrastructure. So, when it changes // in some way, the cloud resource needs to be updated (and vice versa). That is not true // for a component resource. The component is just used for organizational purposes and does // not correspond to a real piece of cloud infrastructure. As such, changes to it *itself* // do not have any effect on the cloud side of things at all. super(type, name, /*custom:*/ false, /*props:*/ remote || opts?.urn ? args : {}, opts, remote); this.__remote = remote; this.__registered = remote || !!opts?.urn; this.__data = remote || opts?.urn ? Promise.resolve(<TData>{}) : this.initializeAndRegisterOutputs(args); } /** @internal */ private async initializeAndRegisterOutputs(args: Inputs) { const data = await this.initialize(args); this.registerOutputs(); return data; } /** * Can be overridden by a subclass to asynchronously initialize data for this Component * automatically when constructed. The data will be available immediately for subclass * constructors to use. To access the data use `.getData`. */ protected async initialize(args: Inputs): Promise<TData> { return <TData>undefined!; } /** * Retrieves the data produces by [initialize]. The data is immediately available in a * derived class's constructor after the `super(...)` call to `ComponentResource`. */ protected getData(): Promise<TData> { return this.__data; } /** * registerOutputs registers synthetic outputs that a component has initialized, usually by * allocating other child sub-resources and propagating their resulting property values. * * ComponentResources can call this at the end of their constructor to indicate that they are * done creating child resources. This is not strictly necessary as this will automatically be * called after the `initialize` method completes. */ protected registerOutputs(outputs?: Inputs | Promise<Inputs> | Output<Inputs>): void { if (this.__registered) { return; } this.__registered = true; registerResourceOutputs(this, outputs || {}); } } (<any>ComponentResource).doNotCapture = true; (<any>ComponentResource.prototype).registerOutputs.doNotCapture = true; (<any>ComponentResource.prototype).initialize.doNotCapture = true; (<any>ComponentResource.prototype).initializeAndRegisterOutputs.doNotCapture = true; /** @internal */ export const testingOptions = { isDryRun: false, }; /** * [mergeOptions] takes two ResourceOptions values and produces a new ResourceOptions with the * respective properties of `opts2` merged over the same properties in `opts1`. The original * options objects will be unchanged. * * Conceptually property merging follows these basic rules: * 1. if the property is a collection, the final value will be a collection containing the values * from each options object. * 2. Simple scaler values from `opts2` (i.e. strings, numbers, bools) will replace the values of * `opts1`. * 3. `opts2` can have properties explicitly provided with `null` or `undefined` as the value. If * explicitly provided, then that will be the final value in the result. * 4. For the purposes of merging `dependsOn`, `provider` and `providers` are always treated as * collections, even if only a single value was provided. */ export function mergeOptions( opts1: CustomResourceOptions | undefined, opts2: CustomResourceOptions | undefined, ): CustomResourceOptions; export function mergeOptions( opts1: ComponentResourceOptions | undefined, opts2: ComponentResourceOptions | undefined, ): ComponentResourceOptions; export function mergeOptions(opts1: ResourceOptions | undefined, opts2: ResourceOptions | undefined): ResourceOptions; export function mergeOptions(opts1: ResourceOptions | undefined, opts2: ResourceOptions | undefined): ResourceOptions { const dest = <any>{ ...opts1 }; const source = <any>{ ...opts2 }; // Ensure provider/providers are all expanded into the `ProviderResource[]` form. // This makes merging simple. expandProviders(dest); expandProviders(source); // iterate specifically over the supplied properties in [source]. Note: there may not be an // corresponding value in [dest]. for (const key of Object.keys(source)) { const destVal = dest[key]; const sourceVal = source[key]; // For 'dependsOn' we might have singleton resources in both options bags. We // want to make sure we combine them into a collection. if (key === "dependsOn") { dest[key] = merge(destVal, sourceVal, /*alwaysCreateArray:*/ true); continue; } dest[key] = merge(destVal, sourceVal, /*alwaysCreateArray:*/ false); } // Now, if we are left with a .providers that is just a single key/value pair, then // collapse that down into .provider form. normalizeProviders(dest); return dest; } function isPromiseOrOutput(val: any): boolean { return val instanceof Promise || Output.isInstance(val); } /** @internal */ export function expandProviders(options: ComponentResourceOptions) { // Convert 'providers' map to array form. if (options.providers && !Array.isArray(options.providers)) { for (const k in options.providers) { if (Object.prototype.hasOwnProperty.call(options.providers, k)) { const v = options.providers[k]; if (k !== v.getPackage()) { const message = `provider resource map where key ${k} doesn't match provider ${v.getPackage()}`; log.warn(message); } } } options.providers = utils.values(options.providers); } } function normalizeProviders(opts: ComponentResourceOptions) { // If we have 0 providers, delete providers. Otherwise, convert providers into a map. const providers = <ProviderResource[]>opts.providers; if (providers) { if (providers.length === 0) { opts.providers = undefined; } else { opts.providers = {}; for (const res of providers) { opts.providers[res.getPackage()] = res; } } } } /** @internal for testing purposes. */ export function merge(dest: any, source: any, alwaysCreateArray: boolean): any { // unwind any top level promise/outputs. if (isPromiseOrOutput(dest)) { return output(dest).apply((d) => merge(d, source, alwaysCreateArray)); } if (isPromiseOrOutput(source)) { return output(source).apply((s) => merge(dest, s, alwaysCreateArray)); } // If either are an array, make a new array and merge the values into it. // Otherwise, just overwrite the destination with the source value. if (alwaysCreateArray || Array.isArray(dest) || Array.isArray(source)) { const result: any[] = []; addToArray(result, dest); addToArray(result, source); return result; } return source; } function addToArray(resultArray: any[], value: any) { if (Array.isArray(value)) { resultArray.push(...value); } else if (value !== undefined && value !== null) { resultArray.push(value); } } /** * A DependencyResource is a resource that is used to indicate that an Output has a dependency on a particular * resource. These resources are only created when dealing with remote component resources. */ export class DependencyResource extends CustomResource { constructor(urn: URN) { super("", "", {}, {}, true); (<any>this).urn = new Output( <any>this, Promise.resolve(urn), Promise.resolve(true), Promise.resolve(false), Promise.resolve([]), ); } } /** * A DependencyProviderResource is a resource that is used by the provider SDK as a stand-in for a provider that * is only used for its reference. Its only valid properties are its URN and ID. */ export class DependencyProviderResource extends ProviderResource { constructor(ref: string) { const [urn, id] = parseResourceReference(ref); const urnParts = urn.split("::"); const qualifiedType = urnParts[2]; const type = qualifiedType.split("$").pop()!; // type will be "pulumi:providers:<package>" and we want the last part. const typeParts = type.split(":"); const pkg = typeParts.length > 2 ? typeParts[2] : ""; super(pkg, "", {}, {}, true); (<any>this).urn = new Output( <any>this, Promise.resolve(urn), Promise.resolve(true), Promise.resolve(false), Promise.resolve([]), ); (<any>this).id = new Output( <any>this, Promise.resolve(id), Promise.resolve(true), Promise.resolve(false), Promise.resolve([]), ); } } /** * parseResourceReference parses the URN and ID out of the provider reference. * @internal */ export function parseResourceReference(ref: string): [string, string] { const lastSep = ref.lastIndexOf("::"); if (lastSep === -1) { throw new Error(`expected '::' in provider reference ${ref}`); } const urn = ref.slice(0, lastSep); const id = ref.slice(lastSep + 2); return [urn, id]; }