Skip to content

Commit

Permalink
feat(hcl2cdk): add dynamic blocks
Browse files Browse the repository at this point in the history
  • Loading branch information
DanielMSchmidt committed Jul 13, 2021
1 parent 56d65f8 commit ca82f9e
Show file tree
Hide file tree
Showing 4 changed files with 194 additions and 67 deletions.
24 changes: 16 additions & 8 deletions packages/@cdktf/hcl2cdk/lib/expressions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ const PROPERTY_ACCESS_REGEX = /\[.*\]/;

export function extractReferencesFromExpression(
input: string,
nodeIds: readonly string[]
nodeIds: readonly string[],
scopedIds: readonly string[] = [] // dynamics introduce new scoped variables that are not the globally accessible ids
): Reference[] {
const isDoubleParanthesis = input.startsWith("${{");
if (!input.startsWith("${")) {
Expand Down Expand Up @@ -79,10 +80,11 @@ export function extractReferencesFromExpression(

const referenceParts = spot.split(".");

const corespondingNodeId = nodeIds.find((id) => {
const corespondingNodeId = [...nodeIds, ...scopedIds].find((id) => {
const parts = id.split(".");
const matchesFirst = parts[0] === referenceParts[0];
const matchesFirstTwo =
parts[0] === referenceParts[0] && parts[1] === referenceParts[1];
matchesFirst && (parts[1] === referenceParts[1] || parts.length === 1);

return (
matchesFirstTwo &&
Expand All @@ -94,10 +96,14 @@ export function extractReferencesFromExpression(
throw new Error(
`Found a reference that is unknown: ${input} was not found in ${JSON.stringify(
nodeIds
)}`
)} with temporary values ${JSON.stringify(scopedIds)}`
);
}

if (scopedIds.includes(corespondingNodeId)) {
return carry;
}

const start = input.indexOf(spot);
const end = start + spot.length;

Expand Down Expand Up @@ -153,20 +159,22 @@ export function referenceToAst(ref: Reference) {

export function referencesToAst(
input: string,
refs: Reference[]
refs: Reference[],
scopedIds: readonly string[] = [] // dynamics introduce new scoped variables that are not the globally accessible ids
): t.Expression {
if (refs.length === 0) {
return t.stringLiteral(input);
}

const refAsts = refs
.sort((a, b) => a.start - b.start)
.filter((ref) => !scopedIds.includes(ref.referencee.id))
.map((ref) => ({ ref, ast: referenceToAst(ref) }));

if (
refs.length === 1 &&
refs[0].start === "${".length &&
refs[0].end === input.length - "}".length
refAsts.length === 1 &&
refAsts[0].ref.start === "${".length &&
refAsts[0].ref.end === input.length - "}".length
) {
return refAsts[0].ast;
}
Expand Down
165 changes: 130 additions & 35 deletions packages/@cdktf/hcl2cdk/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,35 +15,89 @@ import {
referenceToVariableName,
} from "./expressions";

const valueToTs = (item: any, nodeIds: readonly string[]): t.Expression => {
const valueToTs = (
item: any,
nodeIds: string[],
scopedIds: string[] = []
): t.Expression => {
switch (typeof item) {
case "string":
return referencesToAst(
item,
extractReferencesFromExpression(item, nodeIds)
extractReferencesFromExpression(item, nodeIds, scopedIds),
scopedIds
);
case "boolean":
return t.booleanLiteral(item);
case "number":
return t.numericLiteral(item);
case "object":
if (Array.isArray(item)) {
return t.arrayExpression(item.map((i) => valueToTs(i, nodeIds)));
return t.arrayExpression(
item.map((i) => valueToTs(i, nodeIds, scopedIds))
);
}

if (item["dynamic"]) {
const { for_each, ...others } = item["dynamic"];
const dynamicRef = Object.keys(others)[0];
return t.objectExpression([
t.objectProperty(t.identifier(dynamicRef), t.arrayExpression()),
]);
}

return t.objectExpression(
Object.entries(item)
.filter(([_key, value]) => value !== undefined)
.map(([key, value]) =>
t.objectProperty(
t.stringLiteral(camelCase(key)),
valueToTs(value, nodeIds)
t.stringLiteral(key !== "for_each" ? camelCase(key) : key),
valueToTs(value, nodeIds, scopedIds)
)
)
);
}
throw new Error("Unsupported type " + item);
};
type DynamicBlock = {
path: string;
for_each: any;
content: any;
scopedVar: string;
};
const extractDynamicBlocks = (config: any, path = ""): DynamicBlock[] => {
if (typeof config !== "object") {
return [];
}

if (Array.isArray(config)) {
return config.reduce(
(carry, item, index) => [
...carry,
...extractDynamicBlocks(item, `${path}.${index}`),
],
[]
);
}

if (config["dynamic"]) {
const scopedVar = Object.keys(config["dynamic"])[0];
const { for_each, content } = config["dynamic"][scopedVar][0];

return [
{
path: `${path}.${scopedVar}`,
for_each,
content,
scopedVar,
},
];
}

return Object.entries(config).reduce((carry, [key, value]) => {
return [...carry, ...extractDynamicBlocks(value as any, `${path}.${key}`)];
}, [] as DynamicBlock[]);
};

function findUsedReferences(
nodeIds: string[],
Expand All @@ -61,6 +115,15 @@ function findUsedReferences(
}

if (typeof item === "object") {
if (item && "dynamic" in item) {
const dyn = (item as any)["dynamic"];
const { for_each, ...others } = dyn;
const dynamicRef = Object.keys(others)[0];
return [
...references,
...findUsedReferences([...nodeIds, dynamicRef], dyn),
];
}
return [
...references,
...Object.values(item as Record<string, any>).reduce(
Expand All @@ -71,7 +134,7 @@ function findUsedReferences(
}

if (typeof item === "string") {
const extractedRefs = extractReferencesFromExpression(item, nodeIds);
const extractedRefs = extractReferencesFromExpression(item, nodeIds, []);
return [...references, ...extractedRefs];
}
return references;
Expand All @@ -81,7 +144,7 @@ function asExpression(
type: string,
name: string,
config: any,
nodeIds: readonly string[],
nodeIds: string[],
reference?: Reference
) {
const isNamespacedImport = type.includes(".");
Expand Down Expand Up @@ -209,13 +272,26 @@ function getReference(graph: DirectedGraph, id: string) {
}
}

function addOverrideExpression(
variable: string,
path: string,
value: t.Expression
) {
return t.expressionStatement(
t.callExpression(
t.memberExpression(t.identifier(variable), t.identifier("addOverride")),
[t.stringLiteral(path), value]
)
);
}

function resource(
type: string,
key: string,
id: string,
item: Resource,
graph: DirectedGraph
): t.Statement | t.Statement[] {
): t.Statement[] {
const [provider, ...name] = type.split("_");
const nodeIds = graph.nodes();
const resource = `${provider}.${name.join("_")}`;
Expand All @@ -228,36 +304,54 @@ function resource(
nodeIds,
getReference(graph, id)
);
const expressions = [expression];

if (for_each) {
const references = extractReferencesFromExpression(for_each, nodeIds);
const forEachOverrideExpression = t.expressionStatement(
t.callExpression(
t.memberExpression(
t.identifier(varibaleName(resource, id)),
t.identifier("addOverride")
),
[t.stringLiteral("for_each"), referencesToAst(for_each, references)]
const references = extractReferencesFromExpression(for_each, nodeIds, [
"each",
]);
expressions.push(
addOverrideExpression(
varibaleName(resource, id),
"for_each",
referencesToAst(for_each, references)
)
);
return [expression, forEachOverrideExpression];
}

if (count) {
const references = extractReferencesFromExpression(count, nodeIds);
const forEachOverrideExpression = t.expressionStatement(
t.callExpression(
t.memberExpression(
t.identifier(varibaleName(resource, id)),
t.identifier("addOverride")
),
[t.stringLiteral("count"), referencesToAst(count, references)]
const references = extractReferencesFromExpression(count, nodeIds, [
"count",
]);
expressions.push(
addOverrideExpression(
varibaleName(resource, id),
"count",
referencesToAst(count, references)
)
);
return [expression, forEachOverrideExpression];
}

return expression;
// Check for dynamic blocks
return [
...expressions,
...extractDynamicBlocks(config).map(
({ path, for_each, content, scopedVar }) => {
return addOverrideExpression(
varibaleName(resource, id),
path.substring(1), // The path starts with a dot that we don't want
valueToTs(
{
for_each,
content,
},
nodeIds,
[scopedVar]
)
) as any;
}
),
];
}

// locals, provider, variables, and outputs are global key value maps
Expand Down Expand Up @@ -330,25 +424,26 @@ export async function convertToTypescript(filename: string, hcl: string) {
);
const nodeIds = Object.keys(nodeMap);

// Add Edges
function addGlobalEdges(_key: string, id: string, value: unknown) {
function addEdges(id: string, value: unknown) {
findUsedReferences(nodeIds, value).forEach((ref) => {
if (!graph.hasDirectedEdge(ref.referencee.id, id)) {
if (
!graph.hasDirectedEdge(ref.referencee.id, id) &&
graph.hasNode(ref.referencee.id) // in case the referencee is a dynamic variable
) {
graph.addDirectedEdge(ref.referencee.id, id, { ref });
}
});
}
function addGlobalEdges(_key: string, id: string, value: unknown) {
addEdges(id, value);
}
function addNamespacedEdges(
_type: string,
_key: string,
id: string,
value: unknown
) {
findUsedReferences(nodeIds, value).forEach((ref) => {
if (!graph.hasDirectedEdge(ref.referencee.id, id)) {
graph.addDirectedEdge(ref.referencee.id, id, { ref });
}
});
addEdges(id, value);
}

Object.values({
Expand Down
24 changes: 22 additions & 2 deletions packages/@cdktf/hcl2cdk/test/__snapshots__/hcl2cdk.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -199,14 +199,34 @@ new aws.S3Bucket(this, \\"examplebucket\\", {
"
`;
exports[`convert dynamic blocks configuration 1`] = `
"const namespace = new TerraformVariable(this, \\"namespace\\", {});
const settings = new TerraformVariable(this, \\"settings\\", {});
new aws.ElasticBeanstalkEnvironment(this, \\"tfenvtest\\", {
setting: [],
});
awsElasticBeanstalkEnvironmentAwsElasticBeanstalkEnvironmentTfenvtest.addOverride(
\\"setting\\",
{
for_each: settings.fqn,
content: [
{
name: '\${setting.value[\\"name\\"]}',
namespace: namespace.fqn,
value: '\${setting.value[\\"value\\"]}',
},
],
}
);
"
`;
exports[`convert empty provider configuration 1`] = `
"import * as docker from \\"./.gen/docker\\";
new docker.DockerProvider(this, \\"docker\\", {});
"
`;
exports[`convert errors on dynamic blocks 1`] = `"Found a reference that is unknown: \${setting.value[\\"name\\"]} was not found in [\\"var.settings\\",\\"aws_elastic_beanstalk_environment.tfenvtest\\"]"`;
exports[`convert errors on provider alias 1`] = `"Unsupported Terraform feature found at \\"aws2\\": provider alias are not yet supported"`;
exports[`convert for each on list using splat configuration 1`] = `
Expand Down
Loading

0 comments on commit ca82f9e

Please sign in to comment.