@cdktn/hcl2cdk 0.24.0-pre.45 → 0.24.0-pre.48
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +355 -0
- package/README.md +1 -1
- package/build/__tests__/expressions.test.js +10 -19
- package/build/__tests__/functions.test.js +8 -18
- package/build/__tests__/testHelpers.js +3 -2
- package/build/coerceType.js +11 -21
- package/build/dynamic-blocks.js +3 -3
- package/build/expressions.js +13 -22
- package/build/function-bindings/functions.generated.js +2 -2
- package/build/generation.js +24 -34
- package/build/index.js +15 -25
- package/build/iteration.js +7 -6
- package/build/jsii-rosetta-workarounds.js +6 -5
- package/build/partialCode.js +11 -20
- package/build/provider.js +4 -3
- package/build/references.js +6 -5
- package/build/schema.js +8 -18
- package/build/terraformSchema.js +4 -4
- package/build/utils.js +3 -3
- package/build/variables.js +12 -21
- package/package.json +20 -17
- package/package.sh +1 -1
- package/src/__tests__/coerceType.test.ts +207 -0
- package/src/__tests__/expressionToTs.test.ts +1167 -0
- package/src/__tests__/expressions.test.ts +541 -0
- package/src/__tests__/findExpressionType.test.ts +112 -0
- package/src/__tests__/functions.test.ts +768 -0
- package/src/__tests__/generation.test.ts +72 -0
- package/src/__tests__/jsii-rosetta-workarounds.test.ts +145 -0
- package/src/__tests__/partialCode.test.ts +432 -0
- package/src/__tests__/terraformSchema.test.ts +107 -0
- package/src/__tests__/testHelpers.ts +11 -0
- package/src/coerceType.ts +261 -0
- package/src/dynamic-blocks.ts +61 -0
- package/src/expressions.ts +968 -0
- package/src/function-bindings/functions.generated.ts +1139 -0
- package/src/function-bindings/functions.ts +104 -0
- package/src/generation.ts +1189 -0
- package/src/index.ts +584 -0
- package/src/iteration.ts +156 -0
- package/src/jsii-rosetta-workarounds.ts +145 -0
- package/src/partialCode.ts +132 -0
- package/src/provider.ts +60 -0
- package/src/references.ts +193 -0
- package/src/schema.ts +74 -0
- package/src/terraformSchema.ts +182 -0
- package/src/types.ts +58 -0
- package/src/utils.ts +19 -0
- package/src/variables.ts +214 -0
- package/test/__snapshots__/backends.test.ts.snap +70 -0
- package/test/__snapshots__/externals.test.ts.snap +37 -0
- package/test/__snapshots__/granular-imports.test.ts.snap +180 -0
- package/test/__snapshots__/imports.test.ts.snap +159 -0
- package/test/__snapshots__/iteration.test.ts.snap +532 -0
- package/test/__snapshots__/jsiiLanguage.test.ts.snap +347 -0
- package/test/__snapshots__/locals.test.ts.snap +55 -0
- package/test/__snapshots__/modules.test.ts.snap +127 -0
- package/test/__snapshots__/outputs.test.ts.snap +77 -0
- package/test/__snapshots__/partialCode.test.ts.snap +120 -0
- package/test/__snapshots__/provider.test.ts.snap +128 -0
- package/test/__snapshots__/references.test.ts.snap +376 -0
- package/test/__snapshots__/resource-meta-properties.test.ts.snap +342 -0
- package/test/__snapshots__/resources.test.ts.snap +613 -0
- package/test/__snapshots__/tfExpressions.test.ts.snap +537 -0
- package/test/__snapshots__/typeCoercion.test.ts.snap +253 -0
- package/test/__snapshots__/variables.test.ts.snap +150 -0
- package/test/backends.test.ts +75 -0
- package/test/convertProject.test.ts +257 -0
- package/test/externals.test.ts +35 -0
- package/test/globalSetup.ts +224 -0
- package/test/globalTeardown.ts +11 -0
- package/test/granular-imports.test.ts +161 -0
- package/test/hcl2cdk.test.ts +88 -0
- package/test/helpers/convert.ts +543 -0
- package/test/helpers/tmp.ts +25 -0
- package/test/imports.test.ts +141 -0
- package/test/iteration.test.ts +342 -0
- package/test/jsiiLanguage.test.ts +73 -0
- package/test/locals.test.ts +47 -0
- package/test/modules.test.ts +143 -0
- package/test/outputs.test.ts +69 -0
- package/test/partialCode.test.ts +25 -0
- package/test/provider.test.ts +106 -0
- package/test/references.test.ts +287 -0
- package/test/resource-meta-properties.test.ts +288 -0
- package/test/resources.test.ts +551 -0
- package/test/tfExpressions.test.ts +300 -0
- package/test/typeCoercion.test.ts +154 -0
- package/test/variables.test.ts +96 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,584 @@
|
|
|
1
|
+
// Copyright (c) HashiCorp, Inc
|
|
2
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
3
|
+
import { parse } from "@cdktn/hcl2json";
|
|
4
|
+
import {
|
|
5
|
+
isRegistryModule,
|
|
6
|
+
TerraformProviderGenerator,
|
|
7
|
+
CodeMaker,
|
|
8
|
+
} from "@cdktn/provider-generator";
|
|
9
|
+
|
|
10
|
+
import * as t from "@babel/types";
|
|
11
|
+
import prettier from "prettier";
|
|
12
|
+
import * as path from "path";
|
|
13
|
+
import * as glob from "glob";
|
|
14
|
+
import * as fs from "fs";
|
|
15
|
+
import { DirectedGraph } from "graphology";
|
|
16
|
+
import * as rosetta from "jsii-rosetta";
|
|
17
|
+
import * as z from "zod";
|
|
18
|
+
|
|
19
|
+
import { schema } from "./schema";
|
|
20
|
+
import { findUsedReferences } from "./references";
|
|
21
|
+
import {
|
|
22
|
+
backendToExpression,
|
|
23
|
+
gen,
|
|
24
|
+
local,
|
|
25
|
+
moduleImports,
|
|
26
|
+
modules,
|
|
27
|
+
output,
|
|
28
|
+
provider,
|
|
29
|
+
resource,
|
|
30
|
+
variable,
|
|
31
|
+
wrapCodeInConstructor,
|
|
32
|
+
addImportForCodeContainer,
|
|
33
|
+
buildImports,
|
|
34
|
+
generateConfigType,
|
|
35
|
+
imports,
|
|
36
|
+
} from "./generation";
|
|
37
|
+
import { TerraformResourceBlock, ProgramScope } from "./types";
|
|
38
|
+
import {
|
|
39
|
+
forEachProvider,
|
|
40
|
+
forEachGlobal,
|
|
41
|
+
forEachNamespaced,
|
|
42
|
+
resourceStats,
|
|
43
|
+
} from "./iteration";
|
|
44
|
+
import { getProviderRequirements } from "./provider";
|
|
45
|
+
import { logger } from "./utils";
|
|
46
|
+
import { FQPN } from "@cdktn/provider-schema";
|
|
47
|
+
import { attributeNameToCdktfName } from "./generation";
|
|
48
|
+
import {
|
|
49
|
+
replaceCsharpImports,
|
|
50
|
+
replaceGoImports,
|
|
51
|
+
replaceJavaImports,
|
|
52
|
+
replacePythonImports,
|
|
53
|
+
} from "./jsii-rosetta-workarounds";
|
|
54
|
+
import { ProviderSchema } from "@cdktn/commons";
|
|
55
|
+
import { forEachImport } from "./iteration";
|
|
56
|
+
|
|
57
|
+
export const CODE_MARKER = "// define resources here";
|
|
58
|
+
|
|
59
|
+
export async function getParsedHcl(hcl: string) {
|
|
60
|
+
logger.debug(`Parsing HCL: ${hcl}`);
|
|
61
|
+
// Get the JSON representation of the HCL
|
|
62
|
+
let json: Record<string, unknown>;
|
|
63
|
+
try {
|
|
64
|
+
json = await parse("terraform.tf", hcl);
|
|
65
|
+
} catch (err) {
|
|
66
|
+
logger.error(`Failed to parse HCL: ${err}`);
|
|
67
|
+
throw new Error(
|
|
68
|
+
`Error: Could not parse HCL, this means either that the HCL passed is invalid or that you found a bug. If the HCL seems valid, please file a bug under https://github.com/open-constructs/cdk-terrain/issues/new?assignees=&labels=bug%2C+new%2C+feature%2Fconvert&template=bug-report.yml&title=`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Ensure the JSON representation matches the expected structure
|
|
73
|
+
let plan: z.infer<typeof schema>;
|
|
74
|
+
try {
|
|
75
|
+
plan = schema.parse(json);
|
|
76
|
+
} catch (err) {
|
|
77
|
+
throw new Error(`Error: HCL-JSON does not conform to schema. This is not expected, please file a bug under https://github.com/open-constructs/cdk-terrain/issues/new?assignees=&labels=bug%2C+new%2C+feature%2Fconvert&template=bug-report.yml&title=
|
|
78
|
+
Please include this information:
|
|
79
|
+
${JSON.stringify((err as z.ZodError).errors)}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return plan;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function parseProviderRequirements(hcl: string) {
|
|
86
|
+
logger.debug("Parsing provider requirements");
|
|
87
|
+
const plan = await getParsedHcl(hcl);
|
|
88
|
+
return getProviderRequirements(plan);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function convertToTypescript(
|
|
92
|
+
hcl: string,
|
|
93
|
+
providerSchema: ProviderSchema,
|
|
94
|
+
codeContainer: string,
|
|
95
|
+
) {
|
|
96
|
+
logger.debug("Converting to typescript");
|
|
97
|
+
const plan = await getParsedHcl(hcl);
|
|
98
|
+
|
|
99
|
+
// Each key in the scope needs to be unique, therefore we save them in a set
|
|
100
|
+
// Each variable needs to be unique as well, we save them in a record so we can identify if two variables are the same
|
|
101
|
+
const scope: ProgramScope = {
|
|
102
|
+
providerSchema,
|
|
103
|
+
providerGenerator: Object.keys(
|
|
104
|
+
providerSchema.provider_schemas || {},
|
|
105
|
+
).reduce((carry, fqpn) => {
|
|
106
|
+
const providerGenerator = new TerraformProviderGenerator(
|
|
107
|
+
new CodeMaker(),
|
|
108
|
+
providerSchema,
|
|
109
|
+
);
|
|
110
|
+
providerGenerator.buildResourceModels(fqpn as FQPN); // can't use that type on the keys yet, since we are not on TS >=4.4 yet :sadcat:
|
|
111
|
+
return { ...carry, [fqpn]: providerGenerator };
|
|
112
|
+
}, {}),
|
|
113
|
+
constructs: new Set<string>(),
|
|
114
|
+
variables: {},
|
|
115
|
+
hasTokenBasedTypeCoercion: false,
|
|
116
|
+
nodeIds: [],
|
|
117
|
+
importables: [],
|
|
118
|
+
topLevelConfig: {},
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const graph = new DirectedGraph<{
|
|
122
|
+
code: (
|
|
123
|
+
g: DirectedGraph<any>,
|
|
124
|
+
) => Promise<Array<t.Statement | t.VariableDeclaration>>;
|
|
125
|
+
}>();
|
|
126
|
+
|
|
127
|
+
// Get all items in the JSON as a map of id to function that generates the AST
|
|
128
|
+
// We will use this to construct the nodes for a dependency graph
|
|
129
|
+
// We need to use a function here because the same node has different representation based on if it's referenced by another one
|
|
130
|
+
const nodeMap: Record<
|
|
131
|
+
string,
|
|
132
|
+
{
|
|
133
|
+
code: (
|
|
134
|
+
g: typeof graph,
|
|
135
|
+
) => Promise<Array<t.Statement | t.VariableDeclaration>>;
|
|
136
|
+
value: unknown;
|
|
137
|
+
}
|
|
138
|
+
> = {
|
|
139
|
+
...forEachProvider(scope, plan.provider, provider),
|
|
140
|
+
...forEachGlobal(scope, "var", plan.variable, variable),
|
|
141
|
+
// locals are a special case
|
|
142
|
+
...forEachGlobal(
|
|
143
|
+
scope,
|
|
144
|
+
"local",
|
|
145
|
+
Array.isArray(plan.locals)
|
|
146
|
+
? plan.locals.reduce((carry, locals) => ({ ...carry, ...locals }), {})
|
|
147
|
+
: {},
|
|
148
|
+
local,
|
|
149
|
+
),
|
|
150
|
+
...forEachGlobal(scope, "out", plan.output, output),
|
|
151
|
+
...forEachGlobal(scope, "module", plan.module, modules),
|
|
152
|
+
...forEachImport(scope, "import", plan.import, imports),
|
|
153
|
+
...forEachNamespaced(scope, plan.resource, resource),
|
|
154
|
+
...forEachNamespaced(scope, plan.data, resource, "data"),
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// Add all nodes to the dependency graph so we can detect if an edge is added for an unknown link
|
|
158
|
+
Object.entries(nodeMap).forEach(([key, value]) => {
|
|
159
|
+
logger.debug(`Adding node '${key}' to graph`);
|
|
160
|
+
graph.addNode(key, value);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Finding references becomes easier of the to be referenced ids are already known
|
|
164
|
+
const nodeIds = Object.keys(nodeMap);
|
|
165
|
+
scope.nodeIds = nodeIds;
|
|
166
|
+
async function addEdges(id: string, value: TerraformResourceBlock) {
|
|
167
|
+
(await findUsedReferences(nodeIds, value)).forEach((ref) => {
|
|
168
|
+
if (
|
|
169
|
+
!graph.hasDirectedEdge(ref.referencee.id, id) &&
|
|
170
|
+
graph.hasNode(ref.referencee.id) // in case the referencee is a dynamic variable
|
|
171
|
+
) {
|
|
172
|
+
if (!graph.hasNode(id)) {
|
|
173
|
+
throw new Error(
|
|
174
|
+
`The dependency graph is expected to link from ${
|
|
175
|
+
ref.referencee.id
|
|
176
|
+
} to ${id} but ${id} does not exist.
|
|
177
|
+
These nodes exist: ${graph.nodes().join("\n")}`,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// The graph should have no self-references
|
|
182
|
+
if (id === ref.referencee.id) {
|
|
183
|
+
logger.debug(`Skipping self-reference for ${id}`);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
logger.debug(`Adding edge from ${ref.referencee.id} to ${id}`);
|
|
188
|
+
graph.addDirectedEdge(ref.referencee.id, id, { ref });
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// We recursively inspect each resource value to find references to other values
|
|
194
|
+
// We add these to a dependency graph so that the programming code has the right order
|
|
195
|
+
async function addGlobalEdges(
|
|
196
|
+
_scope: ProgramScope,
|
|
197
|
+
_key: string,
|
|
198
|
+
id: string,
|
|
199
|
+
value: TerraformResourceBlock,
|
|
200
|
+
) {
|
|
201
|
+
await addEdges(id, value);
|
|
202
|
+
}
|
|
203
|
+
async function addProviderEdges(
|
|
204
|
+
_scope: ProgramScope,
|
|
205
|
+
_key: string,
|
|
206
|
+
id: string,
|
|
207
|
+
value: TerraformResourceBlock,
|
|
208
|
+
) {
|
|
209
|
+
await addEdges(id, value);
|
|
210
|
+
}
|
|
211
|
+
async function addNamespacedEdges(
|
|
212
|
+
_scope: ProgramScope,
|
|
213
|
+
_type: string,
|
|
214
|
+
_key: string,
|
|
215
|
+
id: string,
|
|
216
|
+
value: TerraformResourceBlock,
|
|
217
|
+
) {
|
|
218
|
+
await addEdges(id, value);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
await Promise.all(
|
|
222
|
+
Object.values({
|
|
223
|
+
...forEachProvider(scope, plan.provider, addProviderEdges),
|
|
224
|
+
...forEachGlobal(scope, "var", plan.variable, addGlobalEdges),
|
|
225
|
+
// locals are a special case
|
|
226
|
+
...forEachGlobal(
|
|
227
|
+
scope,
|
|
228
|
+
"local",
|
|
229
|
+
Array.isArray(plan.locals)
|
|
230
|
+
? plan.locals.reduce((carry, locals) => ({ ...carry, ...locals }), {})
|
|
231
|
+
: {},
|
|
232
|
+
addGlobalEdges,
|
|
233
|
+
),
|
|
234
|
+
...forEachGlobal(scope, "out", plan.output, addGlobalEdges),
|
|
235
|
+
...forEachGlobal(scope, "module", plan.module, addGlobalEdges),
|
|
236
|
+
...forEachNamespaced(scope, plan.resource, addNamespacedEdges),
|
|
237
|
+
...forEachNamespaced(scope, plan.data, addNamespacedEdges, "data"),
|
|
238
|
+
}).map(({ code: addEdgesToGraph }) => addEdgesToGraph(graph)),
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
logger.debug(`Graph: ${JSON.stringify(graph, null, 2)}`);
|
|
242
|
+
logger.debug(`Starting to assemble the typescript code`);
|
|
243
|
+
// We traverse the dependency graph to get the unordered JSON nodes into an ordered array
|
|
244
|
+
// where no node is referenced before it's defined
|
|
245
|
+
// As we check that the nodes on both ends of an edge exist we can be sure
|
|
246
|
+
// that no infinite loop exists, there can be no stray dependency on a node
|
|
247
|
+
const expressions: t.Statement[] = [];
|
|
248
|
+
let nodesToVisit = [...nodeIds];
|
|
249
|
+
// This ensures we detect cycles and don't end up in an endless loop
|
|
250
|
+
let nodesVisitedThisIteration = 0;
|
|
251
|
+
do {
|
|
252
|
+
nodesVisitedThisIteration = 0;
|
|
253
|
+
|
|
254
|
+
// Find next nodes to visit
|
|
255
|
+
const nodeExpressionGenerators = graph.mapNodes((nodeId, { code }) => {
|
|
256
|
+
if (!nodesToVisit.includes(nodeId)) {
|
|
257
|
+
return undefined;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const unresolvedDependencies = graph
|
|
261
|
+
.inNeighbors(nodeId)
|
|
262
|
+
.filter((item) => nodesToVisit.includes(item));
|
|
263
|
+
|
|
264
|
+
if (unresolvedDependencies.length === 0) {
|
|
265
|
+
nodesToVisit = nodesToVisit.filter((id) => nodeId !== id);
|
|
266
|
+
nodesVisitedThisIteration = nodesVisitedThisIteration + 1;
|
|
267
|
+
|
|
268
|
+
logger.debug(`Visiting node ${nodeId}`);
|
|
269
|
+
return code;
|
|
270
|
+
}
|
|
271
|
+
return undefined;
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
// Generate the code for the nodes
|
|
275
|
+
for (const code of nodeExpressionGenerators) {
|
|
276
|
+
if (code) {
|
|
277
|
+
expressions.push(...(await code(graph)));
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
logger.debug(
|
|
282
|
+
`${nodesToVisit.length} unvisited nodes: ${nodesToVisit.join(", ")}`,
|
|
283
|
+
);
|
|
284
|
+
} while (nodesToVisit.length > 0 && nodesVisitedThisIteration != 0);
|
|
285
|
+
|
|
286
|
+
if (nodesToVisit.length > 0) {
|
|
287
|
+
throw new Error(
|
|
288
|
+
`There are ${
|
|
289
|
+
nodesToVisit.length
|
|
290
|
+
} terraform elements that could not be visited.
|
|
291
|
+
This is likely due to a cycle in the dependency graph.
|
|
292
|
+
These nodes are: ${nodesToVisit.join(", ")}`,
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
logger.debug(
|
|
297
|
+
`${nodesToVisit.length} unvisited nodes: ${nodesToVisit.join(", ")}`,
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
const backendExpressions = (
|
|
301
|
+
await Promise.all(
|
|
302
|
+
plan.terraform?.map((terraform) =>
|
|
303
|
+
backendToExpression(scope, terraform.backend),
|
|
304
|
+
) || [Promise.resolve([])],
|
|
305
|
+
)
|
|
306
|
+
).reduce((carry, item) => [...carry, ...item], []);
|
|
307
|
+
|
|
308
|
+
logger.debug(
|
|
309
|
+
`Using these backend expressions: ${JSON.stringify(
|
|
310
|
+
backendExpressions,
|
|
311
|
+
null,
|
|
312
|
+
2,
|
|
313
|
+
)}`,
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
// We collect all module sources
|
|
317
|
+
const moduleRequirements = [
|
|
318
|
+
...new Set(
|
|
319
|
+
Object.values(plan.module || {}).reduce(
|
|
320
|
+
(carry, moduleBlock) => [
|
|
321
|
+
...carry,
|
|
322
|
+
...moduleBlock.reduce(
|
|
323
|
+
(arr, { source, version }) => [
|
|
324
|
+
...arr,
|
|
325
|
+
version ? `${source}@${version}` : source,
|
|
326
|
+
],
|
|
327
|
+
[] as string[],
|
|
328
|
+
),
|
|
329
|
+
],
|
|
330
|
+
[] as string[],
|
|
331
|
+
) || [],
|
|
332
|
+
),
|
|
333
|
+
];
|
|
334
|
+
|
|
335
|
+
logger.debug(
|
|
336
|
+
`Found these modules: ${JSON.stringify(moduleRequirements, null, 2)}`,
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
if (Object.keys(plan.variable || {}).length > 0 && expressions.length > 0) {
|
|
340
|
+
expressions[0] = t.addComment(
|
|
341
|
+
expressions[0],
|
|
342
|
+
"leading",
|
|
343
|
+
`Terraform Variables are not always the best fit for getting inputs in the context of Terraform CDK.
|
|
344
|
+
You can read more about this at https://cdktn.io/docs/concepts/variables-and-outputs#input-variables`,
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const providerRequirements = getProviderRequirements(plan);
|
|
349
|
+
logger.debug(
|
|
350
|
+
`Found these provider requirements: ${JSON.stringify(
|
|
351
|
+
providerRequirements,
|
|
352
|
+
null,
|
|
353
|
+
2,
|
|
354
|
+
)}`,
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
// We add a comment if there are providers with missing schema information
|
|
358
|
+
const providersLackingSchema = Object.keys(providerRequirements).filter(
|
|
359
|
+
(providerName) =>
|
|
360
|
+
providerName !== "terraform" &&
|
|
361
|
+
!Object.keys(providerSchema.provider_schemas || {}).some((schemaName) =>
|
|
362
|
+
schemaName.endsWith(providerName),
|
|
363
|
+
),
|
|
364
|
+
);
|
|
365
|
+
logger.debug(
|
|
366
|
+
`${
|
|
367
|
+
providersLackingSchema.length
|
|
368
|
+
} providers lack schema information: ${providersLackingSchema.join(", ")}`,
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
if (providersLackingSchema.length > 0) {
|
|
372
|
+
expressions[0] = t.addComment(
|
|
373
|
+
expressions[0],
|
|
374
|
+
"leading",
|
|
375
|
+
`The following providers are missing schema information and might need manual adjustments to synthesize correctly: ${providersLackingSchema.join(
|
|
376
|
+
", ",
|
|
377
|
+
)}.
|
|
378
|
+
For a more precise conversion please use the --provider flag in convert.`,
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Always add constructs
|
|
383
|
+
scope.importables.push({
|
|
384
|
+
constructName: "Construct",
|
|
385
|
+
provider: "constructs",
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
if (scope.hasTokenBasedTypeCoercion) {
|
|
389
|
+
scope.importables.push({
|
|
390
|
+
constructName: "Token",
|
|
391
|
+
provider: "cdktn",
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Add specific import for codeContainer
|
|
396
|
+
addImportForCodeContainer(scope, codeContainer);
|
|
397
|
+
const constructImports = buildImports(scope.importables);
|
|
398
|
+
|
|
399
|
+
const code = [...(backendExpressions || []), ...expressions];
|
|
400
|
+
const configTypeName =
|
|
401
|
+
Object.keys(scope.topLevelConfig).length > 0 ? "MyConfig" : undefined;
|
|
402
|
+
|
|
403
|
+
const classConfig = configTypeName
|
|
404
|
+
? [generateConfigType(configTypeName, scope.topLevelConfig)]
|
|
405
|
+
: [];
|
|
406
|
+
|
|
407
|
+
// We split up the generated code so that users can have more control over what to insert where
|
|
408
|
+
return {
|
|
409
|
+
// TODO: Remove imports and code because rosetta won't be able to translate them
|
|
410
|
+
all: await gen([
|
|
411
|
+
...constructImports,
|
|
412
|
+
...moduleImports(plan.module),
|
|
413
|
+
...classConfig,
|
|
414
|
+
wrapCodeInConstructor(
|
|
415
|
+
codeContainer,
|
|
416
|
+
code,
|
|
417
|
+
"MyConvertedCode",
|
|
418
|
+
configTypeName,
|
|
419
|
+
),
|
|
420
|
+
]),
|
|
421
|
+
imports: await gen([...constructImports, ...moduleImports(plan.module)]),
|
|
422
|
+
code: await gen(code),
|
|
423
|
+
providers: Object.entries(providerRequirements).map(([source, version]) =>
|
|
424
|
+
version === "*" ? source : `${source}@${version}`,
|
|
425
|
+
),
|
|
426
|
+
modules: moduleRequirements,
|
|
427
|
+
// We track some usage data to make it easier to understand what is used
|
|
428
|
+
stats: {
|
|
429
|
+
numberOfModules: moduleRequirements.length,
|
|
430
|
+
numberOfProviders: Object.keys(providerRequirements).length,
|
|
431
|
+
resources: resourceStats(plan.resource || {}),
|
|
432
|
+
data: resourceStats(plan.data || {}),
|
|
433
|
+
convertedLines: hcl.split("\n").length,
|
|
434
|
+
},
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
type File = { contents: string; fileName: string };
|
|
439
|
+
const translators = {
|
|
440
|
+
python: {
|
|
441
|
+
visitor: () => new rosetta.PythonVisitor(),
|
|
442
|
+
postTranslationMutation: replacePythonImports,
|
|
443
|
+
},
|
|
444
|
+
java: {
|
|
445
|
+
visitor: () => new rosetta.JavaVisitor(),
|
|
446
|
+
postTranslationMutation: replaceJavaImports,
|
|
447
|
+
},
|
|
448
|
+
csharp: {
|
|
449
|
+
visitor: () => new rosetta.CSharpVisitor(),
|
|
450
|
+
postTranslationMutation: replaceCsharpImports,
|
|
451
|
+
},
|
|
452
|
+
go: {
|
|
453
|
+
visitor: () => new rosetta.GoVisitor(),
|
|
454
|
+
postTranslationMutation: replaceGoImports,
|
|
455
|
+
},
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
function translatorForLanguage(language: keyof typeof translators) {
|
|
459
|
+
return (file: File, throwOnTranslationError: boolean) => {
|
|
460
|
+
const { visitor, postTranslationMutation } = translators[language];
|
|
461
|
+
const { translation, diagnostics } = rosetta.translateTypeScript(
|
|
462
|
+
file,
|
|
463
|
+
visitor(),
|
|
464
|
+
throwOnTranslationError ? { includeCompilerDiagnostics: true } : {},
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
if (
|
|
468
|
+
throwOnTranslationError &&
|
|
469
|
+
diagnostics.filter((diag) => diag.isError).length > 0
|
|
470
|
+
) {
|
|
471
|
+
logger.debug(`Could not translate TS to ${language}:\n${file.contents}`);
|
|
472
|
+
throw new Error(
|
|
473
|
+
`Could not translate TS to ${language}: ${diagnostics
|
|
474
|
+
.map((diag) => diag.formattedMessage)
|
|
475
|
+
.join("\n")}`,
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return postTranslationMutation(translation);
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
type ConvertOptions = {
|
|
484
|
+
/**
|
|
485
|
+
* The language to convert to
|
|
486
|
+
*/
|
|
487
|
+
language: keyof typeof translators | "typescript";
|
|
488
|
+
/**
|
|
489
|
+
* The provider schema to use for conversion
|
|
490
|
+
*/
|
|
491
|
+
providerSchema: ProviderSchema;
|
|
492
|
+
/**
|
|
493
|
+
* The base class to extend from. Defaults to `constructs.Construct`
|
|
494
|
+
*/
|
|
495
|
+
codeContainer?: string;
|
|
496
|
+
/**
|
|
497
|
+
* Whether to throw an error if the translation fails
|
|
498
|
+
* Defaults to false
|
|
499
|
+
*/
|
|
500
|
+
throwOnTranslationError?: boolean;
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
export async function convert(
|
|
504
|
+
hcl: string,
|
|
505
|
+
{
|
|
506
|
+
language,
|
|
507
|
+
providerSchema,
|
|
508
|
+
throwOnTranslationError = false,
|
|
509
|
+
codeContainer = "cdktn.TerraformStack",
|
|
510
|
+
}: ConvertOptions,
|
|
511
|
+
) {
|
|
512
|
+
const fileName = "terraform.tf";
|
|
513
|
+
const translater =
|
|
514
|
+
language === "typescript"
|
|
515
|
+
? (file: File, _throwOnTranslationError: boolean) => file.contents
|
|
516
|
+
: translatorForLanguage(language);
|
|
517
|
+
|
|
518
|
+
if (!translater) {
|
|
519
|
+
throw new Error("Unsupported language used: " + language);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const tsCode = await convertToTypescript(hcl, providerSchema, codeContainer);
|
|
523
|
+
|
|
524
|
+
return {
|
|
525
|
+
...tsCode,
|
|
526
|
+
all: translater(
|
|
527
|
+
{ fileName, contents: tsCode.all },
|
|
528
|
+
throwOnTranslationError,
|
|
529
|
+
),
|
|
530
|
+
imports: translater({ fileName, contents: tsCode.imports }, false),
|
|
531
|
+
code: translater({ fileName, contents: tsCode.code }, false),
|
|
532
|
+
stats: { ...tsCode.stats, language },
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
export function getTerraformConfigFromDir(importPath: string) {
|
|
537
|
+
const absPath = path.resolve(importPath);
|
|
538
|
+
const fileContents = glob
|
|
539
|
+
.sync("./*.tf", { cwd: absPath })
|
|
540
|
+
.map((p) => fs.readFileSync(path.resolve(absPath, p), "utf8"));
|
|
541
|
+
|
|
542
|
+
return fileContents.join("\n");
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
type CdktfJson = Record<string, unknown> & {
|
|
546
|
+
terraformProviders: any[];
|
|
547
|
+
terraformModules: any[];
|
|
548
|
+
};
|
|
549
|
+
export async function convertProject(
|
|
550
|
+
combinedHcl: string,
|
|
551
|
+
{ language, providerSchema }: ConvertOptions,
|
|
552
|
+
) {
|
|
553
|
+
if (language !== "typescript") {
|
|
554
|
+
throw new Error("Unsupported language used: " + language);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const {
|
|
558
|
+
imports,
|
|
559
|
+
code,
|
|
560
|
+
providers,
|
|
561
|
+
modules: tfModules,
|
|
562
|
+
stats,
|
|
563
|
+
} = await convert(combinedHcl, {
|
|
564
|
+
language,
|
|
565
|
+
providerSchema,
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
return {
|
|
569
|
+
code: (inputMainFile: string) => {
|
|
570
|
+
const importMainFile = [imports, inputMainFile].join("\n");
|
|
571
|
+
const outputMainFile = importMainFile.replace(CODE_MARKER, code);
|
|
572
|
+
return prettier.format(outputMainFile, { parser: "babel" });
|
|
573
|
+
},
|
|
574
|
+
cdktfJson: (inputCdktfJson: CdktfJson) => {
|
|
575
|
+
const cdktfJson = { ...inputCdktfJson };
|
|
576
|
+
cdktfJson.terraformProviders = providers;
|
|
577
|
+
cdktfJson.terraformModules = tfModules;
|
|
578
|
+
return cdktfJson;
|
|
579
|
+
},
|
|
580
|
+
stats,
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
export { isRegistryModule, attributeNameToCdktfName };
|