Skip to content

Commit bea7a71

Browse files
committed
feat(lib): add the remove function to TerraformResource
Adds the `remove` function to the `TerraformResource` that enables support for the underlying terraform `removed` block (context: https://developer.hashicorp.com/terraform/language/resources/syntax#removing-resources). Includes rendering support and unit tests.
1 parent 8ea05e2 commit bea7a71

File tree

6 files changed

+422
-0
lines changed

6 files changed

+422
-0
lines changed

packages/cdktf/lib/errors.ts

+28
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,34 @@ export const modulesWithSameAlias = (alias: string) =>
9090
Each provider must have a unique alias when passing multiple providers of the same type to modules.
9191
`);
9292

93+
export const cannotRemoveMovedResource = (resourceId: string) =>
94+
new Error(
95+
`Cannot remove the resource "${resourceId}" because it has already been marked for a move operation.
96+
97+
A resource cannot be moved and removed at the same time. Please ensure the resource is not moved before attempting to remove it.`,
98+
);
99+
100+
export const cannotMoveRemovedResource = (resourceId: string) =>
101+
new Error(
102+
`Cannot move the resource "${resourceId}" because it has already been marked for removal.
103+
104+
A resource cannot be moved and removed at the same time. Please ensure the resource is not removed before attempting to move it.`,
105+
);
106+
107+
export const cannotRemoveImportedResource = (resourceId: string) =>
108+
new Error(
109+
`Cannot remove the resource "${resourceId}" because it has already been marked for import.
110+
111+
A resource cannot be imported and removed at the same time. Please ensure the resource is not imported before attempting to remove it.`,
112+
);
113+
114+
export const cannotImportRemovedResource = (resourceId: string) =>
115+
new Error(
116+
`Cannot import the resource "${resourceId}" because it has already been marked for removal.
117+
118+
A resource cannot be imported and removed at the same time. Please ensure the resource is not removed before attempting to import it.`,
119+
);
120+
93121
export const moveTargetAlreadySet = (
94122
target: string,
95123
friendlyUniqueId: string | undefined,

packages/cdktf/lib/hcl/render.ts

+17
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,23 @@ ${renderAttributes(importBlock)}
460460
return importBlocks.join("\n");
461461
}
462462

463+
/**
464+
*
465+
*/
466+
export function renderRemoved(removed: any) {
467+
const removedBlocks = removed.map((removedBlock: any) => {
468+
const { provisioner, ...otherAttrs } = removedBlock;
469+
const hcl = [`removed {`];
470+
const attrs = renderAttributes(otherAttrs);
471+
if (attrs) hcl.push(attrs);
472+
if (provisioner) hcl.push(renderProvisionerBlock(provisioner));
473+
hcl.push("}");
474+
return hcl.join("\n");
475+
});
476+
477+
return removedBlocks.join("\n");
478+
}
479+
463480
/**
464481
*
465482
*/

packages/cdktf/lib/terraform-provisioner.ts

+41
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,47 @@ export interface RemoteExecProvisioner {
345345
readonly connection?: SSHProvisionerConnection | WinrmProvisionerConnection;
346346
}
347347

348+
/**
349+
* A removed block specific local-exec provisioner invokes a local executable after a resource is destroyed.
350+
* This invokes a process on the machine running Terraform, not on the resource.
351+
*
352+
* See {@link https://developer.hashicorp.com/terraform/language/resources/provisioners/local-exec local-exec}
353+
*/
354+
export interface RemovedBlockLocalExecProvisioner {
355+
readonly type: "local-exec";
356+
/**
357+
* This is the command to execute.
358+
* It can be provided as a relative path to the current working directory or as an absolute path.
359+
* It is evaluated in a shell, and can use environment variables or Terraform variables.
360+
*/
361+
readonly command: string;
362+
/**
363+
* If provided, specifies the working directory where command will be executed.
364+
* It can be provided as a relative path to the current working directory or as an absolute path.
365+
* The directory must exist.
366+
*/
367+
readonly workingDir?: string;
368+
/**
369+
* If provided, this is a list of interpreter arguments used to execute the command.
370+
* The first argument is the interpreter itself.
371+
* It can be provided as a relative path to the current working directory or as an absolute path
372+
* The remaining arguments are appended prior to the command.
373+
* This allows building command lines of the form "/bin/bash", "-c", "echo foo".
374+
* If interpreter is unspecified, sensible defaults will be chosen based on the system OS.
375+
*/
376+
readonly interpreter?: string[];
377+
/**
378+
* A record of key value pairs representing the environment of the executed command.
379+
* It inherits the current process environment.
380+
*/
381+
readonly environment?: Record<string, string>;
382+
/**
383+
* Specifies when Terraform will execute the command.
384+
* For example, when = destroy specifies that the provisioner will run when the associated resource is destroyed
385+
*/
386+
readonly when: "destroy";
387+
}
388+
348389
/**
349390
* Expressions in connection blocks cannot refer to their parent resource by name.
350391
* References create dependencies, and referring to a resource by name within its own block would create a dependency cycle.

packages/cdktf/lib/terraform-resource.ts

+106
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { ITerraformIterator } from "./terraform-iterator";
1818
import { Precondition, Postcondition } from "./terraform-conditions";
1919
import { TerraformCount } from "./terraform-count";
2020
import {
21+
RemovedBlockLocalExecProvisioner,
2122
SSHProvisionerConnection,
2223
WinrmProvisionerConnection,
2324
} from "./terraform-provisioner";
@@ -31,6 +32,10 @@ import {
3132
import { ValidateTerraformVersion } from "./validations/validate-terraform-version";
3233
import { TerraformStack } from "./terraform-stack";
3334
import {
35+
cannotImportRemovedResource,
36+
cannotMoveRemovedResource,
37+
cannotRemoveImportedResource,
38+
cannotRemoveMovedResource,
3439
movedToResourceOfDifferentType,
3540
resourceGivenTwoMoveOperationsById,
3641
resourceGivenTwoMoveOperationsByTarget,
@@ -128,6 +133,19 @@ export interface TerraformResourceImport {
128133
readonly provider?: TerraformProvider;
129134
}
130135

136+
export interface TerraformResourceRemoveLifecycle {
137+
readonly destroy: boolean;
138+
}
139+
140+
export type TerraformResourceRemoveProvisioner =
141+
RemovedBlockLocalExecProvisioner;
142+
143+
export interface TerraformResourceRemove {
144+
readonly from: string;
145+
readonly lifecycle?: TerraformResourceRemoveLifecycle;
146+
readonly provisioners?: Array<TerraformResourceRemoveProvisioner>;
147+
}
148+
131149
// eslint-disable-next-line jsdoc/require-jsdoc
132150
export class TerraformResource
133151
extends TerraformElement
@@ -148,6 +166,7 @@ export class TerraformResource
148166
FileProvisioner | LocalExecProvisioner | RemoteExecProvisioner
149167
>;
150168
private _imported?: TerraformResourceImport;
169+
private _removed?: TerraformResourceRemove;
151170
private _movedByTarget?: TerraformResourceMoveByTarget;
152171
private _movedById?: TerraformResourceMoveById;
153172
private _hasMoved = false;
@@ -271,6 +290,22 @@ export class TerraformResource
271290
...this.constructNodeMetadata,
272291
};
273292

293+
// If we are removing a resource imports and moved blocks are not supported
294+
if (this._removed) {
295+
const { provisioners, ...props } = this._removed;
296+
return {
297+
resource: undefined,
298+
removed: [
299+
{
300+
...props,
301+
provisioner: provisioners?.map(({ type, ...props }) => ({
302+
[type]: keysToSnakeCase(props),
303+
})),
304+
},
305+
],
306+
};
307+
}
308+
274309
const movedBlock = this._buildMovedBlock();
275310
return {
276311
resource: this._hasMoved
@@ -322,6 +357,24 @@ export class TerraformResource
322357
...this.constructNodeMetadata,
323358
};
324359

360+
// If we are removing a resource imports and moved blocks are not supported
361+
if (this._removed) {
362+
const { provisioners, ...props } = this._removed;
363+
return {
364+
resource: undefined,
365+
removed: [
366+
{
367+
...props,
368+
provisioner: provisioners?.map(({ type, ...props }) => ({
369+
[type]: {
370+
value: keysToSnakeCase(props),
371+
},
372+
})),
373+
},
374+
],
375+
};
376+
}
377+
325378
const movedBlock = this._buildMovedBlock();
326379
return {
327380
resource: this._hasMoved
@@ -393,6 +446,11 @@ export class TerraformResource
393446
[this.terraformResourceType]: [this.friendlyUniqueId],
394447
}
395448
: undefined,
449+
removed: this._removed
450+
? {
451+
[this.terraformResourceType]: [this.friendlyUniqueId],
452+
}
453+
: undefined,
396454
};
397455
}
398456

@@ -406,6 +464,9 @@ export class TerraformResource
406464
}
407465

408466
public importFrom(id: string, provider?: TerraformProvider) {
467+
if (this._removed) {
468+
throw cannotImportRemovedResource(this.node.id);
469+
}
409470
this._imported = { id, provider };
410471
this.node.addValidation(
411472
new ValidateTerraformVersion(
@@ -415,6 +476,42 @@ export class TerraformResource
415476
);
416477
}
417478

479+
/**
480+
* Remove this resource, this will destroy the resource and place it within the removed block
481+
* @param lifecycle The lifecycle block to be used for the removed resource
482+
* @param provisioners Optional The provisioners to be used for the removed resource
483+
*/
484+
public remove(
485+
lifecycle: TerraformResourceRemoveLifecycle,
486+
provisioners?: Array<TerraformResourceRemoveProvisioner>,
487+
) {
488+
if (this._movedByTarget) {
489+
throw cannotRemoveMovedResource(this.node.id);
490+
}
491+
if (this._imported) {
492+
throw cannotRemoveImportedResource(this.node.id);
493+
}
494+
this.node.addValidation(
495+
new ValidateTerraformVersion(
496+
">=1.7",
497+
`Removed blocks are only supported for Terraform >=1.7. Please upgrade your Terraform version.`,
498+
),
499+
);
500+
if (provisioners) {
501+
this.node.addValidation(
502+
new ValidateTerraformVersion(
503+
">=1.9",
504+
`A Removed block provisioner is only supported for Terraform >=1.9. Please upgrade your Terraform version.`,
505+
),
506+
);
507+
}
508+
this._removed = {
509+
from: `${this.terraformResourceType}.${this.friendlyUniqueId}`,
510+
lifecycle,
511+
provisioners,
512+
};
513+
}
514+
418515
private _getResourceTarget(moveTarget: string) {
419516
return TerraformStack.of(this).moveTargets.getResourceByTarget(moveTarget);
420517
}
@@ -472,6 +569,9 @@ export class TerraformResource
472569
* @param index Optional The index corresponding to the key the resource is to appear in the foreach of a resource to move to
473570
*/
474571
public moveTo(moveTarget: string, index?: string | number) {
572+
if (this._removed) {
573+
throw cannotMoveRemovedResource(this.node.id);
574+
}
475575
if (this._movedByTarget) {
476576
throw resourceGivenTwoMoveOperationsByTarget(
477577
this.friendlyUniqueId,
@@ -496,6 +596,9 @@ export class TerraformResource
496596
* @param id Full id of resource to move to, e.g. "aws_s3_bucket.example"
497597
*/
498598
public moveToId(id: string) {
599+
if (this._removed) {
600+
throw cannotMoveRemovedResource(this.node.id);
601+
}
499602
if (this._movedById) {
500603
throw resourceGivenTwoMoveOperationsById(
501604
this.node.id,
@@ -519,6 +622,9 @@ export class TerraformResource
519622
* @param id Full id of resource being moved from, e.g. "aws_s3_bucket.example"
520623
*/
521624
public moveFromId(id: string) {
625+
if (this._removed) {
626+
throw cannotMoveRemovedResource(this.node.id);
627+
}
522628
if (this._movedById) {
523629
throw resourceGivenTwoMoveOperationsById(
524630
this.node.id,

packages/cdktf/lib/terraform-stack.ts

+5
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
renderVariable,
3333
renderImport,
3434
cleanForMetadata,
35+
renderRemoved,
3536
} from "./hcl/render";
3637
import {
3738
noStackForConstruct,
@@ -305,6 +306,10 @@ export class TerraformStack extends Construct {
305306
res = [res, renderImport(frag.import)].join("\n\n");
306307
}
307308

309+
if (frag.removed) {
310+
res = [res, renderRemoved(frag.removed)].join("\n\n");
311+
}
312+
308313
if (frag.locals) {
309314
deepMerge(locals, frag);
310315
}

0 commit comments

Comments
 (0)