@cdktn/hcl2cdk 0.24.0-pre.45 → 0.24.0-pre.47
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/iteration.ts
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// Copyright (c) HashiCorp, Inc
|
|
2
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
3
|
+
import { DirectedGraph } from "graphology";
|
|
4
|
+
import { providers as telemetryAllowedProviders } from "./telemetryAllowList.json";
|
|
5
|
+
import { ProgramScope } from "./types";
|
|
6
|
+
import { Import } from "./schema";
|
|
7
|
+
|
|
8
|
+
// locals, variables, and outputs are global key value maps
|
|
9
|
+
export function forEachGlobal<T, R>(
|
|
10
|
+
scope: ProgramScope,
|
|
11
|
+
prefix: string,
|
|
12
|
+
record: Record<string, T> | undefined,
|
|
13
|
+
iterator: (
|
|
14
|
+
scope: ProgramScope,
|
|
15
|
+
key: string,
|
|
16
|
+
id: string,
|
|
17
|
+
value: T,
|
|
18
|
+
graph: DirectedGraph,
|
|
19
|
+
) => Promise<R>,
|
|
20
|
+
): Record<string, { code: (graph: DirectedGraph) => Promise<R>; value: T }> {
|
|
21
|
+
return Object.entries(record || {}).reduce((carry, [key, item]) => {
|
|
22
|
+
const id = `${prefix}.${key}`;
|
|
23
|
+
return {
|
|
24
|
+
...carry,
|
|
25
|
+
[id]: {
|
|
26
|
+
code: async (graph: DirectedGraph) =>
|
|
27
|
+
await iterator(scope, key, id, item, graph),
|
|
28
|
+
value: item,
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}, {});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function forEachImport<R>(
|
|
35
|
+
scope: ProgramScope,
|
|
36
|
+
prefix: string,
|
|
37
|
+
record: Import[] | undefined,
|
|
38
|
+
iterator: (
|
|
39
|
+
scope: ProgramScope,
|
|
40
|
+
id: string,
|
|
41
|
+
value: Import,
|
|
42
|
+
graph: DirectedGraph,
|
|
43
|
+
) => Promise<R>,
|
|
44
|
+
): Record<
|
|
45
|
+
string,
|
|
46
|
+
{ code: (graph: DirectedGraph) => Promise<R>; value: Import }
|
|
47
|
+
> {
|
|
48
|
+
return (record || []).reduce((carry, item) => {
|
|
49
|
+
const target =
|
|
50
|
+
item.to.startsWith("${") && item.to.endsWith("}")
|
|
51
|
+
? item.to.substring(2, item.to.length - 1)
|
|
52
|
+
: item.to;
|
|
53
|
+
|
|
54
|
+
const id = `${prefix}.${target}`;
|
|
55
|
+
return {
|
|
56
|
+
...carry,
|
|
57
|
+
[id]: {
|
|
58
|
+
code: async (graph: DirectedGraph) =>
|
|
59
|
+
await iterator(scope, id, item, graph),
|
|
60
|
+
value: item,
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}, {});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function forEachProvider<T extends { alias?: string }, R>(
|
|
67
|
+
scope: ProgramScope,
|
|
68
|
+
record: Record<string, T[]> | undefined,
|
|
69
|
+
iterator: (
|
|
70
|
+
scope: ProgramScope,
|
|
71
|
+
key: string,
|
|
72
|
+
id: string,
|
|
73
|
+
value: T,
|
|
74
|
+
graph: DirectedGraph,
|
|
75
|
+
) => Promise<R>,
|
|
76
|
+
): Record<string, { code: (graph: DirectedGraph) => Promise<R>; value: T }> {
|
|
77
|
+
return Object.entries(record || {}).reduce((carry, [key, items]) => {
|
|
78
|
+
return {
|
|
79
|
+
...carry,
|
|
80
|
+
...items.reduce((innerCarry, item: T) => {
|
|
81
|
+
const id = item.alias ? `${key}.${item.alias}` : `${key}`;
|
|
82
|
+
return {
|
|
83
|
+
...innerCarry,
|
|
84
|
+
[id]: {
|
|
85
|
+
code: async (graph: DirectedGraph) =>
|
|
86
|
+
await iterator(scope, key, id, item, graph),
|
|
87
|
+
value: item,
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}, {}),
|
|
91
|
+
};
|
|
92
|
+
}, {});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// data and resource are namespaced key value maps
|
|
96
|
+
export function forEachNamespaced<T, R>(
|
|
97
|
+
scope: ProgramScope,
|
|
98
|
+
record: Record<string, Record<string, T>> | undefined,
|
|
99
|
+
iterator: (
|
|
100
|
+
scope: ProgramScope,
|
|
101
|
+
type: string,
|
|
102
|
+
key: string,
|
|
103
|
+
id: string,
|
|
104
|
+
value: T,
|
|
105
|
+
graph: DirectedGraph,
|
|
106
|
+
) => Promise<R>,
|
|
107
|
+
prefix?: string,
|
|
108
|
+
): Record<string, { code: (graph: DirectedGraph) => Promise<R>; value: T }> {
|
|
109
|
+
return Object.entries(record || {}).reduce(
|
|
110
|
+
(outerCarry, [type, items]) => ({
|
|
111
|
+
...outerCarry,
|
|
112
|
+
...Object.entries(items).reduce(
|
|
113
|
+
(innerCarry, [key, item]) => {
|
|
114
|
+
const prefixedType = prefix ? `${prefix}.${type}` : type;
|
|
115
|
+
const id = prefix ? `${prefix}.${type}.${key}` : `${type}.${key}`;
|
|
116
|
+
return {
|
|
117
|
+
...innerCarry,
|
|
118
|
+
[id]: {
|
|
119
|
+
code: async (graph: DirectedGraph) =>
|
|
120
|
+
await iterator(scope, prefixedType, key, id, item, graph),
|
|
121
|
+
value: item,
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
},
|
|
125
|
+
{} as Record<
|
|
126
|
+
string,
|
|
127
|
+
{ code: (graph: DirectedGraph) => Promise<R>; value: T }
|
|
128
|
+
>,
|
|
129
|
+
),
|
|
130
|
+
}),
|
|
131
|
+
{} as Record<
|
|
132
|
+
string,
|
|
133
|
+
{ code: (graph: DirectedGraph) => Promise<R>; value: T }
|
|
134
|
+
>,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function resourceStats(obj: Record<string, Record<string, unknown>>) {
|
|
139
|
+
return Object.entries(obj).reduce(
|
|
140
|
+
(carry, [key, value]) => {
|
|
141
|
+
const [provider, ...resourceParts] = key.split("_");
|
|
142
|
+
const shouldBeTracked = telemetryAllowedProviders.includes(provider);
|
|
143
|
+
const providerKey = shouldBeTracked ? provider : "other";
|
|
144
|
+
const resourceName = shouldBeTracked ? resourceParts.join("_") : "other";
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
...carry,
|
|
148
|
+
[providerKey]: {
|
|
149
|
+
...(carry[providerKey] || {}),
|
|
150
|
+
[resourceName]: Object.keys(value).length,
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
},
|
|
154
|
+
{} as Record<string, Record<string, number>>,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// Copyright (c) HashiCorp, Inc
|
|
2
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
3
|
+
|
|
4
|
+
export function replacePythonImports(code: string) {
|
|
5
|
+
return code
|
|
6
|
+
.split("\n")
|
|
7
|
+
.map((line) => {
|
|
8
|
+
// Replace from-import lines with lib
|
|
9
|
+
const fromImportLibRegex =
|
|
10
|
+
/from \.\.\.gen\.providers\.([^.]+)(?:\.lib)?\.(.*) import/;
|
|
11
|
+
if (fromImportLibRegex.test(line)) {
|
|
12
|
+
return line.replace(fromImportLibRegex, "from imports.$1.$2 import");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Replace import lines with lib
|
|
16
|
+
const importLibRegex =
|
|
17
|
+
/import \.\.\.gen\.providers\.([^.]+)(?:\.lib)?\.(.*) as (.*)/;
|
|
18
|
+
if (importLibRegex.test(line)) {
|
|
19
|
+
return line.replace(importLibRegex, "import imports.$1.$2 as $3");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Replace from-import lines
|
|
23
|
+
if (line.startsWith("from ...gen.providers.")) {
|
|
24
|
+
return line.replace("from ...gen.providers.", "from imports.");
|
|
25
|
+
}
|
|
26
|
+
// Replace import lines
|
|
27
|
+
if (line.startsWith("import ...gen.providers.")) {
|
|
28
|
+
return line.replace("import ...gen.providers.", "import imports.");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Replace modules
|
|
32
|
+
if (line.startsWith("import ...gen.modules.")) {
|
|
33
|
+
return line.replace("import ...gen.modules.", "import imports.");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return line;
|
|
37
|
+
})
|
|
38
|
+
.join("\n");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function replaceJavaImports(code: string) {
|
|
42
|
+
return code
|
|
43
|
+
.split("\n")
|
|
44
|
+
.map((line) => {
|
|
45
|
+
// Replace using lines with lib and precices import
|
|
46
|
+
const importWithLib =
|
|
47
|
+
/import gen\.providers\.([^.]+)(?:\.lib)?\.([^.]+)\.(.*);/;
|
|
48
|
+
const matchWithLib = line.match(importWithLib);
|
|
49
|
+
if (matchWithLib) {
|
|
50
|
+
const [, provider, resource, className] = matchWithLib;
|
|
51
|
+
return `import imports.${provider}.${resource}.${className};`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Replace using lines
|
|
55
|
+
const importWithoutLib = /import gen\.providers\.([^.]+)\.([^.]+)\.\*;/;
|
|
56
|
+
const matchWithoutLib = line.match(importWithoutLib);
|
|
57
|
+
if (matchWithoutLib) {
|
|
58
|
+
const [, provider, resource] = matchWithoutLib;
|
|
59
|
+
return `import imports.${provider}.${resource}.*;`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Replace using lines
|
|
63
|
+
const importModules = /import gen\.modules\.(.+)\.\*;/;
|
|
64
|
+
const importModulesMatch = line.match(importModules);
|
|
65
|
+
if (importModulesMatch) {
|
|
66
|
+
const [, module] = importModulesMatch;
|
|
67
|
+
return `import imports.${module}.*;`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return line;
|
|
71
|
+
})
|
|
72
|
+
.join("\n");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function replaceCsharpImports(code: string) {
|
|
76
|
+
return code
|
|
77
|
+
.split("\n")
|
|
78
|
+
.map((line) => {
|
|
79
|
+
// Replace using lines with lib
|
|
80
|
+
const fromImportLibRegex =
|
|
81
|
+
/using Gen\.Providers\.([^.]*)(?:\.Lib)?\.(.*);/;
|
|
82
|
+
const match = line.match(fromImportLibRegex);
|
|
83
|
+
if (match) {
|
|
84
|
+
const [, provider, resource] = match;
|
|
85
|
+
return `using ${lowercaseFirstChar(provider)}.${resource};`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Replace using lines
|
|
89
|
+
if (line.startsWith("using Gen.Providers.")) {
|
|
90
|
+
const importLine = line.replace("using Gen.Providers.", "");
|
|
91
|
+
|
|
92
|
+
return `using ${importLine
|
|
93
|
+
.substring(0, 1)
|
|
94
|
+
.toLocaleLowerCase()}${importLine.substring(1)}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (line.startsWith("using Gen.Modules.")) {
|
|
98
|
+
const importLine = line.replace("using Gen.Modules.", "");
|
|
99
|
+
|
|
100
|
+
return `using ${importLine}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return line;
|
|
104
|
+
})
|
|
105
|
+
.join("\n");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function replaceGoImports(code: string) {
|
|
109
|
+
return code
|
|
110
|
+
.split("\n")
|
|
111
|
+
.map((line) => {
|
|
112
|
+
// Replace using lines with lib
|
|
113
|
+
const fromImportLibRegex =
|
|
114
|
+
/import "github.com\/aws-samples\/dummy\/gen\/providers\/([^/]*)(?:\/lib)?\/(.*)"/;
|
|
115
|
+
const matchLib = line.match(fromImportLibRegex);
|
|
116
|
+
if (matchLib) {
|
|
117
|
+
const [, provider, resource] = matchLib;
|
|
118
|
+
return `import "cdk.tf/go/stack/generated/${provider}/${resource}"`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Replace using lines
|
|
122
|
+
const fromImportRegex =
|
|
123
|
+
/import "github.com\/aws-samples\/dummy\/gen\/providers\/([^/]+)\/(.*)"/;
|
|
124
|
+
const match = line.match(fromImportRegex);
|
|
125
|
+
if (match) {
|
|
126
|
+
const [, provider, resource] = match;
|
|
127
|
+
return `import "cdk.tf/go/stack/generated/${provider}/${resource}"`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const importModulesRegex =
|
|
131
|
+
/import (.*) "github.com\/aws-samples\/dummy\/gen\/modules\/(.*)"/;
|
|
132
|
+
const matchModules = line.match(importModulesRegex);
|
|
133
|
+
if (matchModules) {
|
|
134
|
+
const [, name, module] = matchModules;
|
|
135
|
+
return `import ${name} "cdk.tf/go/stack/generated/${module}"`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return line;
|
|
139
|
+
})
|
|
140
|
+
.join("\n");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function lowercaseFirstChar(str: string) {
|
|
144
|
+
return str.substring(0, 1).toLocaleLowerCase() + str.substring(1);
|
|
145
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) HashiCorp, Inc.
|
|
3
|
+
* SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { getTypeAtPath } from "./terraformSchema";
|
|
7
|
+
import { ResourceScope, TerraformResourceBlock } from "./types";
|
|
8
|
+
import * as t from "@babel/types";
|
|
9
|
+
import { camelCase } from "./utils";
|
|
10
|
+
|
|
11
|
+
function getConfigFieldName(
|
|
12
|
+
topLevelConfig: Record<string, unknown>,
|
|
13
|
+
name: string,
|
|
14
|
+
) {
|
|
15
|
+
const sanitizedName = camelCase(name);
|
|
16
|
+
return deduplicateName(Object.keys(topLevelConfig), sanitizedName);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function deduplicateName(existingNames: string[], name: string) {
|
|
20
|
+
let newName = name;
|
|
21
|
+
let i = 1;
|
|
22
|
+
while (existingNames.includes(newName)) {
|
|
23
|
+
newName = `${name}${i}`;
|
|
24
|
+
i++;
|
|
25
|
+
}
|
|
26
|
+
return newName;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function fillWithConfigAccessors(
|
|
30
|
+
scope: ResourceScope,
|
|
31
|
+
config: TerraformResourceBlock,
|
|
32
|
+
path: string,
|
|
33
|
+
): any {
|
|
34
|
+
if (Array.isArray(config)) {
|
|
35
|
+
return config.map((c) => fillWithConfigAccessors(scope, c, `${path}.[]`));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (typeof config === "object" && config !== null) {
|
|
39
|
+
const mutated = Object.entries(config).reduce(
|
|
40
|
+
(acc, [key, value]) => ({
|
|
41
|
+
...acc,
|
|
42
|
+
[key]: fillWithConfigAccessors(scope, value, `${path}.${key}`),
|
|
43
|
+
}),
|
|
44
|
+
{} as Record<string, TerraformResourceBlock>,
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
// Get type of this part of the config
|
|
48
|
+
const attributeType = getTypeAtPath(scope.providerSchema, path);
|
|
49
|
+
const requiredAttributes = getRequiredAttributes(attributeType);
|
|
50
|
+
|
|
51
|
+
// Add accessors for all required attributes that are missing
|
|
52
|
+
requiredAttributes.forEach((key) => {
|
|
53
|
+
const value = mutated[key];
|
|
54
|
+
const isNotDirectlyAccessible = value === undefined;
|
|
55
|
+
const isReplacedByAst =
|
|
56
|
+
(t.isNode(mutated) && t.isExpression(mutated)) ||
|
|
57
|
+
t.isExpression(value as any);
|
|
58
|
+
const isEmptyArray = Array.isArray(value) && value.length === 0;
|
|
59
|
+
|
|
60
|
+
// If this was already replaced by an AST node, we don't need to do anything
|
|
61
|
+
// We assume all fields are filled in by the AST
|
|
62
|
+
if (isReplacedByAst) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (isNotDirectlyAccessible || isEmptyArray) {
|
|
67
|
+
const fieldName = getConfigFieldName(scope.topLevelConfig, key);
|
|
68
|
+
mutated[key] = t.memberExpression(
|
|
69
|
+
t.identifier("config"),
|
|
70
|
+
t.identifier(fieldName),
|
|
71
|
+
);
|
|
72
|
+
scope.topLevelConfig[fieldName] = `${path}.${key}`;
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
return mutated;
|
|
77
|
+
} else {
|
|
78
|
+
return config;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
type Key = string;
|
|
83
|
+
export function getRequiredAttributes(
|
|
84
|
+
attributeType: ReturnType<typeof getTypeAtPath>,
|
|
85
|
+
): Key[] {
|
|
86
|
+
if (!attributeType) {
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
if (
|
|
90
|
+
typeof attributeType !== "object" ||
|
|
91
|
+
Array.isArray(attributeType) ||
|
|
92
|
+
attributeType === null ||
|
|
93
|
+
attributeType === undefined ||
|
|
94
|
+
!("block" in attributeType)
|
|
95
|
+
) {
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const requiredAttributes = Object.entries(
|
|
100
|
+
attributeType.block.attributes || {},
|
|
101
|
+
).reduce(
|
|
102
|
+
(acc, [key, value]) => (value.required ? [...acc, key] : acc),
|
|
103
|
+
[] as string[],
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
// Logic taken from (and should be shared with) provider generator resource parser: attributeForBlockType
|
|
107
|
+
const requiredBlockTypes = Object.entries(
|
|
108
|
+
attributeType.block.block_types || {},
|
|
109
|
+
).reduce((acc, [key, value]) => {
|
|
110
|
+
if (
|
|
111
|
+
value.nesting_mode === "single" &&
|
|
112
|
+
!Object.values(value.block.attributes || {}).some((x) => !x.required)
|
|
113
|
+
) {
|
|
114
|
+
return [...acc, key];
|
|
115
|
+
}
|
|
116
|
+
if (value.nesting_mode === "map") {
|
|
117
|
+
return acc;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (
|
|
121
|
+
(value.nesting_mode === "list" || value.nesting_mode === "set") &&
|
|
122
|
+
value.min_items === undefined
|
|
123
|
+
? false
|
|
124
|
+
: (value as any).min_items > 0
|
|
125
|
+
) {
|
|
126
|
+
return [...acc, key];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return acc;
|
|
130
|
+
}, [] as string[]);
|
|
131
|
+
return [...requiredAttributes, ...requiredBlockTypes].sort();
|
|
132
|
+
}
|
package/src/provider.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Copyright (c) HashiCorp, Inc
|
|
2
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
3
|
+
import * as z from "zod";
|
|
4
|
+
import { ProviderSchema, BlockType, Attribute } from "@cdktn/commons";
|
|
5
|
+
import { schema } from "./schema";
|
|
6
|
+
|
|
7
|
+
export { BlockType, Attribute };
|
|
8
|
+
|
|
9
|
+
export function getFullProviderName(
|
|
10
|
+
schema: ProviderSchema,
|
|
11
|
+
providerName: string,
|
|
12
|
+
) {
|
|
13
|
+
return Object.keys(schema.provider_schemas || {}).find((name) =>
|
|
14
|
+
name.endsWith(providerName),
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type Plan = z.infer<typeof schema>;
|
|
19
|
+
export function getProviderRequirements(plan: Plan) {
|
|
20
|
+
// In Terraform one can implicitly define the provider by using resources of that type
|
|
21
|
+
const explicitProviders = Object.keys(plan.provider || {});
|
|
22
|
+
const implicitProviders = Object.keys({ ...plan.resource, ...plan.data })
|
|
23
|
+
.filter((type) => type !== "terraform_remote_state")
|
|
24
|
+
.filter((type) => type !== "terraform_data")
|
|
25
|
+
.map((type) => type.split("_")[0]);
|
|
26
|
+
|
|
27
|
+
const providerRequirements = Array.from(
|
|
28
|
+
new Set([...explicitProviders, ...implicitProviders]),
|
|
29
|
+
).reduce(
|
|
30
|
+
(carry, req) => ({ ...carry, [req]: "*" }),
|
|
31
|
+
{} as Record<string, string>,
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
plan.terraform?.forEach(({ required_providers }) =>
|
|
35
|
+
(required_providers || []).forEach((providerBlock) =>
|
|
36
|
+
Object.entries(providerBlock).forEach(([key, value]) => {
|
|
37
|
+
let name, version;
|
|
38
|
+
if (typeof value === "string") {
|
|
39
|
+
name = key;
|
|
40
|
+
version = value;
|
|
41
|
+
} else {
|
|
42
|
+
name = value.source;
|
|
43
|
+
version = value.version;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!name) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
// implicitly only the last part of the path is used (e.g. docker for kreuzwerker/docker)
|
|
50
|
+
const parts = name.split("/");
|
|
51
|
+
if (parts.length > 1) {
|
|
52
|
+
delete providerRequirements[parts.pop() || ""];
|
|
53
|
+
}
|
|
54
|
+
providerRequirements[name] = version || "*";
|
|
55
|
+
}),
|
|
56
|
+
),
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
return providerRequirements;
|
|
60
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) HashiCorp, Inc.
|
|
3
|
+
* SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
getReferencesInExpression,
|
|
8
|
+
TFExpressionSyntaxTree as tex,
|
|
9
|
+
} from "@cdktn/hcl2json";
|
|
10
|
+
import { logger } from "./utils";
|
|
11
|
+
import { ProgramScope, Reference, TerraformResourceBlock } from "./types";
|
|
12
|
+
import { variableName } from "./variables";
|
|
13
|
+
|
|
14
|
+
export function referenceToVariableName(
|
|
15
|
+
scope: ProgramScope,
|
|
16
|
+
ref: Reference,
|
|
17
|
+
): string {
|
|
18
|
+
const parts = ref.referencee.id.split(".");
|
|
19
|
+
const resource = parts[0] === "data" ? `${parts[0]}.${parts[1]}` : parts[0];
|
|
20
|
+
const name = parts[0] === "data" ? parts[2] : parts[1];
|
|
21
|
+
return variableName(scope, resource, name);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function containsReference(expression: tex.ExpressionType) {
|
|
25
|
+
if (!tex.isScopeTraversalExpression(expression)) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const segments = expression.meta.traversal;
|
|
30
|
+
const rootSegment = segments[0].segment;
|
|
31
|
+
const fullAccessor = expression.meta.fullAccessor;
|
|
32
|
+
|
|
33
|
+
if (
|
|
34
|
+
rootSegment === "count" || // count variable
|
|
35
|
+
rootSegment === "each" || // each variable
|
|
36
|
+
// https://www.terraform.io/docs/language/expressions/references.html#filesystem-and-workspace-info
|
|
37
|
+
fullAccessor.startsWith("path.module") ||
|
|
38
|
+
fullAccessor.startsWith("path.root") ||
|
|
39
|
+
fullAccessor.startsWith("path.cwd") ||
|
|
40
|
+
fullAccessor.startsWith("terraform.workspace") ||
|
|
41
|
+
fullAccessor.startsWith("self.") // block local value
|
|
42
|
+
) {
|
|
43
|
+
logger.debug(`skipping ${fullAccessor}`);
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function extractReferencesFromExpression(
|
|
51
|
+
input: string,
|
|
52
|
+
nodeIds: readonly string[],
|
|
53
|
+
scopedIds: readonly string[] = [], // dynamics introduce new scoped variables that are not the globally accessible ids
|
|
54
|
+
): Promise<Reference[]> {
|
|
55
|
+
logger.debug(`extractReferencesFromExpression(${input})`);
|
|
56
|
+
const possibleVariableSpots = await getReferencesInExpression(
|
|
57
|
+
"main.tf",
|
|
58
|
+
input,
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
logger.debug(
|
|
62
|
+
`found possible variable spots: ${JSON.stringify(possibleVariableSpots)}`,
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
return possibleVariableSpots.reduce((carry, spot) => {
|
|
66
|
+
const { value, startPosition, endPosition } = spot;
|
|
67
|
+
// no reference
|
|
68
|
+
if (
|
|
69
|
+
!value.includes(".") || // just a literal
|
|
70
|
+
value.startsWith(".") || // dangling property access
|
|
71
|
+
value.endsWith("...") || // spread (likely in for loop)
|
|
72
|
+
value.startsWith("count.") || // count variable
|
|
73
|
+
value.startsWith("each.") || // each variable
|
|
74
|
+
// https://www.terraform.io/docs/language/expressions/references.html#filesystem-and-workspace-info
|
|
75
|
+
value.startsWith("path.module") ||
|
|
76
|
+
value.startsWith("path.root") ||
|
|
77
|
+
value.startsWith("path.cwd") ||
|
|
78
|
+
value.startsWith("terraform.workspace") ||
|
|
79
|
+
value.startsWith("self.") // block local value
|
|
80
|
+
) {
|
|
81
|
+
logger.debug(`skipping ${value}`);
|
|
82
|
+
return carry;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const referenceParts = value.split(".");
|
|
86
|
+
|
|
87
|
+
logger.debug(
|
|
88
|
+
`Searching for node id '${value}' in ${JSON.stringify(nodeIds)}`,
|
|
89
|
+
);
|
|
90
|
+
const corespondingNodeId = [...nodeIds, ...scopedIds].find((id) => {
|
|
91
|
+
const parts = id.split(".");
|
|
92
|
+
const matchesFirst = parts[0] === referenceParts[0];
|
|
93
|
+
const matchesFirstTwo =
|
|
94
|
+
matchesFirst &&
|
|
95
|
+
(parts[1] === referenceParts[1] || referenceParts.length === 1);
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
matchesFirstTwo &&
|
|
99
|
+
(parts[0] === "data" ? parts[2] === referenceParts[2] : true)
|
|
100
|
+
);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
if (!corespondingNodeId) {
|
|
104
|
+
// This is most likely a false positive, so we just ignore it
|
|
105
|
+
// We include the log below to help debugging
|
|
106
|
+
logger.error(
|
|
107
|
+
`Found a reference that is unknown: ${input} has reference "${value}". The id was not found in ${JSON.stringify(
|
|
108
|
+
nodeIds,
|
|
109
|
+
)} with temporary values ${JSON.stringify(scopedIds)}.`,
|
|
110
|
+
);
|
|
111
|
+
return carry;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (scopedIds.includes(corespondingNodeId)) {
|
|
115
|
+
logger.debug(`skipping '${value}' since it's a scoped variable`);
|
|
116
|
+
return carry;
|
|
117
|
+
}
|
|
118
|
+
logger.debug(`Found node id '${corespondingNodeId}'`);
|
|
119
|
+
|
|
120
|
+
const spotParts = value.split(".");
|
|
121
|
+
let isThereANumericAccessor = false;
|
|
122
|
+
const referenceSpotParts = spotParts.filter((part) => {
|
|
123
|
+
if (!Number.isNaN(parseInt(part, 10))) {
|
|
124
|
+
isThereANumericAccessor = true;
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return !isThereANumericAccessor;
|
|
129
|
+
});
|
|
130
|
+
const fullReference = isThereANumericAccessor
|
|
131
|
+
? referenceSpotParts.slice(0, 2).join(".")
|
|
132
|
+
: value;
|
|
133
|
+
|
|
134
|
+
const isVariable = value.startsWith("var.");
|
|
135
|
+
const useFqn =
|
|
136
|
+
// Can not use FQN on vars
|
|
137
|
+
!isVariable &&
|
|
138
|
+
// Can not use FQN on locals
|
|
139
|
+
!value.startsWith("local.") &&
|
|
140
|
+
// If the following character is
|
|
141
|
+
(input.substr(endPosition + 1, 1) === "*" || // a * (splat) we need to use the FQN
|
|
142
|
+
input.substr(endPosition, 1) === "[" || // a property access
|
|
143
|
+
isThereANumericAccessor); // a numeric access
|
|
144
|
+
|
|
145
|
+
const ref: Reference = {
|
|
146
|
+
start: startPosition,
|
|
147
|
+
end: endPosition,
|
|
148
|
+
referencee: {
|
|
149
|
+
id: corespondingNodeId,
|
|
150
|
+
full: fullReference,
|
|
151
|
+
},
|
|
152
|
+
useFqn,
|
|
153
|
+
isVariable,
|
|
154
|
+
};
|
|
155
|
+
logger.debug(`Found reference ${JSON.stringify(ref)}`);
|
|
156
|
+
return [...carry, ref];
|
|
157
|
+
}, [] as Reference[]);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export async function findUsedReferences(
|
|
161
|
+
nodeIds: string[],
|
|
162
|
+
item: TerraformResourceBlock,
|
|
163
|
+
): Promise<Reference[]> {
|
|
164
|
+
logger.debug(`findUsedReferences(${nodeIds}, ${item})`);
|
|
165
|
+
if (typeof item === "string") {
|
|
166
|
+
return await extractReferencesFromExpression(item, nodeIds, []);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (typeof item !== "object" || item === null || item === undefined) {
|
|
170
|
+
return [];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (Array.isArray(item)) {
|
|
174
|
+
return (
|
|
175
|
+
await Promise.all(item.map((i) => findUsedReferences(nodeIds, i)))
|
|
176
|
+
).flat();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (item && "dynamic" in item) {
|
|
180
|
+
const dyn = (item as any)["dynamic"];
|
|
181
|
+
const { for_each: _for_each, ...others } = dyn;
|
|
182
|
+
const dynamicRef = Object.keys(others)[0];
|
|
183
|
+
return await findUsedReferences([...nodeIds, dynamicRef], dyn);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return (
|
|
187
|
+
await Promise.all(
|
|
188
|
+
Object.values(item as Record<string, any>).map((i) =>
|
|
189
|
+
findUsedReferences(nodeIds, i),
|
|
190
|
+
),
|
|
191
|
+
)
|
|
192
|
+
).flat();
|
|
193
|
+
}
|