@cdktn/provider-generator 0.25.0-pre.6 → 0.25.0-pre.60
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/build/__tests__/edge-provider-schema/builder.js +3 -3
- package/build/__tests__/edge-provider-schema/cli.js +18 -8
- package/build/__tests__/edge-provider-schema.test.js +18 -8
- package/build/__tests__/provider.test.js +44 -16
- package/build/get/__tests__/constructs-maker.test.js +91 -8
- package/build/get/__tests__/generator/complex-computed-types.test.js +18 -8
- package/build/get/__tests__/generator/deep-nested-attributes.test.js +18 -8
- package/build/get/__tests__/generator/description-escaping.test.js +18 -8
- package/build/get/__tests__/generator/empty-provider-resources.test.js +18 -8
- package/build/get/__tests__/generator/ephemeral-resources.test.js +18 -8
- package/build/get/__tests__/generator/export-sharding.test.js +18 -8
- package/build/get/__tests__/generator/import-style.test.js +18 -8
- package/build/get/__tests__/generator/module-generator.test.js +18 -8
- package/build/get/__tests__/generator/nested-types.test.js +18 -8
- package/build/get/__tests__/generator/provider-functions-name-collision.test.d.ts +2 -0
- package/build/get/__tests__/generator/provider-functions-name-collision.test.js +89 -0
- package/build/get/__tests__/generator/provider-functions.test.js +18 -8
- package/build/get/__tests__/generator/provider.test.js +18 -8
- package/build/get/__tests__/generator/resource-types.test.js +18 -8
- package/build/get/__tests__/generator/skipped-attributes.test.js +18 -8
- package/build/get/__tests__/generator/types.test.js +18 -8
- package/build/get/__tests__/generator/versions-file.test.js +25 -10
- package/build/get/__tests__/generator/write-only.test.js +18 -8
- package/build/get/__tests__/target-versions.test.js +18 -8
- package/build/get/__tests__/util.js +20 -11
- package/build/get/constructs-maker.d.ts +17 -0
- package/build/get/constructs-maker.js +68 -10
- package/build/get/generator/emitter/struct-emitter.js +20 -10
- package/build/get/generator/loop-detection.js +2 -3
- package/build/get/generator/models/attribute-model.js +3 -3
- package/build/get/generator/models/attribute-type-model.js +3 -3
- package/build/get/generator/models/provider-function-model.js +3 -4
- package/build/get/generator/models/resource-model.d.ts +5 -1
- package/build/get/generator/models/resource-model.js +20 -10
- package/build/get/generator/models/supported-stored-classes.js +2 -3
- package/build/get/generator/provider-generator.d.ts +15 -1
- package/build/get/generator/provider-generator.js +20 -5
- package/build/get/generator/resource-parser.d.ts +3 -1
- package/build/get/generator/resource-parser.js +16 -7
- package/build/get/generator/sanitized-comments.js +2 -3
- package/build/get/generator/skipped-attributes.js +2 -3
- package/build/index.js +3 -3
- package/build/util.js +3 -4
- package/package.json +36 -13
- package/src/__tests__/__snapshots__/provider.test.ts.snap +1309 -3206
- package/src/__tests__/provider.test.ts +31 -8
- package/src/get/__tests__/constructs-maker.test.ts +125 -1
- package/src/get/__tests__/generator/fixtures/provider-functions-name-collision.test.fixture.json +42 -0
- package/src/get/__tests__/generator/provider-functions-name-collision.test.ts +84 -0
- package/src/get/__tests__/generator/versions-file.test.ts +10 -5
- package/src/get/constructs-maker.ts +65 -0
- package/src/get/generator/emitter/struct-emitter.ts +2 -2
- package/src/get/generator/models/attribute-type-model.ts +1 -2
- package/src/get/generator/models/resource-model.ts +32 -9
- package/src/get/generator/provider-generator.ts +27 -4
- package/src/get/generator/resource-parser.ts +21 -3
- package/tsconfig.json +4 -0
|
@@ -70,6 +70,27 @@ function resourceTypesPresentInSnapshot(
|
|
|
70
70
|
return resources;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Rewrites the registry host in any fully-qualified-name key. The host records
|
|
75
|
+
* which CLI fetched the schema - terraform's or tofu's - rather than anything
|
|
76
|
+
* about the generated code, so one snapshot serves both.
|
|
77
|
+
*/
|
|
78
|
+
function stubRegistryHosts(value: any): any {
|
|
79
|
+
if (Array.isArray(value)) return value.map(stubRegistryHosts);
|
|
80
|
+
if (value && typeof value === "object") {
|
|
81
|
+
return Object.fromEntries(
|
|
82
|
+
Object.entries(value).map(([k, v]) => [
|
|
83
|
+
k.replace(
|
|
84
|
+
/^registry\.(?:terraform\.io|opentofu\.org)\//,
|
|
85
|
+
"STUBBED_REGISTRY/",
|
|
86
|
+
),
|
|
87
|
+
stubRegistryHosts(v),
|
|
88
|
+
]),
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
return value;
|
|
92
|
+
}
|
|
93
|
+
|
|
73
94
|
describe("Provider", () => {
|
|
74
95
|
it("generates a provider", async () => {
|
|
75
96
|
const constraint = new TerraformProviderConstraint(
|
|
@@ -86,7 +107,7 @@ describe("Provider", () => {
|
|
|
86
107
|
process.env.CDKTF_EXPERIMENTAL_PROVIDER_SCHEMA_CACHE_PATH,
|
|
87
108
|
);
|
|
88
109
|
await maker.generate([constraint]);
|
|
89
|
-
const snapshot = directorySnapshot(workdir);
|
|
110
|
+
const snapshot = stubRegistryHosts(directorySnapshot(workdir));
|
|
90
111
|
expect(snapshot).toMatchSnapshot();
|
|
91
112
|
});
|
|
92
113
|
}, 600_000);
|
|
@@ -103,7 +124,7 @@ describe("Provider", () => {
|
|
|
103
124
|
targetLanguage: Language.TYPESCRIPT,
|
|
104
125
|
});
|
|
105
126
|
await maker.generate([constraint]);
|
|
106
|
-
const snapshot = directorySnapshot(workdir);
|
|
127
|
+
const snapshot = stubRegistryHosts(directorySnapshot(workdir));
|
|
107
128
|
|
|
108
129
|
const terraformResourceTypesPresent: string[] =
|
|
109
130
|
resourceTypesPresentInSnapshot(snapshot, "datadog");
|
|
@@ -127,7 +148,7 @@ describe("Provider", () => {
|
|
|
127
148
|
const constraint = new TerraformProviderConstraint({
|
|
128
149
|
name: "dockerr",
|
|
129
150
|
source: "registry.terraform.io/kreuzwerker/docker",
|
|
130
|
-
version: "3.0
|
|
151
|
+
version: "3.9.0",
|
|
131
152
|
});
|
|
132
153
|
return await mkdtemp(async (workdir) => {
|
|
133
154
|
const jsiiPath = path.join(workdir, ".jsii");
|
|
@@ -140,7 +161,7 @@ describe("Provider", () => {
|
|
|
140
161
|
process.env.CDKTF_EXPERIMENTAL_PROVIDER_SCHEMA_CACHE_PATH,
|
|
141
162
|
);
|
|
142
163
|
await maker.generate([constraint]);
|
|
143
|
-
const snapshot = directorySnapshot(workdir);
|
|
164
|
+
const snapshot = stubRegistryHosts(directorySnapshot(workdir));
|
|
144
165
|
|
|
145
166
|
const terraformResourceTypesPresent: string[] =
|
|
146
167
|
resourceTypesPresentInSnapshot(snapshot, "dockerr");
|
|
@@ -160,11 +181,13 @@ describe("Provider", () => {
|
|
|
160
181
|
version: "2.51.0",
|
|
161
182
|
source: "DrFaust92/bitbucket",
|
|
162
183
|
});
|
|
184
|
+
// andsafe-AG/bitbucket is Terraform-registry only; zahiar/bitbucket is
|
|
185
|
+
// mirrored on both, so this runs under either CLI.
|
|
163
186
|
const constraint2 = new TerraformProviderConstraint({
|
|
164
187
|
name: "abitbucket",
|
|
165
|
-
namespace: "
|
|
166
|
-
version: "
|
|
167
|
-
source: "
|
|
188
|
+
namespace: "zahiar",
|
|
189
|
+
version: "0.3.0",
|
|
190
|
+
source: "zahiar/bitbucket",
|
|
168
191
|
});
|
|
169
192
|
return await mkdtemp(async (workdir) => {
|
|
170
193
|
const jsiiPath = path.join(workdir, ".jsii");
|
|
@@ -178,7 +201,7 @@ describe("Provider", () => {
|
|
|
178
201
|
);
|
|
179
202
|
await maker.generate([constraint, constraint2]);
|
|
180
203
|
console.log("workdir", workdir);
|
|
181
|
-
const snapshot = directorySnapshot(workdir);
|
|
204
|
+
const snapshot = stubRegistryHosts(directorySnapshot(workdir));
|
|
182
205
|
expect(snapshot).toMatchSnapshot();
|
|
183
206
|
});
|
|
184
207
|
}, 600_000);
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
ConstructsMakerModuleTarget,
|
|
9
9
|
ConstructsMakerProviderTarget,
|
|
10
10
|
} from "@cdktn/commons";
|
|
11
|
-
import { determineGoModuleName } from "../constructs-maker";
|
|
11
|
+
import { ConstructsMaker, determineGoModuleName } from "../constructs-maker";
|
|
12
12
|
|
|
13
13
|
describe("constructsMaker", () => {
|
|
14
14
|
describe("determineGoModuleName", () => {
|
|
@@ -115,4 +115,128 @@ describe("constructsMaker", () => {
|
|
|
115
115
|
expect(target.srcMakName).toEqual("security_group");
|
|
116
116
|
});
|
|
117
117
|
});
|
|
118
|
+
|
|
119
|
+
describe("removeOrphanedResourceFolders", () => {
|
|
120
|
+
let tmpDir: string;
|
|
121
|
+
|
|
122
|
+
const providerDir = (provider: string) =>
|
|
123
|
+
path.join(tmpDir, "providers", provider);
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Lay out a provider folder containing the given resource directories plus
|
|
127
|
+
* the provider-level index files a real generation run writes.
|
|
128
|
+
*/
|
|
129
|
+
const seedProvider = async (provider: string, resources: string[]) => {
|
|
130
|
+
for (const resource of resources) {
|
|
131
|
+
await fs.mkdirs(path.join(providerDir(provider), resource));
|
|
132
|
+
await fs.writeFile(
|
|
133
|
+
path.join(providerDir(provider), resource, "index.ts"),
|
|
134
|
+
"// generated\n",
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
await fs.writeFile(
|
|
138
|
+
path.join(providerDir(provider), "index.ts"),
|
|
139
|
+
"// generated by cdktn get\n",
|
|
140
|
+
);
|
|
141
|
+
await fs.writeFile(
|
|
142
|
+
path.join(providerDir(provider), "lazy-index.ts"),
|
|
143
|
+
"// generated by cdktn get\n",
|
|
144
|
+
);
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const makeMaker = (targetLanguage: Language) =>
|
|
148
|
+
new ConstructsMaker({
|
|
149
|
+
codeMakerOutput: tmpDir,
|
|
150
|
+
targetLanguage,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
beforeEach(async () => {
|
|
154
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "cdktf.orphans"));
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
afterEach(async () => {
|
|
158
|
+
await fs.remove(tmpDir);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("removes resource folders the current run did not emit", async () => {
|
|
162
|
+
// 'teams' and 'data-lake-pipeline' exist only in the previously generated
|
|
163
|
+
// provider version; the new schema no longer produces them.
|
|
164
|
+
await seedProvider("mongodbatlas", [
|
|
165
|
+
"cluster",
|
|
166
|
+
"teams",
|
|
167
|
+
"data-lake-pipeline",
|
|
168
|
+
]);
|
|
169
|
+
|
|
170
|
+
const maker = makeMaker(Language.TYPESCRIPT);
|
|
171
|
+
maker.emittedResourceFolders = { mongodbatlas: ["cluster"] };
|
|
172
|
+
await maker.removeOrphanedResourceFolders();
|
|
173
|
+
|
|
174
|
+
expect(
|
|
175
|
+
await fs.pathExists(path.join(providerDir("mongodbatlas"), "cluster")),
|
|
176
|
+
).toBe(true);
|
|
177
|
+
expect(
|
|
178
|
+
await fs.pathExists(path.join(providerDir("mongodbatlas"), "teams")),
|
|
179
|
+
).toBe(false);
|
|
180
|
+
expect(
|
|
181
|
+
await fs.pathExists(
|
|
182
|
+
path.join(providerDir("mongodbatlas"), "data-lake-pipeline"),
|
|
183
|
+
),
|
|
184
|
+
).toBe(false);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("keeps provider-level files", async () => {
|
|
188
|
+
await seedProvider("mongodbatlas", ["cluster", "teams"]);
|
|
189
|
+
|
|
190
|
+
const maker = makeMaker(Language.TYPESCRIPT);
|
|
191
|
+
maker.emittedResourceFolders = { mongodbatlas: ["cluster"] };
|
|
192
|
+
await maker.removeOrphanedResourceFolders();
|
|
193
|
+
|
|
194
|
+
expect(
|
|
195
|
+
await fs.pathExists(path.join(providerDir("mongodbatlas"), "index.ts")),
|
|
196
|
+
).toBe(true);
|
|
197
|
+
expect(
|
|
198
|
+
await fs.pathExists(
|
|
199
|
+
path.join(providerDir("mongodbatlas"), "lazy-index.ts"),
|
|
200
|
+
),
|
|
201
|
+
).toBe(true);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("leaves providers that were not regenerated untouched", async () => {
|
|
205
|
+
// filterAlreadyGenerated can skip a provider entirely, in which case it
|
|
206
|
+
// has no entry in emittedResourceFolders and must not be pruned.
|
|
207
|
+
await seedProvider("mongodbatlas", ["cluster"]);
|
|
208
|
+
await seedProvider("onepassword", ["item"]);
|
|
209
|
+
|
|
210
|
+
const maker = makeMaker(Language.TYPESCRIPT);
|
|
211
|
+
maker.emittedResourceFolders = { mongodbatlas: ["cluster"] };
|
|
212
|
+
await maker.removeOrphanedResourceFolders();
|
|
213
|
+
|
|
214
|
+
expect(
|
|
215
|
+
await fs.pathExists(path.join(providerDir("onepassword"), "item")),
|
|
216
|
+
).toBe(true);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("is a no-op for non-TypeScript targets", async () => {
|
|
220
|
+
// Other languages co-locate providers and modules, so a dropped resource
|
|
221
|
+
// is indistinguishable from a hand-written construct.
|
|
222
|
+
await seedProvider("mongodbatlas", ["cluster", "teams"]);
|
|
223
|
+
|
|
224
|
+
const maker = makeMaker(Language.PYTHON);
|
|
225
|
+
maker.emittedResourceFolders = { mongodbatlas: ["cluster"] };
|
|
226
|
+
await maker.removeOrphanedResourceFolders();
|
|
227
|
+
|
|
228
|
+
expect(
|
|
229
|
+
await fs.pathExists(path.join(providerDir("mongodbatlas"), "teams")),
|
|
230
|
+
).toBe(true);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("does not throw when the provider folder is missing", async () => {
|
|
234
|
+
const maker = makeMaker(Language.TYPESCRIPT);
|
|
235
|
+
maker.emittedResourceFolders = { mongodbatlas: ["cluster"] };
|
|
236
|
+
|
|
237
|
+
await expect(
|
|
238
|
+
maker.removeOrphanedResourceFolders(),
|
|
239
|
+
).resolves.not.toThrow();
|
|
240
|
+
});
|
|
241
|
+
});
|
|
118
242
|
});
|
package/src/get/__tests__/generator/fixtures/provider-functions-name-collision.test.fixture.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"provider_schemas": {
|
|
3
|
+
"registry.terraform.io/hashicorp/example": {
|
|
4
|
+
"resource_schemas": {
|
|
5
|
+
"example_provider_functions": {
|
|
6
|
+
"version": 0,
|
|
7
|
+
"block": {
|
|
8
|
+
"attributes": {
|
|
9
|
+
"id": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"description_kind": "plain",
|
|
12
|
+
"computed": true
|
|
13
|
+
},
|
|
14
|
+
"name": {
|
|
15
|
+
"type": "string",
|
|
16
|
+
"description_kind": "plain",
|
|
17
|
+
"optional": true
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"functions": {
|
|
24
|
+
"sum": {
|
|
25
|
+
"description": "Adds up two numbers.",
|
|
26
|
+
"summary": "Sum numbers",
|
|
27
|
+
"return_type": "number",
|
|
28
|
+
"parameters": [
|
|
29
|
+
{
|
|
30
|
+
"name": "a",
|
|
31
|
+
"type": "number"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"name": "b",
|
|
35
|
+
"type": "number"
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Copyright (c) HashiCorp, Inc
|
|
2
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
3
|
+
import * as fs from "fs";
|
|
4
|
+
import * as path from "path";
|
|
5
|
+
import { CodeMaker } from "codemaker";
|
|
6
|
+
import { TerraformProviderGenerator } from "../../generator/provider-generator";
|
|
7
|
+
import { sanitizeClassOrNamespaceName } from "../../generator/resource-parser";
|
|
8
|
+
import { createTmpHelper } from "../util";
|
|
9
|
+
|
|
10
|
+
const tmp = createTmpHelper();
|
|
11
|
+
|
|
12
|
+
// A provider declaring provider-defined functions emits them to
|
|
13
|
+
// providers/<provider>/provider-functions/index.ts. A resource named
|
|
14
|
+
// <provider>_provider_functions maps to that same directory, so without a
|
|
15
|
+
// guard the resource overwrites the functions wrapper and the provider index
|
|
16
|
+
// exports the name twice.
|
|
17
|
+
describe("resource named like the provider functions submodule", () => {
|
|
18
|
+
let workdir: string;
|
|
19
|
+
|
|
20
|
+
beforeAll(async () => {
|
|
21
|
+
const code = new CodeMaker();
|
|
22
|
+
workdir = tmp("provider-functions-name-collision.test");
|
|
23
|
+
const spec = JSON.parse(
|
|
24
|
+
fs.readFileSync(
|
|
25
|
+
path.join(
|
|
26
|
+
__dirname,
|
|
27
|
+
"fixtures",
|
|
28
|
+
"provider-functions-name-collision.test.fixture.json",
|
|
29
|
+
),
|
|
30
|
+
"utf-8",
|
|
31
|
+
),
|
|
32
|
+
);
|
|
33
|
+
new TerraformProviderGenerator(code, spec).generateAll();
|
|
34
|
+
await code.save(workdir);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const read = (relPath: string) =>
|
|
38
|
+
fs.readFileSync(path.join(workdir, relPath), "utf-8");
|
|
39
|
+
|
|
40
|
+
test("the functions submodule holds the functions wrapper, not the resource", () => {
|
|
41
|
+
const output = read("providers/example/provider-functions/index.ts");
|
|
42
|
+
expect(output).toContain("class ExampleProviderFunctions");
|
|
43
|
+
expect(output).not.toContain("cdktn.TerraformResource");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("the resource is emitted to its own directory", () => {
|
|
47
|
+
const output = read(
|
|
48
|
+
"providers/example/provider-functions-resource/index.ts",
|
|
49
|
+
);
|
|
50
|
+
expect(output).toContain("class ProviderFunctionsResource");
|
|
51
|
+
expect(output).toContain("cdktn.TerraformResource");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("the provider index exports both, without duplicate names", () => {
|
|
55
|
+
const index = read("providers/example/index.ts");
|
|
56
|
+
expect(index).toContain(
|
|
57
|
+
"export * as providerFunctions from './provider-functions/index';",
|
|
58
|
+
);
|
|
59
|
+
expect(index).toContain(
|
|
60
|
+
"export * as providerFunctionsResource from './provider-functions-resource/index';",
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const exportedNames = [...index.matchAll(/export \* as (\w+) from/g)].map(
|
|
64
|
+
(m) => m[1],
|
|
65
|
+
);
|
|
66
|
+
expect(new Set(exportedNames).size).toBe(exportedNames.length);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe("sanitizeClassOrNamespaceName", () => {
|
|
71
|
+
test("suffixes a resource named provider_functions", () => {
|
|
72
|
+
expect(sanitizeClassOrNamespaceName("provider_functions")).toBe(
|
|
73
|
+
"provider_functions_resource",
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("still suffixes a resource named provider", () => {
|
|
78
|
+
expect(sanitizeClassOrNamespaceName("provider")).toBe("provider_resource");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("leaves an unrelated resource name alone", () => {
|
|
82
|
+
expect(sanitizeClassOrNamespaceName("function_app")).toBe("function_app");
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -33,7 +33,8 @@ describe("versions.json file generation", () => {
|
|
|
33
33
|
fqn: "kreuzwerker/docker",
|
|
34
34
|
name: "docker",
|
|
35
35
|
source: "kreuzwerker/docker",
|
|
36
|
-
|
|
36
|
+
// OpenTofu rejects everything below 3.7.0 as unsigned; see #440.
|
|
37
|
+
version: "3.9.0",
|
|
37
38
|
},
|
|
38
39
|
];
|
|
39
40
|
|
|
@@ -62,10 +63,14 @@ describe("versions.json file generation", () => {
|
|
|
62
63
|
path.join(workdir, "versions.json"),
|
|
63
64
|
"utf-8",
|
|
64
65
|
);
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
// versions.json is keyed by fully qualified name, and the host is
|
|
67
|
+
// whichever registry the fetching CLI used - terraform's or tofu's - so
|
|
68
|
+
// assert on the provider part rather than pinning one registry.
|
|
69
|
+
const keys = Object.keys(JSON.parse(output)).map((k) =>
|
|
70
|
+
k.split("/").slice(1).join("/"),
|
|
71
|
+
);
|
|
72
|
+
expect(keys).toEqual(
|
|
73
|
+
expect.arrayContaining(constraints.map((c) => c.fqn)),
|
|
69
74
|
);
|
|
70
75
|
},
|
|
71
76
|
);
|
|
@@ -315,6 +315,8 @@ export class ConstructsMaker {
|
|
|
315
315
|
private readonly codeMakerOutdir: string;
|
|
316
316
|
private readonly code: CodeMaker;
|
|
317
317
|
private versions: { [providerName: string]: string | undefined };
|
|
318
|
+
/** Resource directory names emitted per provider folder during this run */
|
|
319
|
+
public emittedResourceFolders: { [providerName: string]: string[] };
|
|
318
320
|
|
|
319
321
|
constructor(
|
|
320
322
|
private readonly options: GetOptions,
|
|
@@ -328,6 +330,7 @@ export class ConstructsMaker {
|
|
|
328
330
|
fs.mkdirpSync(this.codeMakerOutdir);
|
|
329
331
|
this.code = new CodeMaker();
|
|
330
332
|
this.versions = {};
|
|
333
|
+
this.emittedResourceFolders = {};
|
|
331
334
|
}
|
|
332
335
|
private async generateTypescriptProvider(
|
|
333
336
|
target: ConstructsMakerProviderTarget,
|
|
@@ -336,10 +339,15 @@ export class ConstructsMaker {
|
|
|
336
339
|
const endTSTimer = logTimespan(`Generate Typescript for ${target.name}`);
|
|
337
340
|
const generator = new TerraformProviderGenerator(this.code, schema, {
|
|
338
341
|
importExtension: this.options.languageOptions?.importExtension,
|
|
342
|
+
targetVersions: this.options.targetVersions,
|
|
339
343
|
});
|
|
340
344
|
generator.generate(target);
|
|
341
345
|
|
|
342
346
|
this.versions = { ...this.versions, ...generator.versions };
|
|
347
|
+
this.emittedResourceFolders = {
|
|
348
|
+
...this.emittedResourceFolders,
|
|
349
|
+
...generator.emittedResourceFolders,
|
|
350
|
+
};
|
|
343
351
|
endTSTimer();
|
|
344
352
|
}
|
|
345
353
|
|
|
@@ -595,6 +603,62 @@ export class ConstructsMaker {
|
|
|
595
603
|
});
|
|
596
604
|
}
|
|
597
605
|
|
|
606
|
+
/**
|
|
607
|
+
* Delete resource directories under `providers/` that this run did not emit, so each provider folder holds
|
|
608
|
+
* exactly the resources in the version it was generated against.
|
|
609
|
+
*
|
|
610
|
+
* Walks only providers present in `emittedResourceFolders`, keeping any provider this run skipped. Within those,
|
|
611
|
+
* removes every subdirectory not emitted and leaves provider-level files (`index.ts`, `lazy-index.ts`) in place.
|
|
612
|
+
* Deletes files, so it must run after `save()` has written the current run's output.
|
|
613
|
+
*
|
|
614
|
+
* TypeScript only: `providers/<name>/<resource>/` maps one-to-one onto the emitted index exports. Other languages
|
|
615
|
+
* co-locate providers and modules in the same folders, which makes a dropped resource indistinguishable from a
|
|
616
|
+
* hand-written construct.
|
|
617
|
+
*/
|
|
618
|
+
public async removeOrphanedResourceFolders() {
|
|
619
|
+
if (!this.isJavascriptTarget) {
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
for (const [provider, emittedFolders] of Object.entries(
|
|
624
|
+
this.emittedResourceFolders,
|
|
625
|
+
)) {
|
|
626
|
+
const providerFolder = path.resolve(
|
|
627
|
+
this.codeMakerOutdir,
|
|
628
|
+
"providers",
|
|
629
|
+
provider,
|
|
630
|
+
);
|
|
631
|
+
|
|
632
|
+
let entries: string[] = [];
|
|
633
|
+
try {
|
|
634
|
+
entries = await fs.readdir(providerFolder);
|
|
635
|
+
} catch (e) {
|
|
636
|
+
logger.debug(
|
|
637
|
+
`Could not list resource folders in '${providerFolder}': ${e}`,
|
|
638
|
+
);
|
|
639
|
+
continue;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
const expected = new Set(emittedFolders);
|
|
643
|
+
for (const entry of entries) {
|
|
644
|
+
if (expected.has(entry)) {
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
const entryPath = path.resolve(providerFolder, entry);
|
|
649
|
+
if (!fs.statSync(entryPath).isDirectory()) {
|
|
650
|
+
// Leave index.ts / lazy-index.ts and any other provider-level files alone.
|
|
651
|
+
continue;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
logger.debug(
|
|
655
|
+
`Removing orphaned resource folder '${entry}' from provider '${provider}': it was not emitted for the current provider version`,
|
|
656
|
+
);
|
|
657
|
+
await fs.remove(entryPath);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
598
662
|
// emits a constraints.json file with a map of the used provider fqpns and version constraints
|
|
599
663
|
// this is used for caching purposes
|
|
600
664
|
private emitConstraintsFile(
|
|
@@ -757,6 +821,7 @@ a NODE_OPTIONS variable, we won't override it. Hence, the provider generation mi
|
|
|
757
821
|
|
|
758
822
|
if (this.isJavascriptTarget) {
|
|
759
823
|
await this.save();
|
|
824
|
+
await this.removeOrphanedResourceFolders();
|
|
760
825
|
}
|
|
761
826
|
|
|
762
827
|
if (!this.isJavascriptTarget || this.options.outputJsii) {
|
|
@@ -61,13 +61,13 @@ export class StructEmitter {
|
|
|
61
61
|
comment.line(att.description);
|
|
62
62
|
comment.line(``);
|
|
63
63
|
comment.line(
|
|
64
|
-
`Docs at
|
|
64
|
+
`Docs at ${resource.docsRegistryName}: {@link ${resource.linkToDocs}#${att.terraformName} ${resource.className}#${att.terraformName}}`,
|
|
65
65
|
);
|
|
66
66
|
this.warnAboutIdField(att);
|
|
67
67
|
comment.end();
|
|
68
68
|
} else {
|
|
69
69
|
comment.line(
|
|
70
|
-
`Docs at
|
|
70
|
+
`Docs at ${resource.docsRegistryName}: {@link ${resource.linkToDocs}#${att.terraformName} ${resource.className}#${att.terraformName}}`,
|
|
71
71
|
);
|
|
72
72
|
this.warnAboutIdField(att);
|
|
73
73
|
comment.end();
|
|
@@ -215,8 +215,7 @@ export interface CollectionAttributeTypeModel extends AttributeTypeModel {
|
|
|
215
215
|
// into the nearest typed "Any"-wrapper. undefined only defensively - see
|
|
216
216
|
// resolveStoredClassName in supported-stored-classes.ts.
|
|
217
217
|
readonly resolvedStoredClass:
|
|
218
|
-
|
|
219
|
-
| undefined;
|
|
218
|
+
{ name: string; collapsed: boolean } | undefined;
|
|
220
219
|
}
|
|
221
220
|
|
|
222
221
|
export class ListAttributeTypeModel implements CollectionAttributeTypeModel {
|
|
@@ -5,7 +5,11 @@ import path from "path";
|
|
|
5
5
|
import { FQPN, parseFQPN, ProviderName } from "@cdktn/provider-schema";
|
|
6
6
|
import { AttributeModel } from "./attribute-model";
|
|
7
7
|
import { Struct, ConfigStruct } from "./struct";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
Schema,
|
|
10
|
+
TerraformTargetVersions,
|
|
11
|
+
registryForTargetVersions,
|
|
12
|
+
} from "@cdktn/commons";
|
|
9
13
|
import { ProviderFunctionsModel } from "./provider-function-model";
|
|
10
14
|
|
|
11
15
|
// Limit is 1200 to prevent stack size error.
|
|
@@ -25,6 +29,7 @@ interface ResourceModelOptions {
|
|
|
25
29
|
schema: Schema;
|
|
26
30
|
terraformSchemaType: string;
|
|
27
31
|
configStructName: string;
|
|
32
|
+
targetVersions?: TerraformTargetVersions;
|
|
28
33
|
}
|
|
29
34
|
|
|
30
35
|
type DefinitionPath = string;
|
|
@@ -35,6 +40,7 @@ export class ResourceModel {
|
|
|
35
40
|
public baseName: string;
|
|
36
41
|
public provider: ProviderName;
|
|
37
42
|
public fqpn: FQPN;
|
|
43
|
+
private readonly targetVersions?: TerraformTargetVersions;
|
|
38
44
|
public providerVersionConstraint?: string;
|
|
39
45
|
public providerVersion?: string;
|
|
40
46
|
public terraformProviderSource?: string;
|
|
@@ -68,6 +74,7 @@ export class ResourceModel {
|
|
|
68
74
|
this.attributes = options.attributes;
|
|
69
75
|
this.schema = options.schema;
|
|
70
76
|
this.fqpn = options.fqpn;
|
|
77
|
+
this.targetVersions = options.targetVersions;
|
|
71
78
|
this.provider = parseFQPN(options.fqpn).name;
|
|
72
79
|
this.terraformProviderName = this.provider;
|
|
73
80
|
this.fileName = options.fileName;
|
|
@@ -108,16 +115,32 @@ export class ResourceModel {
|
|
|
108
115
|
return this.escapeSchema(JSON.stringify(this.schema, undefined, 2));
|
|
109
116
|
}
|
|
110
117
|
|
|
118
|
+
/** Registry that linkToDocs points at, for use in the surrounding prose. */
|
|
119
|
+
public get docsRegistryName(): string {
|
|
120
|
+
return registryForTargetVersions(this.targetVersions).displayName;
|
|
121
|
+
}
|
|
122
|
+
|
|
111
123
|
public get linkToDocs(): string {
|
|
112
|
-
|
|
124
|
+
// Keyed off the project's declared targets, not the FQPN hostname: that
|
|
125
|
+
// hostname reflects whichever CLI fetched the schema, which would make the
|
|
126
|
+
// generated link differ between a terraform and a tofu `cdktn get`.
|
|
127
|
+
const registry = registryForTargetVersions(this.targetVersions);
|
|
128
|
+
const { namespace, name } = parseFQPN(this.fqpn);
|
|
113
129
|
const version = this.providerVersion || "latest";
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
130
|
+
const kind = this.isProvider
|
|
131
|
+
? "provider"
|
|
132
|
+
: this.isDataSource
|
|
133
|
+
? "data-source"
|
|
134
|
+
: this.isEphemeralResource
|
|
135
|
+
? "ephemeral-resource"
|
|
136
|
+
: "resource";
|
|
137
|
+
return registry.docsUrl(
|
|
138
|
+
namespace,
|
|
139
|
+
name,
|
|
140
|
+
version,
|
|
141
|
+
kind,
|
|
142
|
+
this.terraformDocName,
|
|
143
|
+
);
|
|
121
144
|
}
|
|
122
145
|
|
|
123
146
|
public get isProvider(): boolean {
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
logger,
|
|
9
9
|
ProviderSchema,
|
|
10
10
|
TerraformProviderConstraint,
|
|
11
|
+
TerraformTargetVersions,
|
|
11
12
|
} from "@cdktn/commons";
|
|
12
13
|
import { FQPN, parseFQPN, ProviderName } from "@cdktn/provider-schema";
|
|
13
14
|
import {
|
|
@@ -29,6 +30,8 @@ export interface TerraformProviderGeneratorOptions {
|
|
|
29
30
|
* Defaults to "" (CommonJS).
|
|
30
31
|
*/
|
|
31
32
|
readonly importExtension?: string;
|
|
33
|
+
/** Project's declared targetVersions; selects the docs registry. */
|
|
34
|
+
readonly targetVersions?: TerraformTargetVersions;
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
interface ProviderData {
|
|
@@ -107,12 +110,17 @@ export interface ProviderConstraints {
|
|
|
107
110
|
}
|
|
108
111
|
|
|
109
112
|
export class TerraformProviderGenerator {
|
|
110
|
-
private resourceParser
|
|
113
|
+
private resourceParser: ResourceParser;
|
|
111
114
|
private resourceEmitter: ResourceEmitter;
|
|
112
115
|
private structEmitter: StructEmitter;
|
|
113
116
|
private providerFunctionsEmitter: ProviderFunctionsEmitter;
|
|
114
117
|
private readonly importExtension: string;
|
|
115
118
|
public versions: { [fqpn: string]: string | undefined } = {};
|
|
119
|
+
/**
|
|
120
|
+
* Resource directory names emitted per provider folder Used to prune directories
|
|
121
|
+
* left over from a previous provider version.
|
|
122
|
+
*/
|
|
123
|
+
public emittedResourceFolders: { [provider: string]: string[] } = {};
|
|
116
124
|
|
|
117
125
|
constructor(
|
|
118
126
|
private readonly code: CodeMaker,
|
|
@@ -121,6 +129,7 @@ export class TerraformProviderGenerator {
|
|
|
121
129
|
) {
|
|
122
130
|
this.code.indentation = 2;
|
|
123
131
|
this.importExtension = options.importExtension ?? "";
|
|
132
|
+
this.resourceParser = new ResourceParser(options.targetVersions);
|
|
124
133
|
this.resourceEmitter = new ResourceEmitter(this.code, this.importExtension);
|
|
125
134
|
this.structEmitter = new StructEmitter(this.code, this.importExtension);
|
|
126
135
|
this.providerFunctionsEmitter = new ProviderFunctionsEmitter(this.code);
|
|
@@ -298,6 +307,20 @@ export class TerraformProviderGenerator {
|
|
|
298
307
|
|
|
299
308
|
this.emitIndexFile(name, files);
|
|
300
309
|
this.emitLazyIndexFile(name, files);
|
|
310
|
+
|
|
311
|
+
// Record exactly what this run emitted, derived the same way the index
|
|
312
|
+
// files derive their exports, so pruning can never disagree with them.
|
|
313
|
+
this.emittedResourceFolders[name] = files.map((file) =>
|
|
314
|
+
this.resourceFolderName(name, file),
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Map an emitted file path to the resource directory name it lives in, e.g.
|
|
320
|
+
* `providers/mongodbatlas/teams/index.ts` -> `teams`.
|
|
321
|
+
*/
|
|
322
|
+
private resourceFolderName(provider: ProviderName, file: string): string {
|
|
323
|
+
return file.replace(`providers/${provider}/`, "").replace("/index.ts", "");
|
|
301
324
|
}
|
|
302
325
|
|
|
303
326
|
private emitProviderFunctions(
|
|
@@ -322,7 +345,7 @@ export class TerraformProviderGenerator {
|
|
|
322
345
|
? resource.provider
|
|
323
346
|
: resource.terraformType;
|
|
324
347
|
this.code.line(
|
|
325
|
-
`Refer to the
|
|
348
|
+
`Refer to the ${resource.docsRegistryName} for docs: [\`${type}\`](${resource.linkToDocs}).`,
|
|
326
349
|
);
|
|
327
350
|
this.code.closeFile(filePath);
|
|
328
351
|
}
|
|
@@ -333,7 +356,7 @@ export class TerraformProviderGenerator {
|
|
|
333
356
|
this.code.openFile(filePath);
|
|
334
357
|
this.code.line("// generated by cdktn get");
|
|
335
358
|
for (const file of files) {
|
|
336
|
-
const dirName =
|
|
359
|
+
const dirName = this.resourceFolderName(provider, file);
|
|
337
360
|
this.code.line(
|
|
338
361
|
`export * as ${toCamelCase(dirName)} from './${dirName}/index${this.importExtension}';`,
|
|
339
362
|
);
|
|
@@ -348,7 +371,7 @@ export class TerraformProviderGenerator {
|
|
|
348
371
|
this.code.openFile(filePath);
|
|
349
372
|
this.code.line("// generated by cdktn get");
|
|
350
373
|
for (const file of files) {
|
|
351
|
-
const dirName =
|
|
374
|
+
const dirName = this.resourceFolderName(provider, file);
|
|
352
375
|
this.code.line(
|
|
353
376
|
`Object.defineProperty(exports, '${toCamelCase(
|
|
354
377
|
dirName,
|