Skip to content
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

feat(hcl2cdk): support variable types and validations #2773

Merged
merged 5 commits into from
Apr 13, 2023
Merged
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
15 changes: 15 additions & 0 deletions go.work.sum
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,31 @@ github.com/aws/jsii-runtime-go v1.73.0 h1:4ncTOPq5SAHI0KgkP1ZdH49hRYVE3VCOZKqtil
github.com/aws/jsii-runtime-go v1.73.0/go.mod h1:Li7bq0fd6cg3hbRmYQf3l2VOF9dRwF9fLk3GgHOsHVQ=
github.com/aws/jsii-runtime-go v1.75.0 h1:NhpUfyiL7/wsRuUekFsz8FFBCYLfPD/l61kKg9kL/a4=
github.com/aws/jsii-runtime-go v1.75.0/go.mod h1:TKCyrtM0pygEPo4rDZzbMSDNCDNTSYSN6/mGyHI6O3I=
github.com/aws/jsii-runtime-go v1.79.0 h1:iYNJR1RCCba6N/vrzT9pNKC1gPDbLYGJV1YLn5pqTPA=
github.com/aws/jsii-runtime-go v1.79.0/go.mod h1:4IGCggNIyxe54k/INmsXrEjx9hUYQGw2W7a1I5x6l78=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
golang.org/x/tools v0.5.0/go.mod h1:N+Kgy78s5I24c24dU8OfWNEotWjutIs8SnJvn5IDq+k=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,7 @@ describe("expressions", () => {
constructs: new Set<string>(),
variables: {},
hasTokenBasedTypeCoercion: false,
nodeIds: [],
};
expect(
generate(
Expand Down
2 changes: 2 additions & 0 deletions packages/@cdktf/hcl2cdk/lib/__tests__/functions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -660,6 +660,7 @@ function terraformLiteralValueToTs(
providerGenerator: {},
providerSchema: {},
variables: {},
nodeIds: [],
},
literalExpression,
tfAst.meta.type,
Expand Down Expand Up @@ -728,6 +729,7 @@ function terraformFunctionCallToTs(
providerGenerator: {},
providerSchema: {},
variables: {},
nodeIds: [],
},
callExpression,
returnType,
Expand Down
64 changes: 64 additions & 0 deletions packages/@cdktf/hcl2cdk/lib/__tests__/generation.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/

import generate from "@babel/generator";
import { variableTypeToAst } from "../generation";

describe("variableTypeToAst", () => {
async function run(type: string) {
return generate(await variableTypeToAst(type)).code;
}

it("should convert a simple type", async () => {
expect(await run("${string}")).toMatchInlineSnapshot(
`"cdktf.VariableType.STRING"`
);
});

it("should convert a object type", async () => {
expect(
await run(
"${object({\n name = string\n address = string\n })}"
)
).toMatchInlineSnapshot(`
"cdktf.VariableType.object({
"address": cdktf.VariableType.STRING,
"name": cdktf.VariableType.STRING
})"
`);
});

it("should convert a list type", async () => {
expect(
await run(
"${list(object({\n internal = number\n external = number\n protocol = string\n }))}"
)
).toMatchInlineSnapshot(`
"cdktf.VariableType.list(cdktf.VariableType.object({
"external": cdktf.VariableType.NUMBER,
"internal": cdktf.VariableType.NUMBER,
"protocol": cdktf.VariableType.STRING
}))"
`);
});

it("should convert a set type", async () => {
expect(await run("${set(string)}")).toMatchInlineSnapshot(
`"cdktf.VariableType.set(cdktf.VariableType.STRING)"`
);
});

it("should convert a map type", async () => {
expect(await run("${map(string)}")).toMatchInlineSnapshot(
`"cdktf.VariableType.map(cdktf.VariableType.STRING)"`
);
});

it("should convert a tuple type", async () => {
expect(await run("${tuple(string, number, bool)}")).toMatchInlineSnapshot(
`"cdktf.VariableType.tuple(cdktf.VariableType.STRING, cdktf.VariableType.NUMBER, cdktf.VariableType.BOOL)"`
);
});
});
34 changes: 24 additions & 10 deletions packages/@cdktf/hcl2cdk/lib/expressions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -777,12 +777,9 @@ export function findExpressionType(
return "string";
}

export async function convertTerraformExpressionToTs(
input: string,
scope: ResourceScope,
targetType: () => AttributeType
): Promise<t.Expression> {
logger.debug(`convertTerraformExpressionToTs(${input})`);
export async function expressionAst(
input: string
): Promise<tex.ExpressionType> {
const sanitizedInput = wrapTerraformExpression(input);
const isWrapped = sanitizedInput.length !== input.length;
const ast = await getExpressionAst("main.tf", sanitizedInput);
Expand All @@ -791,12 +788,22 @@ export async function convertTerraformExpressionToTs(
throw new Error(`Unable to parse terraform expression: ${input}`);
}

let tsExpression;
if (isWrapped) {
tsExpression = convertTFExpressionAstToTs(ast.children[0], scope);
} else {
tsExpression = convertTFExpressionAstToTs(ast, scope);
return ast.children[0];
}
return ast;
}

export async function convertTerraformExpressionToTs(
input: string,
scope: ResourceScope,
targetType: () => AttributeType
): Promise<t.Expression> {
logger.debug(`convertTerraformExpressionToTs(${input})`);
const tsExpression = convertTFExpressionAstToTs(
await expressionAst(input),
scope
);

return coerceType(
scope,
Expand Down Expand Up @@ -1069,6 +1076,13 @@ export function constructAst(
return camelCase(sanitizeClassOrNamespaceName(resource));
}

if (type.startsWith("var.")) {
return t.memberExpression(
t.identifier("cdktf"),
t.identifier("TerraformVariable")
);
}

// resources or data sources
if (!type.includes("./") && type.includes(".")) {
const parts = type.split(".");
Expand Down
72 changes: 68 additions & 4 deletions packages/@cdktf/hcl2cdk/lib/generation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import {
constructAst,
isNestedDynamicBlock,
convertTerraformExpressionToTs,
expressionAst,
findUsedReferences,
} from "./expressions";
import {
TerraformModuleConstraint,
Expand All @@ -38,6 +40,7 @@ import {
getDesiredType,
} from "./terraformSchema";
import { Errors } from "@cdktf/commons";
import { TFExpressionSyntaxTree as tex } from "@cdktf/hcl2json";

function getReference(graph: DirectedGraph, id: string) {
logger.debug(`Finding reference for ${id}`);
Expand Down Expand Up @@ -70,6 +73,14 @@ export const valueToTs = async (
): Promise<t.Expression> => {
switch (typeof item) {
case "string":
if (
(await findUsedReferences(scope.nodeIds, item)).some((ref) =>
path.startsWith(ref.referencee.id)
)
) {
return t.stringLiteral(item);
}

return await convertTerraformExpressionToTs(`"${item}"`, scope, () =>
getDesiredType(scope, path)
);
Expand Down Expand Up @@ -189,7 +200,8 @@ export const valueToTs = async (
!path.includes("lifecycle") &&
(key === "for_each" ||
!typeMetadata ||
isMapAttribute(attributeType));
isMapAttribute(attributeType)) &&
!(path.startsWith("var.") && path.includes("validation"));

return t.objectProperty(
t.stringLiteral(
Expand Down Expand Up @@ -657,14 +669,66 @@ export async function output(
);
}

export async function variableTypeToAst(type: string): Promise<t.Expression> {
function parsedTypeToAst(type: tex.ExpressionType): t.Expression {
if (tex.isScopeTraversalExpression(type)) {
switch (type.meta.value) {
case "string":
return t.identifier("cdktf.VariableType.STRING");
case "number":
return t.identifier("cdktf.VariableType.NUMBER");
case "bool":
return t.identifier("cdktf.VariableType.BOOL");
case "any":
default:
return t.identifier("cdktf.VariableType.ANY");
}
}

if (tex.isFunctionCallExpression(type)) {
switch (type.meta.name) {
case "list":
case "set":
case "map":
case "tuple":
case "object":
return t.callExpression(
t.identifier(`cdktf.VariableType.${type.meta.name}`),
type.children.map((child) => parsedTypeToAst(child))
);
}
}

if (tex.isObjectExpression(type)) {
return t.objectExpression(
Object.entries(type.meta.items).map(([key, value]) =>
t.objectProperty(
t.stringLiteral(key),
// This does not deal with complex types nested within objects
// If such a type is found it will result in an Any type
// e.g. { foo: list(string) } will result in { foo: any }
parsedTypeToAst({
type: "scopeTraversal",
meta: { value },
} as any)
)
)
);
}

return t.identifier("cdktf.VariableType.ANY");
}

return parsedTypeToAst(await expressionAst(type));
}

export async function variable(
scope: ProgramScope,
key: string,
id: string,
item: Variable,
graph: DirectedGraph
) {
// We don't handle type information right now
const [{ type, ...props }] = item;

if (!getReference(graph, id)) {
Expand All @@ -673,9 +737,9 @@ export async function variable(

return asExpression(
scope,
"cdktf.TerraformVariable",
id,
key,
props,
{ ...props, type: type ? await variableTypeToAst(type) : undefined },
false,
false,
getReference(graph, id)
Expand Down
20 changes: 18 additions & 2 deletions packages/@cdktf/hcl2cdk/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,6 @@ export async function convertToTypescript(
logger.debug("Converting to typescript");
const plan = await getParsedHcl(hcl);

logger.debug(`Parsed HCL: ${JSON.stringify(plan, null, 2)}`);

// Each key in the scope needs to be unique, therefore we save them in a set
// Each variable needs to be unique as well, we save them in a record so we can identify if two variables are the same
const scope: ProgramScope = {
Expand All @@ -105,6 +103,7 @@ export async function convertToTypescript(
constructs: new Set<string>(),
variables: {},
hasTokenBasedTypeCoercion: false,
nodeIds: [],
};

const graph = new DirectedGraph<{
Expand Down Expand Up @@ -145,6 +144,7 @@ export async function convertToTypescript(

// Finding references becomes easier of the to be referenced ids are already known
const nodeIds = Object.keys(nodeMap);
scope.nodeIds = nodeIds;
async function addEdges(id: string, value: TerraformResourceBlock) {
(await findUsedReferences(nodeIds, value)).forEach((ref) => {
if (
Expand All @@ -160,6 +160,12 @@ export async function convertToTypescript(
);
}

// The graph should have no self-references
if (id === ref.referencee.id) {
logger.debug(`Skipping self-reference for ${id}`);
return;
}

logger.debug(`Adding edge from ${ref.referencee.id} to ${id}`);
graph.addDirectedEdge(ref.referencee.id, id, { ref });
}
Expand Down Expand Up @@ -259,6 +265,16 @@ export async function convertToTypescript(
);
} while (nodesToVisit.length > 0 && nodesVisitedThisIteration != 0);

if (nodesToVisit.length > 0) {
throw new Error(
`There are ${
nodesToVisit.length
} terraform elements that could not be visited.
This is likely due to a cycle in the dependency graph.
These nodes are: ${nodesToVisit.join(", ")}`
);
}

logger.debug(
`${nodesToVisit.length} unvisited nodes: ${nodesToVisit.join(", ")}`
);
Expand Down
2 changes: 1 addition & 1 deletion packages/@cdktf/hcl2cdk/lib/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const variableConfig = tfObject({
description: z.string(),
sensitive: z.boolean(),
nullable: z.boolean().optional(),
validation: z.array(z.record(validationConfig)).optional(),
validation: z.array(validationConfig).optional(),
});
export type Variable = z.infer<typeof variableConfig>;

Expand Down
1 change: 1 addition & 0 deletions packages/@cdktf/hcl2cdk/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export type ProgramScope = {
>;
// Temporary flag to indicate if we need to import the cdktf library to access the token class
hasTokenBasedTypeCoercion: boolean;
nodeIds: string[]; // temporarily added until replaced
DanielMSchmidt marked this conversation as resolved.
Show resolved Hide resolved
};

export type ResourceScope = ProgramScope & {
Expand Down
Loading