-
Notifications
You must be signed in to change notification settings - Fork 455
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
cdktf: construct name should be unique per resource type [same as terraform] #3290
Comments
Hi @shinebayar-g 👋 Thank you for raising this. I agree from a Terraform point of view – unfortunately, we can't control this, as the underlying construct package (which is used by all CDKs), makes no distinction between types of constructs and requires a unique name within each scope. However, you can work around this by using new RandomProvider(this, "random");
new Password(this, "password", { length: 16 });
const pet = new Pet(this, "pet-password", {});
pet.overrideLogicalId("password"); produces: "resource": {
"random_password": {
"password": {
"//": {
"metadata": {
"path": "test-override/password",
"uniqueId": "password"
}
},
"length": 16
}
},
"random_pet": {
"password": {
"//": {
"metadata": {
"path": "test-override/pet-password",
"uniqueId": "password"
}
}
}
}
}, |
If you feel strongly about keeping the terraform naming conventions in place in your CDKTF application you can extend the existing resources: import { S3Bucket, S3BucketConfig } from "./.gen/providers/aws/s3-bucket"
class BetterS3Bucket extends S3Bucket {
constructor(scope: Construct, name: string, config: S3BucketConfig) {
super(scope, `${S3Bucket.tfResourceType}-${name}`, config);
this.overrideLogicalId(name)
}
} For this class the same constraints as in Terraform would apply. |
Thanks for the explanation and workarounds.
It looks like every resource is extended from |
Err... we do feel strongly about this. CDKTF will be a tough sell in any organisation unless it saves us time writing code.
I don't fully agree with the "we can't control this" part. Child classes of In terms of practical solutions for people who might have wasted time looking into a nice hack for this, I just gave up and used import { S3BucketServerSideEncryptionConfigurationA } from "@cdktf/provider-aws/lib/s3-bucket-server-side-encryption-configuration";
import { v4 as uuid } from "uuid";
new S3BucketServerSideEncryptionConfigurationA(this, uuid(), {
bucket: "my-bucket",
rule: [{ applyServerSideEncryptionByDefault: { sseAlgorithm: "aws:kms" } }],
}).overrideLogicalId(id); Code brevity is really appreciated, especially for long resource names like |
The following patches seem to be working on my side (haven't done extensive testing):
diff --git a/lib/construct.js b/lib/construct.js
index 4bb61d6c0c27e4fcd5a3abcd4fbd762827804890..cbe28b328b94660a8c58be1cbc8a0d339d1dd2aa 100644
--- a/lib/construct.js
+++ b/lib/construct.js
@@ -20,7 +20,9 @@ class Node {
static of(construct) {
return construct.node;
}
- constructor(host, scope, id) {
+ // Store originalId to allow constructs with different types to have the same id
+ // See: https://github.com/hashicorp/terraform-cdk/issues/3290
+ constructor(host, scope, id, originalId) {
this.host = host;
this._locked = false; // if this is "true", addChild will fail
this._children = {};
@@ -30,6 +32,7 @@ class Node {
this._validations = new Array();
id = id ?? ''; // if undefined, convert to empty string
this.id = sanitizeId(id);
+ this.originalId = originalId !== undefined ? sanitizeId(originalId) : undefined;
this.scope = scope;
if (scope && !this.id) {
throw new Error('Only root constructs may have an empty ID');
@@ -422,8 +425,8 @@ class Construct {
* the ID includes a path separator (`/`), then it will be replaced by double
* dash `--`.
*/
- constructor(scope, id) {
- this.node = new Node(this, scope, id);
+ constructor(scope, id, originalId) {
+ this.node = new Node(this, scope, id, originalId);
// implement IDependable privately
dependency_1.Dependable.implement(this, {
dependencyRoots: [this],
diff --git a/lib/terraform-element.js b/lib/terraform-element.js
index 7c142f61c501b2072959219daaafa3b744c7d3fe..bf61f1b5f24e17e66f90938b0216613244117962 100644
--- a/lib/terraform-element.js
+++ b/lib/terraform-element.js
@@ -12,10 +12,20 @@ const terraform_stack_1 = require("./terraform-stack");
const tfExpression_1 = require("./tfExpression");
const errors_1 = require("./errors");
const TERRAFORM_ELEMENT_SYMBOL = Symbol.for("cdktf/TerraformElement");
+
+function sanitize(prefix) {
+ return prefix.replaceAll(/[^a-z0-9-_]/ig, "_");
+}
+
// eslint-disable-next-line jsdoc/require-jsdoc
class TerraformElement extends constructs_1.Construct {
constructor(scope, id, elementType) {
- super(scope, id);
+ // Store originalId to allow constructs with different types to have the same id
+ // See: https://github.com/hashicorp/terraform-cdk/issues/3290
+ const originalId = id;
+ id = sanitize(`${elementType ?? new.target.name}_${id}`);
+
+ super(scope, id, originalId);
this.rawOverrides = {};
Object.defineProperty(this, TERRAFORM_ELEMENT_SYMBOL, { value: true });
this._elementType = elementType;
diff --git a/lib/terraform-stack.js b/lib/terraform-stack.js
index 4fa973fda6420cb134dc14650cd5e7cdc4bd4e7f..9eceee623995a5fd247a47b009adfe6e2958c49e 100644
--- a/lib/terraform-stack.js
+++ b/lib/terraform-stack.js
@@ -136,7 +136,9 @@ class TerraformStack extends constructs_1.Construct {
? tfElement.cdktfStack
: this;
const stackIndex = node.scopes.indexOf(stack);
- const components = node.scopes.slice(stackIndex + 1).map((c) => c.node.id);
+ // Store originalId to allow constructs with different types to have the same id
+ // See: https://github.com/hashicorp/terraform-cdk/issues/3290
+ const components = node.scopes.slice(stackIndex + 1).map((c) => c.node.originalId ?? c.node.id);
return components.length > 0 ? (0, unique_1.makeUniqueId)(components) : "";
}
allProviders() {
After applying those patches, internally class DevStack extends TerraformStack {
constructor(scope: Construct, id: string) {
super(scope, id);
new GoogleProvider(this, "dev", {
project: "foo",
});
// cdktf id = ComputeNetwork_main
// state id = google_compute_network.main
const vpc = new ComputeNetwork(this, "main", {
name: "main",
routingMode: "REGIONAL",
autoCreateSubnetworks: false,
});
// cdktf id = ComputeRoute_main
// state id = google_compute_route.main
new ComputeRoute(this, "main", {
name: "egress-internet",
description: "route through IGW to access internet",
destRange: "0.0.0.0/0",
network: vpc.id,
nextHopGateway: "default-internet-gateway",
});
}
} |
Description
Currently Construct naming experience is not consistent with Terraform. Consider the following example:
Above code will give an error:
Ideally resource names shouldn't include resource types. In Terraform, we just name it as
google_compute_network.main
andgoogle_compute_route.main
as described in https://developer.hashicorp.com/terraform/language/resources/syntax#resource-syntaxBecause of this we're forced to duplicate the resource type in the name to get around the error.
which ultimately leaks to the terraform resource names:
Resources declared inside
Construct
s even have random suffix attached to them which makes this constraint even more meaningless.References
https://developer.hashicorp.com/terraform/language/resources/syntax#resource-syntax
Help Wanted
Community Note
The text was updated successfully, but these errors were encountered: