Skip to content

feat(lib): add the remove function to TerraformResource #3849

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

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions packages/cdktf/lib/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,34 @@ export const modulesWithSameAlias = (alias: string) =>
Each provider must have a unique alias when passing multiple providers of the same type to modules.
`);

export const cannotRemoveMovedResource = (resourceId: string) =>
new Error(
`Cannot remove the resource "${resourceId}" because it has already been marked for a move operation.

A resource cannot be moved and removed at the same time. Please ensure the resource is not moved before attempting to remove it.`,
);

export const cannotMoveRemovedResource = (resourceId: string) =>
new Error(
`Cannot move the resource "${resourceId}" because it has already been marked for removal.

A resource cannot be moved and removed at the same time. Please ensure the resource is not removed before attempting to move it.`,
);

export const cannotRemoveImportedResource = (resourceId: string) =>
new Error(
`Cannot remove the resource "${resourceId}" because it has already been marked for import.

A resource cannot be imported and removed at the same time. Please ensure the resource is not imported before attempting to remove it.`,
);

export const cannotImportRemovedResource = (resourceId: string) =>
new Error(
`Cannot import the resource "${resourceId}" because it has already been marked for removal.

A resource cannot be imported and removed at the same time. Please ensure the resource is not removed before attempting to import it.`,
);

export const moveTargetAlreadySet = (
target: string,
friendlyUniqueId: string | undefined,
Expand Down
17 changes: 17 additions & 0 deletions packages/cdktf/lib/hcl/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,23 @@ ${renderAttributes(importBlock)}
return importBlocks.join("\n");
}

/**
*
*/
export function renderRemoved(removed: any) {
const removedBlocks = removed.map((removedBlock: any) => {
const { provisioner, ...otherAttrs } = removedBlock;
const hcl = [`removed {`];
const attrs = renderAttributes(otherAttrs);
if (attrs) hcl.push(attrs);
if (provisioner) hcl.push(renderProvisionerBlock(provisioner));
hcl.push("}");
return hcl.join("\n");
});

return removedBlocks.join("\n");
}

/**
*
*/
Expand Down
41 changes: 41 additions & 0 deletions packages/cdktf/lib/terraform-provisioner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,47 @@ export interface RemoteExecProvisioner {
readonly connection?: SSHProvisionerConnection | WinrmProvisionerConnection;
}

/**
* A removed block specific local-exec provisioner invokes a local executable after a resource is destroyed.
* This invokes a process on the machine running Terraform, not on the resource.
*
* See {@link https://developer.hashicorp.com/terraform/language/resources/provisioners/local-exec local-exec}
*/
export interface RemovedBlockLocalExecProvisioner {
readonly type: "local-exec";
/**
* This is the command to execute.
* It can be provided as a relative path to the current working directory or as an absolute path.
* It is evaluated in a shell, and can use environment variables or Terraform variables.
*/
readonly command: string;
/**
* If provided, specifies the working directory where command will be executed.
* It can be provided as a relative path to the current working directory or as an absolute path.
* The directory must exist.
*/
readonly workingDir?: string;
/**
* If provided, this is a list of interpreter arguments used to execute the command.
* The first argument is the interpreter itself.
* It can be provided as a relative path to the current working directory or as an absolute path
* The remaining arguments are appended prior to the command.
* This allows building command lines of the form "/bin/bash", "-c", "echo foo".
* If interpreter is unspecified, sensible defaults will be chosen based on the system OS.
*/
readonly interpreter?: string[];
/**
* A record of key value pairs representing the environment of the executed command.
* It inherits the current process environment.
*/
readonly environment?: Record<string, string>;
/**
* Specifies when Terraform will execute the command.
* For example, when = destroy specifies that the provisioner will run when the associated resource is destroyed
*/
readonly when: "destroy";
}

/**
* Expressions in connection blocks cannot refer to their parent resource by name.
* References create dependencies, and referring to a resource by name within its own block would create a dependency cycle.
Expand Down
107 changes: 107 additions & 0 deletions packages/cdktf/lib/terraform-resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { ITerraformIterator } from "./terraform-iterator";
import { Precondition, Postcondition } from "./terraform-conditions";
import { TerraformCount } from "./terraform-count";
import {
RemovedBlockLocalExecProvisioner,
SSHProvisionerConnection,
WinrmProvisionerConnection,
} from "./terraform-provisioner";
Expand All @@ -31,6 +32,10 @@ import {
import { ValidateTerraformVersion } from "./validations/validate-terraform-version";
import { TerraformStack } from "./terraform-stack";
import {
cannotImportRemovedResource,
cannotMoveRemovedResource,
cannotRemoveImportedResource,
cannotRemoveMovedResource,
movedToResourceOfDifferentType,
resourceGivenTwoMoveOperationsById,
resourceGivenTwoMoveOperationsByTarget,
Expand Down Expand Up @@ -128,6 +133,19 @@ export interface TerraformResourceImport {
readonly provider?: TerraformProvider;
}

export interface TerraformResourceRemoveLifecycle {
readonly destroy: boolean;
}

export type TerraformResourceRemoveProvisioner =
RemovedBlockLocalExecProvisioner;

export interface TerraformResourceRemove {
readonly from: string;
readonly lifecycle?: TerraformResourceRemoveLifecycle;
readonly provisioners?: Array<TerraformResourceRemoveProvisioner>;
}

// eslint-disable-next-line jsdoc/require-jsdoc
export class TerraformResource
extends TerraformElement
Expand All @@ -148,6 +166,7 @@ export class TerraformResource
FileProvisioner | LocalExecProvisioner | RemoteExecProvisioner
>;
private _imported?: TerraformResourceImport;
private _removed?: TerraformResourceRemove;
private _movedByTarget?: TerraformResourceMoveByTarget;
private _movedById?: TerraformResourceMoveById;
private _hasMoved = false;
Expand Down Expand Up @@ -271,6 +290,22 @@ export class TerraformResource
...this.constructNodeMetadata,
};

// If we are removing a resource imports and moved blocks are not supported
if (this._removed) {
const { provisioners, ...props } = this._removed;
return {
resource: undefined,
removed: [
{
...props,
provisioner: provisioners?.map(({ type, ...props }) => ({
[type]: keysToSnakeCase(props),
})),
},
],
};
}

const movedBlock = this._buildMovedBlock();
return {
resource: this._hasMoved
Expand Down Expand Up @@ -322,6 +357,24 @@ export class TerraformResource
...this.constructNodeMetadata,
};

// If we are removing a resource imports and moved blocks are not supported
if (this._removed) {
const { provisioners, ...props } = this._removed;
return {
resource: undefined,
removed: [
{
...props,
provisioner: provisioners?.map(({ type, ...props }) => ({
[type]: {
value: keysToSnakeCase(props),
},
})),
},
],
};
}

const movedBlock = this._buildMovedBlock();
return {
resource: this._hasMoved
Expand Down Expand Up @@ -393,6 +446,12 @@ export class TerraformResource
[this.terraformResourceType]: [this.friendlyUniqueId],
}
: undefined,
// TODO: its was unclear to me if how removed should interact with `toMetadata`
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking to get some clarification here how removed should work with toMetadata()

removed: this._removed
? {
[this.terraformResourceType]: [this.friendlyUniqueId],
}
: undefined,
};
}

Expand All @@ -406,6 +465,9 @@ export class TerraformResource
}

public importFrom(id: string, provider?: TerraformProvider) {
if (this._removed) {
throw cannotImportRemovedResource(this.node.id);
}
this._imported = { id, provider };
this.node.addValidation(
new ValidateTerraformVersion(
Expand All @@ -415,6 +477,42 @@ export class TerraformResource
);
}

/**
* Remove this resource, this will destroy the resource and place it within the removed block
* @param lifecycle The lifecycle block to be used for the removed resource
* @param provisioners Optional The provisioners to be used for the removed resource
*/
public remove(
lifecycle: TerraformResourceRemoveLifecycle,
provisioners?: Array<TerraformResourceRemoveProvisioner>,
) {
if (this._movedByTarget) {
throw cannotRemoveMovedResource(this.node.id);
}
if (this._imported) {
throw cannotRemoveImportedResource(this.node.id);
}
this.node.addValidation(
new ValidateTerraformVersion(
">=1.7",
`Removed blocks are only supported for Terraform >=1.7. Please upgrade your Terraform version.`,
),
);
if (provisioners) {
this.node.addValidation(
new ValidateTerraformVersion(
">=1.9",
`A Removed block provisioner is only supported for Terraform >=1.9. Please upgrade your Terraform version.`,
),
);
}
this._removed = {
from: `${this.terraformResourceType}.${this.friendlyUniqueId}`,
lifecycle,
provisioners,
};
}

private _getResourceTarget(moveTarget: string) {
return TerraformStack.of(this).moveTargets.getResourceByTarget(moveTarget);
}
Expand Down Expand Up @@ -472,6 +570,9 @@ export class TerraformResource
* @param index Optional The index corresponding to the key the resource is to appear in the foreach of a resource to move to
*/
public moveTo(moveTarget: string, index?: string | number) {
if (this._removed) {
throw cannotMoveRemovedResource(this.node.id);
}
if (this._movedByTarget) {
throw resourceGivenTwoMoveOperationsByTarget(
this.friendlyUniqueId,
Expand All @@ -496,6 +597,9 @@ export class TerraformResource
* @param id Full id of resource to move to, e.g. "aws_s3_bucket.example"
*/
public moveToId(id: string) {
if (this._removed) {
throw cannotMoveRemovedResource(this.node.id);
}
if (this._movedById) {
throw resourceGivenTwoMoveOperationsById(
this.node.id,
Expand All @@ -519,6 +623,9 @@ export class TerraformResource
* @param id Full id of resource being moved from, e.g. "aws_s3_bucket.example"
*/
public moveFromId(id: string) {
if (this._removed) {
throw cannotMoveRemovedResource(this.node.id);
}
if (this._movedById) {
throw resourceGivenTwoMoveOperationsById(
this.node.id,
Expand Down
5 changes: 5 additions & 0 deletions packages/cdktf/lib/terraform-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
renderVariable,
renderImport,
cleanForMetadata,
renderRemoved,
} from "./hcl/render";
import {
noStackForConstruct,
Expand Down Expand Up @@ -305,6 +306,10 @@ export class TerraformStack extends Construct {
res = [res, renderImport(frag.import)].join("\n\n");
}

if (frag.removed) {
res = [res, renderRemoved(frag.removed)].join("\n\n");
}

if (frag.locals) {
deepMerge(locals, frag);
}
Expand Down
Loading