@cdktn/provider-generator 0.25.0-pre.9 → 0.25.0
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/README.md +12 -0
- 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-docs-registry.test.d.ts +2 -0
- package/build/get/__tests__/generator/module-docs-registry.test.js +91 -0
- 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__/registry-class-invalidation.test.d.ts +2 -0
- package/build/get/__tests__/registry-class-invalidation.test.js +120 -0
- package/build/get/__tests__/target-versions.test.js +46 -8
- package/build/get/__tests__/util.js +20 -11
- package/build/get/constructs-maker.d.ts +17 -0
- package/build/get/constructs-maker.js +86 -11
- 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/module-generator.d.ts +3 -2
- package/build/get/generator/module-generator.js +15 -17
- 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 +32 -9
- 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/module-docs-registry.test.ts +89 -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/__tests__/registry-class-invalidation.test.ts +123 -0
- package/src/get/__tests__/target-versions.test.ts +43 -0
- package/src/get/constructs-maker.ts +108 -7
- 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/module-generator.ts +20 -18
- 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,89 @@
|
|
|
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 {
|
|
7
|
+
ConstructsMakerModuleTarget,
|
|
8
|
+
Language,
|
|
9
|
+
TerraformModuleConstraint,
|
|
10
|
+
TerraformTargetVersions,
|
|
11
|
+
} from "@cdktn/commons";
|
|
12
|
+
import { ModuleGenerator } from "../../generator/module-generator";
|
|
13
|
+
import { createTmpHelper } from "../util";
|
|
14
|
+
|
|
15
|
+
const tmp = createTmpHelper();
|
|
16
|
+
|
|
17
|
+
async function docsComment(
|
|
18
|
+
source: string,
|
|
19
|
+
targetVersions?: TerraformTargetVersions,
|
|
20
|
+
): Promise<string> {
|
|
21
|
+
const workdir = tmp("module-docs-registry");
|
|
22
|
+
const target = ConstructsMakerModuleTarget.from(
|
|
23
|
+
new TerraformModuleConstraint(source),
|
|
24
|
+
Language.TYPESCRIPT,
|
|
25
|
+
) as ConstructsMakerModuleTarget;
|
|
26
|
+
target.spec = { name: target.name, inputs: [], outputs: [] };
|
|
27
|
+
|
|
28
|
+
const code = new CodeMaker();
|
|
29
|
+
new ModuleGenerator(code, [target], targetVersions);
|
|
30
|
+
await code.save(workdir);
|
|
31
|
+
|
|
32
|
+
const emitted = fs.readFileSync(path.join(workdir, target.fileName), "utf-8");
|
|
33
|
+
const line = emitted
|
|
34
|
+
.split("\n")
|
|
35
|
+
.find((l) => l.includes("Docs at") || l.includes("Source at"));
|
|
36
|
+
return (line ?? "").trim();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe("generated module docs links", () => {
|
|
40
|
+
const module = "terraform-aws-modules/vpc/aws@3.19.0";
|
|
41
|
+
const submodule =
|
|
42
|
+
"terraform-aws-modules/vpc/aws//modules/vpc-endpoints@3.19.0";
|
|
43
|
+
|
|
44
|
+
it("uses the Terraform Registry when nothing is declared", async () => {
|
|
45
|
+
expect(await docsComment(module)).toContain(
|
|
46
|
+
"https://registry.terraform.io/modules/terraform-aws-modules/vpc/aws/3.19.0",
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("uses the OpenTofu Registry when only OpenTofu is declared", async () => {
|
|
51
|
+
const comment = await docsComment(module, { opentofu: ">=1.6.0" });
|
|
52
|
+
expect(comment).toContain("Docs at OpenTofu Registry");
|
|
53
|
+
expect(comment).toContain(
|
|
54
|
+
"https://search.opentofu.org/module/terraform-aws-modules/vpc/aws/v3.19.0",
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("links an OpenTofu submodule with the singular segment", async () => {
|
|
59
|
+
expect(await docsComment(submodule, { opentofu: ">=1.6.0" })).toContain(
|
|
60
|
+
"https://search.opentofu.org/module/terraform-aws-modules/vpc/aws/v3.19.0/submodule/vpc-endpoints",
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Registries serve submodule pages only for a direct child of `modules/`
|
|
65
|
+
// (checked against rendered pages); anything else links to the module.
|
|
66
|
+
it.each([
|
|
67
|
+
[
|
|
68
|
+
"an examples path",
|
|
69
|
+
"terraform-aws-modules/vpc/aws//examples/complete@3.19.0",
|
|
70
|
+
],
|
|
71
|
+
[
|
|
72
|
+
"a nested modules path",
|
|
73
|
+
"terraform-aws-modules/vpc/aws//modules/x/y@3.19.0",
|
|
74
|
+
],
|
|
75
|
+
])("links %s to the module page", async (_name, source) => {
|
|
76
|
+
expect(await docsComment(source)).toContain(
|
|
77
|
+
"{@link https://registry.terraform.io/modules/terraform-aws-modules/vpc/aws/3.19.0 ",
|
|
78
|
+
);
|
|
79
|
+
expect(await docsComment(source, { opentofu: ">=1.6.0" })).toContain(
|
|
80
|
+
"{@link https://search.opentofu.org/module/terraform-aws-modules/vpc/aws/v3.19.0 ",
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("keeps the Terraform submodule shape unchanged", async () => {
|
|
85
|
+
expect(await docsComment(submodule)).toContain(
|
|
86
|
+
"https://registry.terraform.io/modules/terraform-aws-modules/vpc/aws/3.19.0/submodules/vpc-endpoints",
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -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
|
);
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// Copyright (c) HashiCorp, Inc
|
|
2
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
3
|
+
import * as fs from "fs-extra";
|
|
4
|
+
import * as path from "path";
|
|
5
|
+
import {
|
|
6
|
+
Language,
|
|
7
|
+
TerraformProviderConstraint,
|
|
8
|
+
TerraformTargetVersions,
|
|
9
|
+
} from "@cdktn/commons";
|
|
10
|
+
import { createTmpHelper } from "./util";
|
|
11
|
+
|
|
12
|
+
const mockReadSchema = jest.fn();
|
|
13
|
+
jest.mock("@cdktn/provider-schema", () => ({
|
|
14
|
+
readSchema: (...args: unknown[]) => mockReadSchema(...args),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
// Imported after the mock so ConstructsMaker picks up the mocked readSchema.
|
|
18
|
+
import { ConstructsMaker } from "../constructs-maker";
|
|
19
|
+
|
|
20
|
+
const tmp = createTmpHelper();
|
|
21
|
+
|
|
22
|
+
// No resources, so generation stays in TypeScript emission.
|
|
23
|
+
const fakeSchema = {
|
|
24
|
+
providerSchema: {
|
|
25
|
+
format_version: "0.2" as const,
|
|
26
|
+
provider_schemas: {
|
|
27
|
+
"registry.terraform.io/hashicorp/random": {
|
|
28
|
+
resource_schemas: null,
|
|
29
|
+
data_source_schemas: null,
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
provider_versions: { "registry.terraform.io/hashicorp/random": "3.2.0" },
|
|
33
|
+
cli_name: "terraform",
|
|
34
|
+
cli_version: "1.7.5",
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
describe("filterAlreadyGenerated: registry class", () => {
|
|
39
|
+
const constraint = new TerraformProviderConstraint("hashicorp/random@=3.2.0");
|
|
40
|
+
let outdir: string;
|
|
41
|
+
|
|
42
|
+
const maker = (targetVersions?: TerraformTargetVersions) =>
|
|
43
|
+
new ConstructsMaker({
|
|
44
|
+
codeMakerOutput: outdir,
|
|
45
|
+
targetLanguage: Language.TYPESCRIPT,
|
|
46
|
+
targetVersions,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
/** A real previous `cdktn get`, so constraints.json is what generation writes. */
|
|
50
|
+
const generateWith = (targetVersions?: TerraformTargetVersions) =>
|
|
51
|
+
maker(targetVersions).generate([constraint]);
|
|
52
|
+
|
|
53
|
+
beforeEach(() => {
|
|
54
|
+
mockReadSchema.mockReset();
|
|
55
|
+
mockReadSchema.mockResolvedValue(fakeSchema);
|
|
56
|
+
outdir = tmp("registry-class");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it.each<
|
|
60
|
+
[
|
|
61
|
+
string,
|
|
62
|
+
TerraformTargetVersions | undefined,
|
|
63
|
+
TerraformTargetVersions | undefined,
|
|
64
|
+
]
|
|
65
|
+
>([
|
|
66
|
+
["nothing changed", undefined, undefined],
|
|
67
|
+
[
|
|
68
|
+
"both products declared, the same class as nothing",
|
|
69
|
+
undefined,
|
|
70
|
+
{ terraform: ">=1.5.7", opentofu: ">=1.6.0" },
|
|
71
|
+
],
|
|
72
|
+
[
|
|
73
|
+
"a range moving within the OpenTofu class",
|
|
74
|
+
{ opentofu: ">=1.6.0" },
|
|
75
|
+
{ opentofu: ">=1.8.0" },
|
|
76
|
+
],
|
|
77
|
+
])("skips regeneration: %s", async (_name, before, after) => {
|
|
78
|
+
await generateWith(before);
|
|
79
|
+
|
|
80
|
+
await expect(
|
|
81
|
+
maker(after).filterAlreadyGenerated([constraint]),
|
|
82
|
+
).resolves.toEqual([]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("regenerates when the project switches to OpenTofu-only", async () => {
|
|
86
|
+
await generateWith(undefined);
|
|
87
|
+
|
|
88
|
+
await expect(
|
|
89
|
+
maker({ opentofu: ">=1.6.0" }).filterAlreadyGenerated([constraint]),
|
|
90
|
+
).resolves.toEqual([constraint]);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("regenerates when the project switches away from OpenTofu-only", async () => {
|
|
94
|
+
await generateWith({ opentofu: ">=1.6.0" });
|
|
95
|
+
|
|
96
|
+
await expect(
|
|
97
|
+
maker({ terraform: ">=1.5.7" }).filterAlreadyGenerated([constraint]),
|
|
98
|
+
).resolves.toEqual([constraint]);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// constraints.json written before targetVersions was stamped has no such field;
|
|
102
|
+
// it must read as the Terraform class, which is what it was generated for.
|
|
103
|
+
describe("constraints.json from before targetVersions was recorded", () => {
|
|
104
|
+
beforeEach(async () => {
|
|
105
|
+
await generateWith({ terraform: ">=1.5.7" });
|
|
106
|
+
const file = path.join(outdir, "constraints.json");
|
|
107
|
+
const { targetVersions: _dropped, ...older } = fs.readJsonSync(file);
|
|
108
|
+
fs.writeJsonSync(file, older);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("is fresh for a Terraform project", async () => {
|
|
112
|
+
await expect(
|
|
113
|
+
maker().filterAlreadyGenerated([constraint]),
|
|
114
|
+
).resolves.toEqual([]);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("is stale for an OpenTofu-only project", async () => {
|
|
118
|
+
await expect(
|
|
119
|
+
maker({ opentofu: ">=1.6.0" }).filterAlreadyGenerated([constraint]),
|
|
120
|
+
).resolves.toEqual([constraint]);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
});
|
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
import * as fs from "fs-extra";
|
|
4
4
|
import * as path from "path";
|
|
5
5
|
import {
|
|
6
|
+
ConstructsMakerModuleTarget,
|
|
6
7
|
Language,
|
|
7
8
|
TerraformDependencyConstraint,
|
|
9
|
+
TerraformModuleConstraint,
|
|
8
10
|
TerraformTargetVersions,
|
|
9
11
|
} from "@cdktn/commons";
|
|
10
12
|
import { createTmpHelper } from "./util";
|
|
@@ -204,3 +206,44 @@ describe("targetVersions threading through 'cdktn get'", () => {
|
|
|
204
206
|
expect(toGenerate).toEqual([]);
|
|
205
207
|
});
|
|
206
208
|
});
|
|
209
|
+
|
|
210
|
+
describe("targetVersions reaching generated module bindings", () => {
|
|
211
|
+
const moduleConstraint = new TerraformModuleConstraint(
|
|
212
|
+
"terraform-aws-modules/vpc/aws@3.19.0",
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
const emittedSources = (dir: string): string[] =>
|
|
216
|
+
fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
|
|
217
|
+
const full = path.join(dir, entry.name);
|
|
218
|
+
if (entry.isDirectory()) return emittedSources(full);
|
|
219
|
+
return entry.name.endsWith(".ts") ? [fs.readFileSync(full, "utf8")] : [];
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
beforeEach(() => {
|
|
223
|
+
mockReadSchema.mockReset();
|
|
224
|
+
const target = ConstructsMakerModuleTarget.from(
|
|
225
|
+
moduleConstraint,
|
|
226
|
+
Language.TYPESCRIPT,
|
|
227
|
+
) as ConstructsMakerModuleTarget;
|
|
228
|
+
mockReadSchema.mockResolvedValue({
|
|
229
|
+
moduleSchema: {
|
|
230
|
+
[target.moduleKey]: { name: target.name, inputs: [], outputs: [] },
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("links an OpenTofu-only project's modules to the OpenTofu registry", async () => {
|
|
236
|
+
const workdir = tmp("target-versions-module.test");
|
|
237
|
+
const maker = new ConstructsMaker({
|
|
238
|
+
codeMakerOutput: workdir,
|
|
239
|
+
targetLanguage: Language.TYPESCRIPT,
|
|
240
|
+
targetVersions: { opentofu: ">=1.6.0" },
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
await maker.generate([moduleConstraint]);
|
|
244
|
+
|
|
245
|
+
expect(emittedSources(workdir).join("\n")).toContain(
|
|
246
|
+
"https://search.opentofu.org/module/terraform-aws-modules/vpc/aws/v3.19.0",
|
|
247
|
+
);
|
|
248
|
+
});
|
|
249
|
+
});
|