packages/@alicloud/ros-cdk-core/lib/ros-resource.ts (231 lines of code) (raw):

import { RosDeletionPolicy } from "./ros-resource-policy"; import { capitalizePropertyNames, ignoreEmpty, PostResolveToken } from "./util"; import { Construct, IConstruct } from "./construct-compat"; import { TagManager } from "./tag-manager"; import { Reference } from "./reference"; import { addDependency } from "./deps"; import { RosReference } from "./private/ros-reference"; import { RemovalPolicy, RemovalPolicyOptions } from "./ros-removal-policy"; import { RosCondition } from "./ros-condition"; import { RosRefElement } from "./ros-element"; import {IResolvable} from "./resolvable"; export interface RosResourceProps { /** * ROS template resource type (e.g. `ALIYUN::ECS::Instance`). */ readonly type: string; /** * Resource properties. * * @default - No resource properties. */ readonly properties?: { [name: string]: any }; } /** * Represents a ROS resource. */ export class RosResource extends RosRefElement { /** * Check whether the given construct is a RosResource */ public static isRosResource(construct: IConstruct): construct is RosResource { return (construct as any).rosResourceType !== undefined; } // MAINTAINERS NOTE: this class serves as the base class for the generated L1 // ("ALIYUN") resources (such as `ecs.Instance`). These resources will have a // property for each ROS property of the resource. This means that // if at some point in the future a property is introduced with a name similar // to one of the properties here, it will be "masked" by the derived class. To // that end, we prefix all properties in this class with `rosXxx` with the // hope to avoid those conflicts in the future. /** * Options for this resource, such as condition, update policy etc. */ public readonly rosOptions: IRosResourceOptions = {}; /** * ROS resource type. */ public readonly rosResourceType: string; /** * Aliyun resource properties. * @internal */ protected readonly _rosProperties: any; /** * An object to be merged on top of the entire resource definition. */ private readonly rawOverrides: any = {}; /** * Logical IDs of dependencies. * * Is filled during prepare(). */ private readonly dependsOn = new Set<RosResource>(); private readonly id: string; private readonly rosDependsOn = new Set<string>(); /** * Creates a resource construct. * @param rosResourceType The ROS type of this resource (e.g. ALIYUN::ECS::Instance) */ constructor(scope: Construct, id: string, props: RosResourceProps) { super(scope, id); this.id = id; if (!props.type) { throw new Error("The `type` property is required"); } this.rosResourceType = props.type; this._rosProperties = props.properties || {}; } /** * Sets the deletion policy of the resource based on the removal policy specified. */ public applyRemovalPolicy( policy: RemovalPolicy | undefined, options: RemovalPolicyOptions = {} ) { policy = policy || options.defaultPolicy || RemovalPolicy.RETAIN; let deletionPolicy; switch (policy) { case RemovalPolicy.DESTROY: deletionPolicy = RosDeletionPolicy.DELETE; break; case RemovalPolicy.RETAIN: deletionPolicy = RosDeletionPolicy.RETAIN; break; default: throw new Error(`Invalid removal policy: ${policy}`); } this.rosOptions.deletionPolicy = deletionPolicy; // if (options.applyToUpdateReplacePolicy !== false) { // this.rosOptions.updateReplacePolicy = deletionPolicy; // } } /** * Returns a token for an runtime attribute of this resource. * Ideally, use generated attribute accessors (e.g. `resource.arn`), but this can be used for future compatibility * in case there is no generated attribute. * @param attributeName The name of the attribute. */ public getAtt(attributeName: string): Reference { return RosReference.for(this, attributeName); } /** * Adds an override to the synthesized ROS resource. To add a * property override, either use `addPropertyOverride` or prefix `path` with * "Properties." (i.e. `Properties.TopicName`). * * If the override is nested, separate each nested level using a dot (.) in the path parameter. * If there is an array as part of the nesting, specify the index in the path. * * For example, * ```typescript * addOverride('Properties.GlobalSecondaryIndexes.0.Projection.NonKeyAttributes', ['myattribute']) * addOverride('Properties.GlobalSecondaryIndexes.1.ProjectionType', 'INCLUDE') * ``` * would add the overrides * ```json * "Properties": { * "GlobalSecondaryIndexes": [ * { * "Projection": { * "NonKeyAttributes": [ "myattribute" ] * ... * } * ... * }, * { * "ProjectionType": "INCLUDE" * ... * }, * ] * ... * } * ``` * * @param path - The path of the property, you can use dot notation to * override values in complex types. Any intermdediate keys * will be created as needed. * @param value - The value. Could be primitive or complex. */ public addOverride(path: string, value: any) { const parts = path.split("."); let curr: any = this.rawOverrides; while (parts.length > 1) { const key = parts.shift()!; // if we can't recurse further or the previous value is not an // object overwrite it with an object. const isObject = curr[key] != null && typeof curr[key] === "object" && !Array.isArray(curr[key]); if (!isObject) { curr[key] = {}; } curr = curr[key]; } const lastKey = parts.shift()!; curr[lastKey] = value; } /** * Syntactic sugar for `addOverride(path, undefined)`. * @param path The path of the value to delete */ public addDeletionOverride(path: string) { this.addOverride(path, undefined); } public addMetaData(key: string, value: any) { if (!this.rosOptions.metadata) { this.rosOptions.metadata = {}; } this.rosOptions.metadata[key] = value; } public addDesc(desc: string) { this.rosOptions.description = desc; } public fetchDesc() { return this.rosOptions.description; } public addCondition(con: RosCondition) { this.rosOptions.condition = con; } public fetchCondition() { return this.rosOptions.condition; } public addCount(count: number | IResolvable) { this.rosOptions.count = count; } /** * Adds an override to a resource property. * * Syntactic sugar for `addOverride("Properties.<...>", value)`. * * @param propertyPath The path of the property * @param value The value */ public addPropertyOverride(propertyPath: string, value: any) { this.addOverride(`Properties.${propertyPath}`, value); } /** * Adds an override that deletes the value of a property from the resource definition. * @param propertyPath The path to the property. */ public addPropertyDeletionOverride(propertyPath: string) { this.addPropertyOverride(propertyPath, undefined); } /** * Indicates that this resource depends on another resource and cannot be * provisioned unless the other resource has been successfully provisioned. * * This can be used for resources across stacks (or nested stack) boundaries * and the dependency will automatically be transferred to the relevant scope. */ public addDependsOn(target: RosResource) { addDependency( this, target, `"${this.node.path}" depends on "${target.node.path}"` ); } /** * @returns a string representation of this resource */ public toString() { return `${super.toString()} [${this.rosResourceType}]`; } /** * Called by the `addDependency` helper function in order to realize a direct * dependency between two resources that are directly defined in the same * stacks. * * Use `resource.addDependsOn` to define the dependency between two resources, * which also takes stack boundaries into account. * * @internal */ public _addResourceDependency(target: RosResource) { this.dependsOn.add(target); } public addRosDependency(target: string) { // target.forEach(x => this.rosDependsOn.add(x)); this.rosDependsOn.add(target); } public fetchRosDependency() { return Array.from(this.rosDependsOn); } /** * Emits ROS template for this resource. * @internal */ public _toRosTemplate(): object { try { const ret = { Resources: { // Post-Resolve operation since otherwise deepMerge is going to mix values into // the Token objects returned by ignoreEmpty. [this.id]: new PostResolveToken( { Type: this.rosResourceType, Properties: ignoreEmpty(this.rosProperties), DependsOn: ignoreEmpty(renderRosDependsOn(this.rosDependsOn)), // UpdatePolicy: capitalizePropertyNames(this, this.rosOptions.updatePolicy), // UpdateReplacePolicy: capitalizePropertyNames(this, this.rosOptions.updateReplacePolicy), DeletionPolicy: capitalizePropertyNames( this, this.rosOptions.deletionPolicy ), Metadata: ignoreEmpty(this.rosOptions.metadata), Description: ignoreEmpty(this.rosOptions.description), Condition: this.rosOptions.condition && this.rosOptions.condition.logicalId, Count: ignoreEmpty(this.rosOptions.count), }, (props) => { const renderedProps = this.renderProperties( props.Properties || {} ); props.Properties = renderedProps && (Object.values(renderedProps).find((v) => !!v) ? renderedProps : undefined); return deepMerge(props, this.rawOverrides); } ), }, }; return ret; } catch (e) { // Change message e.message = `While synthesizing ${this.node.path}: ${e.message}`; // Adjust stack trace (make it look like node built it, too...) const trace = this.creationStack; if (trace) { const creationStack = ["--- resource created at ---", ...trace].join( "\n at " ); const problemTrace = e.stack.substr( e.stack.indexOf(e.message) + e.message.length ); e.stack = `${e.message}\n ${creationStack}\n --- problem discovered at ---${problemTrace}`; } // Re-throw throw e; } function renderRosDependsOn(rosDependsOn: Set<string>) { return Array.from(rosDependsOn); } } protected get rosProperties(): { [key: string]: any } { const props = this._rosProperties || {}; if (TagManager.isTaggable(this)) { const tagsProp: { [key: string]: any } = {}; tagsProp[this.tags.tagPropertyName] = this.tags.renderTags(); return deepMerge(props, tagsProp); } return props; } protected renderProperties(props: { [key: string]: any; }): { [key: string]: any } { return props; } /** * Return properties modified after initiation * * Resources that expose mutable properties should override this function to * collect and return the properties object for this resource. */ protected get updatedProperites(): { [key: string]: any } { return this._rosProperties; } protected validateProperties(_properties: any) { // Nothing } } export interface IRosResourceOptions { /** * A condition to associate with this resource. This means that only if the condition evaluates to 'true' when the stack * is deployed, the resource will be included. This is provided to allow CDK projects to produce legacy templates, but noramlly * there is no need to use it in CDK projects. */ condition?: RosCondition; /** * With the DeletionPolicy attribute you can preserve or (in some cases) backup a resource when its stack is deleted. * You specify a DeletionPolicy attribute for each resource that you want to control. If a resource has no DeletionPolicy * attribute, ROS deletes the resource by default. Note that this capability also applies to update operations * that lead to resources being removed. */ deletionPolicy?: RosDeletionPolicy; /** * Metadata associated with the ROS resource. This is not the same as the construct metadata which can be added * using construct.addMetadata(), but would not appear in the ROS template automatically. */ metadata?: { [key: string]: any }; description?: string; count?: number | IResolvable; } /** * Merges `source` into `target`, overriding any existing values. * `null`s will cause a value to be deleted. */ function deepMerge(target: any, ...sources: any[]) { for (const source of sources) { if (typeof source !== "object" || typeof target !== "object") { throw new Error( `Invalid usage. Both source (${JSON.stringify( source )}) and target (${JSON.stringify(target)}) must be objects` ); } for (const key of Object.keys(source)) { const value = source[key]; if (typeof value === "object" && value != null && !Array.isArray(value)) { // if the value at the target is not an object, override it with an // object so we can continue the recursion if (typeof target[key] !== "object") { target[key] = {}; } deepMerge(target[key], value); // if the result of the merge is an empty object, it's because the // eventual value we assigned is `undefined`, and there are no // sibling concrete values alongside, so we can delete this tree. const output = target[key]; if (typeof output === "object" && Object.keys(output).length === 0) { delete target[key]; } } else if (value === undefined) { delete target[key]; } else { target[key] = value; } } } return target; }