@vertile-ai/iac 0.0.1
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 +255 -0
- package/docs/README.md +40 -0
- package/docs/index.html +276 -0
- package/docs/manifest.md +130 -0
- package/docs/positioning.md +65 -0
- package/docs/roadmap.md +77 -0
- package/examples/dynomic/README.md +22 -0
- package/examples/dynomic/apps/admin/package.json +7 -0
- package/examples/dynomic/apps/admin/vercel.json +4 -0
- package/examples/dynomic/apps/web/package.json +7 -0
- package/examples/dynomic/apps/web/vercel.json +4 -0
- package/examples/dynomic/infrastructure/iac/iac.json +191 -0
- package/examples/dynomic/package.json +17 -0
- package/package.json +51 -0
- package/schema/iac.schema.json +266 -0
- package/src/apply.mjs +38 -0
- package/src/cli.mjs +74 -0
- package/src/core/args.mjs +36 -0
- package/src/core/concepts.mjs +26 -0
- package/src/core/context.mjs +39 -0
- package/src/core/hcl.mjs +110 -0
- package/src/core/manifest.mjs +110 -0
- package/src/core/render.mjs +38 -0
- package/src/core/terraform.mjs +42 -0
- package/src/core/vercel-manifests.mjs +179 -0
- package/src/plan.mjs +32 -0
- package/src/providers/aws/index.mjs +153 -0
- package/src/providers/digitalocean/index.mjs +85 -0
- package/src/providers/vercel/index.mjs +60 -0
- package/src/provision-env.mjs +798 -0
- package/src/reconcile-project-domains.mjs +549 -0
- package/src/reconcile-project-settings.mjs +385 -0
- package/src/render.mjs +27 -0
- package/src/shared.mjs +116 -0
- package/src/sync-env.mjs +297 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://vertile.ai/schemas/iac.schema.json",
|
|
4
|
+
"title": "Vertile AI IaC Manifest",
|
|
5
|
+
"description": "Portable app infrastructure intent for Vertile AI IaC.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["version", "project", "providers", "apps"],
|
|
8
|
+
"additionalProperties": true,
|
|
9
|
+
"properties": {
|
|
10
|
+
"$schema": {
|
|
11
|
+
"type": "string"
|
|
12
|
+
},
|
|
13
|
+
"version": {
|
|
14
|
+
"const": 1
|
|
15
|
+
},
|
|
16
|
+
"project": {
|
|
17
|
+
"oneOf": [
|
|
18
|
+
{ "type": "string", "minLength": 1 },
|
|
19
|
+
{
|
|
20
|
+
"type": "object",
|
|
21
|
+
"required": ["name"],
|
|
22
|
+
"additionalProperties": true,
|
|
23
|
+
"properties": {
|
|
24
|
+
"key": { "type": "string", "minLength": 1 },
|
|
25
|
+
"name": { "type": "string", "minLength": 1 }
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
},
|
|
30
|
+
"environments": {
|
|
31
|
+
"type": "array",
|
|
32
|
+
"items": { "type": "string", "minLength": 1 },
|
|
33
|
+
"uniqueItems": true,
|
|
34
|
+
"default": ["development", "preview", "production"]
|
|
35
|
+
},
|
|
36
|
+
"providers": {
|
|
37
|
+
"type": "object",
|
|
38
|
+
"additionalProperties": { "$ref": "#/$defs/providerConfig" },
|
|
39
|
+
"properties": {
|
|
40
|
+
"vercel": {
|
|
41
|
+
"allOf": [
|
|
42
|
+
{ "$ref": "#/$defs/providerConfig" },
|
|
43
|
+
{
|
|
44
|
+
"type": "object",
|
|
45
|
+
"properties": {
|
|
46
|
+
"team": { "type": "string" },
|
|
47
|
+
"teamId": { "type": "string" },
|
|
48
|
+
"teamSlug": { "type": "string" },
|
|
49
|
+
"projectDefaults": { "$ref": "#/$defs/vercelProjectSettings" },
|
|
50
|
+
"projectSettingsDefaults": { "$ref": "#/$defs/vercelProjectSettings" }
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
},
|
|
55
|
+
"aws": {
|
|
56
|
+
"allOf": [
|
|
57
|
+
{ "$ref": "#/$defs/providerConfig" },
|
|
58
|
+
{
|
|
59
|
+
"type": "object",
|
|
60
|
+
"properties": {
|
|
61
|
+
"region": { "type": "string" },
|
|
62
|
+
"defaultTags": {
|
|
63
|
+
"type": "object",
|
|
64
|
+
"additionalProperties": { "type": ["string", "number", "boolean"] }
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
]
|
|
69
|
+
},
|
|
70
|
+
"digitalocean": {
|
|
71
|
+
"allOf": [
|
|
72
|
+
{ "$ref": "#/$defs/providerConfig" },
|
|
73
|
+
{
|
|
74
|
+
"type": "object",
|
|
75
|
+
"properties": {
|
|
76
|
+
"region": { "type": "string" },
|
|
77
|
+
"version": { "type": "string" }
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
]
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
"env": {
|
|
85
|
+
"type": "object",
|
|
86
|
+
"additionalProperties": true,
|
|
87
|
+
"properties": {
|
|
88
|
+
"sourceDir": { "type": "string" },
|
|
89
|
+
"dir": { "type": "string" },
|
|
90
|
+
"infraDir": { "type": "string" },
|
|
91
|
+
"sharedKey": { "type": "string" },
|
|
92
|
+
"sync": {
|
|
93
|
+
"type": "object",
|
|
94
|
+
"additionalProperties": true,
|
|
95
|
+
"properties": {
|
|
96
|
+
"apps": {
|
|
97
|
+
"type": "array",
|
|
98
|
+
"items": { "type": "string", "minLength": 1 },
|
|
99
|
+
"uniqueItems": true
|
|
100
|
+
},
|
|
101
|
+
"sharedKey": { "type": "string" },
|
|
102
|
+
"skipInVercel": { "type": "boolean" }
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
"apps": {
|
|
108
|
+
"type": "array",
|
|
109
|
+
"items": { "$ref": "#/$defs/app" }
|
|
110
|
+
},
|
|
111
|
+
"domains": {
|
|
112
|
+
"type": "array",
|
|
113
|
+
"items": { "$ref": "#/$defs/domain" }
|
|
114
|
+
},
|
|
115
|
+
"objectStorage": {
|
|
116
|
+
"type": "array",
|
|
117
|
+
"items": { "$ref": "#/$defs/keyedResource" }
|
|
118
|
+
},
|
|
119
|
+
"databases": {
|
|
120
|
+
"type": "array",
|
|
121
|
+
"items": {
|
|
122
|
+
"allOf": [
|
|
123
|
+
{ "$ref": "#/$defs/keyedResource" },
|
|
124
|
+
{
|
|
125
|
+
"type": "object",
|
|
126
|
+
"properties": {
|
|
127
|
+
"engine": { "type": "string" }
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
]
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
"queues": {
|
|
134
|
+
"type": "array",
|
|
135
|
+
"items": {
|
|
136
|
+
"allOf": [
|
|
137
|
+
{ "$ref": "#/$defs/keyedResource" },
|
|
138
|
+
{
|
|
139
|
+
"type": "object",
|
|
140
|
+
"properties": {
|
|
141
|
+
"kind": { "type": "string" }
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
]
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
"sandboxes": {
|
|
148
|
+
"type": "array",
|
|
149
|
+
"items": { "$ref": "#/$defs/keyedResource" }
|
|
150
|
+
},
|
|
151
|
+
"clusters": {
|
|
152
|
+
"type": "array",
|
|
153
|
+
"items": {
|
|
154
|
+
"allOf": [
|
|
155
|
+
{ "$ref": "#/$defs/keyedResource" },
|
|
156
|
+
{
|
|
157
|
+
"type": "object",
|
|
158
|
+
"properties": {
|
|
159
|
+
"size": { "type": "integer", "minimum": 1 },
|
|
160
|
+
"nodes": { "type": "integer", "minimum": 1 }
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
]
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
"$defs": {
|
|
168
|
+
"providerConfig": {
|
|
169
|
+
"type": "object",
|
|
170
|
+
"additionalProperties": true,
|
|
171
|
+
"properties": {
|
|
172
|
+
"resources": {
|
|
173
|
+
"type": "array",
|
|
174
|
+
"items": { "$ref": "#/$defs/providerResource" }
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
"providerResource": {
|
|
179
|
+
"type": "object",
|
|
180
|
+
"required": ["type", "name"],
|
|
181
|
+
"additionalProperties": true,
|
|
182
|
+
"properties": {
|
|
183
|
+
"type": { "type": "string", "minLength": 1 },
|
|
184
|
+
"name": { "type": "string", "minLength": 1 },
|
|
185
|
+
"values": {
|
|
186
|
+
"type": "object",
|
|
187
|
+
"additionalProperties": true
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
"vercelProjectSettings": {
|
|
192
|
+
"type": "object",
|
|
193
|
+
"additionalProperties": true,
|
|
194
|
+
"properties": {
|
|
195
|
+
"rootDirectory": { "type": ["string", "null"] },
|
|
196
|
+
"nodeVersion": { "type": ["string", "null"] },
|
|
197
|
+
"enableAffectedProjectsDeployments": { "type": ["boolean", "null"] }
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
"app": {
|
|
201
|
+
"type": "object",
|
|
202
|
+
"required": ["key"],
|
|
203
|
+
"additionalProperties": true,
|
|
204
|
+
"properties": {
|
|
205
|
+
"key": { "type": "string", "minLength": 1 },
|
|
206
|
+
"id": { "type": "string" },
|
|
207
|
+
"projectId": { "type": "string" },
|
|
208
|
+
"name": { "type": "string" },
|
|
209
|
+
"framework": { "type": "string" },
|
|
210
|
+
"rootDirectory": { "type": "string" },
|
|
211
|
+
"outputDir": { "type": "string" },
|
|
212
|
+
"sourceKey": { "type": "string" },
|
|
213
|
+
"nodeVersion": { "type": "string" },
|
|
214
|
+
"enableAffectedProjectsDeployments": { "type": "boolean" },
|
|
215
|
+
"domains": {
|
|
216
|
+
"type": "array",
|
|
217
|
+
"items": { "$ref": "#/$defs/domain" }
|
|
218
|
+
},
|
|
219
|
+
"env": {
|
|
220
|
+
"type": "object",
|
|
221
|
+
"additionalProperties": true,
|
|
222
|
+
"properties": {
|
|
223
|
+
"sourceKey": { "type": "string" },
|
|
224
|
+
"outputDir": { "type": "string" },
|
|
225
|
+
"sharedPrefix": { "type": "string" }
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
"providers": {
|
|
229
|
+
"type": "object",
|
|
230
|
+
"additionalProperties": true
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
"domain": {
|
|
235
|
+
"oneOf": [
|
|
236
|
+
{ "type": "string", "minLength": 1 },
|
|
237
|
+
{
|
|
238
|
+
"type": "object",
|
|
239
|
+
"required": ["name"],
|
|
240
|
+
"additionalProperties": true,
|
|
241
|
+
"properties": {
|
|
242
|
+
"name": { "type": "string", "minLength": 1 },
|
|
243
|
+
"app": { "type": "string" },
|
|
244
|
+
"project": { "type": "string" },
|
|
245
|
+
"key": { "type": "string" },
|
|
246
|
+
"gitBranch": { "type": ["string", "null"] },
|
|
247
|
+
"verified": { "type": "boolean" }
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
]
|
|
251
|
+
},
|
|
252
|
+
"keyedResource": {
|
|
253
|
+
"type": "object",
|
|
254
|
+
"required": ["key"],
|
|
255
|
+
"additionalProperties": true,
|
|
256
|
+
"properties": {
|
|
257
|
+
"key": { "type": "string", "minLength": 1 },
|
|
258
|
+
"name": { "type": "string" },
|
|
259
|
+
"providers": {
|
|
260
|
+
"type": "object",
|
|
261
|
+
"additionalProperties": true
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
package/src/apply.mjs
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import process from 'node:process'
|
|
4
|
+
import { hasFlag, parseTargetOption, readOption } from './core/args.mjs'
|
|
5
|
+
import { resolvePlatformContext } from './core/context.mjs'
|
|
6
|
+
import { assertEnvironment, readManifest } from './core/manifest.mjs'
|
|
7
|
+
import { writeTargets } from './core/render.mjs'
|
|
8
|
+
import { terraformApply } from './core/terraform.mjs'
|
|
9
|
+
|
|
10
|
+
async function main() {
|
|
11
|
+
const argv = process.argv.slice(2)
|
|
12
|
+
const context = resolvePlatformContext(argv)
|
|
13
|
+
const manifest = readManifest(context.manifestPath)
|
|
14
|
+
const environment = readOption(argv, '--env') || 'production'
|
|
15
|
+
const targets = parseTargetOption(argv)
|
|
16
|
+
const autoApprove = hasFlag(argv, '--yes') || hasFlag(argv, '--auto-approve')
|
|
17
|
+
|
|
18
|
+
assertEnvironment(manifest, environment)
|
|
19
|
+
|
|
20
|
+
if (!autoApprove && !process.stdin.isTTY) {
|
|
21
|
+
throw new Error('Refusing non-interactive apply without --yes.')
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const rendered = await writeTargets({ context, manifest, environment, targets })
|
|
25
|
+
for (const item of rendered) {
|
|
26
|
+
console.log(`Applying ${item.workspace}`)
|
|
27
|
+
terraformApply({
|
|
28
|
+
terraformBin: context.terraformBin,
|
|
29
|
+
workspace: item.workspace,
|
|
30
|
+
autoApprove,
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
main().catch((error) => {
|
|
36
|
+
console.error(error.message)
|
|
37
|
+
process.exit(1)
|
|
38
|
+
})
|
package/src/cli.mjs
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawnSync } from 'node:child_process'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import process from 'node:process'
|
|
6
|
+
import { fileURLToPath } from 'node:url'
|
|
7
|
+
import { sharedOptionsHelp } from './shared.mjs'
|
|
8
|
+
|
|
9
|
+
const command = process.argv[2]
|
|
10
|
+
const args = process.argv.slice(3)
|
|
11
|
+
const root = path.dirname(fileURLToPath(import.meta.url))
|
|
12
|
+
|
|
13
|
+
const commands = new Map([
|
|
14
|
+
['render', path.join(root, 'render.mjs')],
|
|
15
|
+
['plan', path.join(root, 'plan.mjs')],
|
|
16
|
+
['apply', path.join(root, 'apply.mjs')],
|
|
17
|
+
['sync-env', path.join(root, 'sync-env.mjs')],
|
|
18
|
+
['env', path.join(root, 'provision-env.mjs')],
|
|
19
|
+
['projects', path.join(root, 'reconcile-project-settings.mjs')],
|
|
20
|
+
['domains', path.join(root, 'reconcile-project-domains.mjs')],
|
|
21
|
+
])
|
|
22
|
+
|
|
23
|
+
function printHelp() {
|
|
24
|
+
console.log(`vertile-iac
|
|
25
|
+
|
|
26
|
+
Usage:
|
|
27
|
+
vertile-iac render --target=vercel|aws|digitalocean|all --env=<name> [options]
|
|
28
|
+
vertile-iac plan --target=vercel|aws|digitalocean|all --env=<name> [options]
|
|
29
|
+
vertile-iac apply --target=vercel|aws|digitalocean|all --env=<name> [options]
|
|
30
|
+
vertile-iac sync-env [options]
|
|
31
|
+
vertile-iac env [options]
|
|
32
|
+
vertile-iac projects [options]
|
|
33
|
+
vertile-iac domains [options]
|
|
34
|
+
|
|
35
|
+
Commands:
|
|
36
|
+
render Render Terraform workspaces from infrastructure/iac/iac.json.
|
|
37
|
+
plan Render Terraform workspaces and run terraform plan.
|
|
38
|
+
apply Render Terraform workspaces and run terraform apply.
|
|
39
|
+
sync-env Generate package .env files from the configured env source tree.
|
|
40
|
+
env Compatibility: reconcile Vercel team and project environment variables.
|
|
41
|
+
projects Compatibility: reconcile Vercel project settings.
|
|
42
|
+
domains Compatibility: reconcile Vercel project domains.
|
|
43
|
+
|
|
44
|
+
${sharedOptionsHelp()}
|
|
45
|
+
--out <path> Generated Terraform root. Defaults to .vertile/terraform.
|
|
46
|
+
--target <name|all> Target provider: vercel, aws, digitalocean, or all.
|
|
47
|
+
--env <name> Environment to render, plan, or apply. Defaults to production.
|
|
48
|
+
--terraform-bin <path> Terraform executable. Defaults to terraform.
|
|
49
|
+
--yes Allow non-interactive apply with Terraform auto-approve.
|
|
50
|
+
`)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!command || command === '--help' || command === '-h') {
|
|
54
|
+
printHelp()
|
|
55
|
+
process.exit(0)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const script = commands.get(command)
|
|
59
|
+
if (!script) {
|
|
60
|
+
console.error(`Unknown command: ${command}`)
|
|
61
|
+
printHelp()
|
|
62
|
+
process.exit(1)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const result = spawnSync(process.execPath, [script, ...args], {
|
|
66
|
+
stdio: 'inherit',
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
if (result.error) {
|
|
70
|
+
console.error(result.error.message)
|
|
71
|
+
process.exit(1)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
process.exit(result.status ?? 0)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export const supportedTargets = ['vercel', 'aws', 'digitalocean']
|
|
2
|
+
|
|
3
|
+
export function readOption(argv, name) {
|
|
4
|
+
const prefix = `${name}=`
|
|
5
|
+
const inline = argv.find((arg) => arg.startsWith(prefix))
|
|
6
|
+
if (inline) return inline.slice(prefix.length)
|
|
7
|
+
|
|
8
|
+
const index = argv.indexOf(name)
|
|
9
|
+
if (index !== -1) return argv[index + 1] || ''
|
|
10
|
+
|
|
11
|
+
return ''
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function hasFlag(argv, name) {
|
|
15
|
+
return argv.includes(name)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function parseTargetOption(argv) {
|
|
19
|
+
const target = readOption(argv, '--target') || 'all'
|
|
20
|
+
if (target === 'all') return supportedTargets
|
|
21
|
+
|
|
22
|
+
const targets = target
|
|
23
|
+
.split(',')
|
|
24
|
+
.map((value) => value.trim())
|
|
25
|
+
.filter(Boolean)
|
|
26
|
+
|
|
27
|
+
for (const value of targets) {
|
|
28
|
+
if (!supportedTargets.includes(value)) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
`Invalid --target value "${value}". Use one of: ${supportedTargets.join(', ')}, all`,
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return [...new Set(targets)]
|
|
36
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { sanitizeName } from './hcl.mjs'
|
|
2
|
+
|
|
3
|
+
export function providerValues(item, provider) {
|
|
4
|
+
return {
|
|
5
|
+
...(item.values || {}),
|
|
6
|
+
...((item.providers && item.providers[provider]) || {}),
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function resourceName(...parts) {
|
|
11
|
+
return sanitizeName(parts.filter(Boolean).join('_'))
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function providerResourceName(manifest, environment, item) {
|
|
15
|
+
return resourceName(manifest.project.name, environment, item.key)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function compactBody(body) {
|
|
19
|
+
return Object.fromEntries(
|
|
20
|
+
Object.entries(body).filter(([, value]) => value !== undefined && value !== ''),
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function terraformVariableName(...parts) {
|
|
25
|
+
return resourceName(...parts)
|
|
26
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import { findProjectRoot } from '../shared.mjs'
|
|
3
|
+
import { readOption } from './args.mjs'
|
|
4
|
+
|
|
5
|
+
function resolveFrom(rootDir, value) {
|
|
6
|
+
if (!value) return ''
|
|
7
|
+
return path.isAbsolute(value) ? value : path.join(rootDir, value)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function resolvePlatformContext(argv) {
|
|
11
|
+
const repoRootArg = readOption(argv, '--repo-root')
|
|
12
|
+
const repoRoot = repoRootArg
|
|
13
|
+
? path.resolve(repoRootArg)
|
|
14
|
+
: findProjectRoot(process.cwd())
|
|
15
|
+
const iacDir = resolveFrom(
|
|
16
|
+
repoRoot,
|
|
17
|
+
readOption(argv, '--iac-dir') || 'infrastructure/iac',
|
|
18
|
+
)
|
|
19
|
+
const manifestPath = resolveFrom(
|
|
20
|
+
repoRoot,
|
|
21
|
+
readOption(argv, '--iac-manifest') || path.relative(repoRoot, path.join(iacDir, 'iac.json')),
|
|
22
|
+
)
|
|
23
|
+
const generatedRoot = resolveFrom(
|
|
24
|
+
repoRoot,
|
|
25
|
+
readOption(argv, '--out') || '.vertile/terraform',
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
repoRoot,
|
|
30
|
+
iacDir,
|
|
31
|
+
manifestPath,
|
|
32
|
+
generatedRoot,
|
|
33
|
+
terraformBin: readOption(argv, '--terraform-bin') || 'terraform',
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function targetWorkspace(context, target) {
|
|
38
|
+
return path.join(context.generatedRoot, target)
|
|
39
|
+
}
|
package/src/core/hcl.mjs
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
function isPlainObject(value) {
|
|
2
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function quote(value) {
|
|
6
|
+
return JSON.stringify(String(value))
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function formatKey(key) {
|
|
10
|
+
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(key) ? key : quote(key)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function formatValue(value, indent = 0) {
|
|
14
|
+
const pad = ' '.repeat(indent)
|
|
15
|
+
const nestedPad = ' '.repeat(indent + 2)
|
|
16
|
+
|
|
17
|
+
if (typeof value === 'string') return quote(value)
|
|
18
|
+
if (typeof value === 'number' || typeof value === 'boolean') return String(value)
|
|
19
|
+
if (value === null) return 'null'
|
|
20
|
+
|
|
21
|
+
if (Array.isArray(value)) {
|
|
22
|
+
if (value.length === 0) return '[]'
|
|
23
|
+
return `[\n${value.map((item) => `${nestedPad}${formatValue(item, indent + 2)},`).join('\n')}\n${pad}]`
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (isPlainObject(value)) {
|
|
27
|
+
if (value.__raw) return value.value
|
|
28
|
+
const entries = Object.entries(value)
|
|
29
|
+
if (entries.length === 0) return '{}'
|
|
30
|
+
return `{\n${entries.map(([key, item]) => `${nestedPad}${formatKey(key)} = ${formatValue(item, indent + 2)}`).join('\n')}\n${pad}}`
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return quote(value)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function block(type, labels = [], body = {}) {
|
|
37
|
+
const labelText = labels.map((label) => ` ${quote(label)}`).join('')
|
|
38
|
+
const lines = [`${type}${labelText} {`]
|
|
39
|
+
|
|
40
|
+
for (const [key, value] of Object.entries(body)) {
|
|
41
|
+
if (value === undefined) continue
|
|
42
|
+
if (value && typeof value === 'object' && value.__raw) {
|
|
43
|
+
lines.push(` ${key} = ${value.value}`)
|
|
44
|
+
continue
|
|
45
|
+
}
|
|
46
|
+
lines.push(` ${key} = ${formatValue(value, 2)}`)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
lines.push('}')
|
|
50
|
+
return lines.join('\n')
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function nestedBlock(type, body = {}, indent = 0) {
|
|
54
|
+
const pad = ' '.repeat(indent)
|
|
55
|
+
const lines = [`${pad}${type} {`]
|
|
56
|
+
|
|
57
|
+
for (const [key, value] of Object.entries(body)) {
|
|
58
|
+
if (value === undefined) continue
|
|
59
|
+
lines.push(`${pad} ${key} = ${formatValue(value, indent + 2)}`)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
lines.push(`${pad}}`)
|
|
63
|
+
return lines.join('\n')
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function raw(value) {
|
|
67
|
+
return { __raw: true, value }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function sanitizeName(value) {
|
|
71
|
+
const sanitized = String(value)
|
|
72
|
+
.toLowerCase()
|
|
73
|
+
.replace(/[^a-z0-9_]+/g, '_')
|
|
74
|
+
.replace(/^_+|_+$/g, '')
|
|
75
|
+
return sanitized || 'resource'
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function renderGenericResources(resources = []) {
|
|
79
|
+
return resources
|
|
80
|
+
.map((resource) => block('resource', [resource.type, resource.name], resource.values || {}))
|
|
81
|
+
.join('\n\n')
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function renderLocals(manifest, environment) {
|
|
85
|
+
return block('locals', [], {
|
|
86
|
+
project_name: manifest.project.name,
|
|
87
|
+
environment,
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function renderRequiredProvider(name, source, version) {
|
|
92
|
+
return [
|
|
93
|
+
'terraform {',
|
|
94
|
+
' required_providers {',
|
|
95
|
+
` ${name} = {`,
|
|
96
|
+
` source = ${quote(source)}`,
|
|
97
|
+
` version = ${quote(version)}`,
|
|
98
|
+
' }',
|
|
99
|
+
' }',
|
|
100
|
+
'}',
|
|
101
|
+
].join('\n')
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function renderVariable(name, options = {}) {
|
|
105
|
+
return block('variable', [name], options)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function renderOutput(name, options = {}) {
|
|
109
|
+
return block('output', [name], options)
|
|
110
|
+
}
|