@vertile-ai/iac 0.0.1 → 0.1.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 +73 -37
- package/docs/README.md +3 -2
- package/docs/index.html +2 -2
- package/docs/manifest.md +38 -5
- package/examples/bun-hono-api/README.md +9 -0
- package/examples/bun-hono-api/infrastructure/iac/iac.do.json +87 -0
- package/examples/bun-hono-api/infrastructure/iac/iac.json +107 -0
- package/examples/bun-hono-api/package.json +15 -0
- package/examples/bun-hono-api/src/index.ts +7 -0
- package/examples/go-api/README.md +8 -0
- package/examples/go-api/cmd/server/main.go +16 -0
- package/examples/go-api/go.mod +3 -0
- package/examples/go-api/infrastructure/iac/iac.do.json +90 -0
- package/examples/go-api/infrastructure/iac/iac.json +106 -0
- package/examples/next-monorepo/README.md +22 -0
- package/examples/next-monorepo/apps/admin/package.json +7 -0
- package/examples/next-monorepo/apps/web/package.json +7 -0
- package/examples/next-monorepo/infrastructure/iac/iac.aws.json +172 -0
- package/examples/next-monorepo/infrastructure/iac/iac.do.json +129 -0
- package/examples/{dynomic → next-monorepo}/infrastructure/iac/iac.json +102 -23
- package/examples/next-monorepo/infrastructure/iac/iac.vercel.json +109 -0
- package/examples/{dynomic → next-monorepo}/package.json +2 -2
- package/examples/node-api/README.md +8 -0
- package/examples/node-api/infrastructure/iac/iac.aws.json +132 -0
- package/examples/node-api/infrastructure/iac/iac.json +111 -0
- package/examples/node-api/package.json +12 -0
- package/examples/node-api/src/server.js +8 -0
- package/examples/python-fastapi-api/README.md +8 -0
- package/examples/python-fastapi-api/app/main.py +8 -0
- package/examples/python-fastapi-api/infrastructure/iac/iac.aws.json +129 -0
- package/examples/python-fastapi-api/infrastructure/iac/iac.json +113 -0
- package/examples/python-fastapi-api/pyproject.toml +10 -0
- package/examples/react-spa/README.md +8 -0
- package/examples/react-spa/index.html +2 -0
- package/examples/react-spa/infrastructure/iac/iac.aws.json +115 -0
- package/examples/react-spa/infrastructure/iac/iac.json +111 -0
- package/examples/react-spa/infrastructure/iac/iac.vercel.json +76 -0
- package/examples/react-spa/package.json +19 -0
- package/examples/react-spa/src/main.jsx +8 -0
- package/examples/sveltekit-web/README.md +9 -0
- package/examples/sveltekit-web/infrastructure/iac/iac.json +76 -0
- package/examples/sveltekit-web/infrastructure/iac/iac.vercel.json +74 -0
- package/examples/sveltekit-web/package.json +19 -0
- package/examples/sveltekit-web/src/routes/+page.svelte +3 -0
- package/examples/sveltekit-web/svelte.config.js +7 -0
- package/package.json +1 -1
- package/schema/iac.schema.json +83 -2
- package/src/apply.mjs +3 -4
- package/src/cli.mjs +1 -0
- package/src/core/context.mjs +4 -2
- package/src/core/deployments.mjs +39 -0
- package/src/core/env-files.mjs +38 -0
- package/src/core/env-source.mjs +5 -0
- package/src/core/hcl.mjs +2 -1
- package/src/core/manifest.mjs +15 -1
- package/src/core/render.mjs +19 -8
- package/src/core/vercel-manifests.mjs +5 -11
- package/src/plan.mjs +3 -4
- package/src/providers/aws/index.mjs +43 -24
- package/src/provision-env.mjs +72 -24
- package/src/render.mjs +3 -4
- package/src/shared.mjs +0 -4
- package/src/sync-env.mjs +33 -38
- package/examples/dynomic/README.md +0 -22
- package/examples/dynomic/apps/admin/package.json +0 -7
- package/examples/dynomic/apps/web/package.json +0 -7
- /package/examples/{dynomic → next-monorepo}/apps/admin/vercel.json +0 -0
- /package/examples/{dynomic → next-monorepo}/apps/web/vercel.json +0 -0
package/src/core/context.mjs
CHANGED
|
@@ -34,6 +34,8 @@ export function resolvePlatformContext(argv) {
|
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
export function targetWorkspace(context, target) {
|
|
38
|
-
return
|
|
37
|
+
export function targetWorkspace(context, target, deploymentName = '') {
|
|
38
|
+
return deploymentName
|
|
39
|
+
? path.join(context.generatedRoot, target, deploymentName)
|
|
40
|
+
: path.join(context.generatedRoot, target)
|
|
39
41
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
function asObject(value) {
|
|
2
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {}
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function providerDeployments(manifest, target) {
|
|
6
|
+
return asObject(asObject(manifest.providers[target]).deployments)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function resolveDeployment({ manifest, target, environment, deploymentName = '' }) {
|
|
10
|
+
const deployments = providerDeployments(manifest, target)
|
|
11
|
+
const name = deploymentName || (deployments[environment] ? environment : '')
|
|
12
|
+
|
|
13
|
+
if (!name) {
|
|
14
|
+
return {
|
|
15
|
+
name: '',
|
|
16
|
+
environment,
|
|
17
|
+
values: {},
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (!deployments[name] && Object.keys(deployments).length === 0) {
|
|
22
|
+
return {
|
|
23
|
+
name: '',
|
|
24
|
+
environment,
|
|
25
|
+
values: {},
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!deployments[name]) {
|
|
30
|
+
throw new Error(`Unknown ${target} deployment "${name}". Use one of: ${Object.keys(deployments).join(', ')}`)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const values = asObject(deployments[name])
|
|
34
|
+
return {
|
|
35
|
+
name,
|
|
36
|
+
environment: values.environment || environment,
|
|
37
|
+
values,
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const defaultEnvironmentFiles = {
|
|
2
|
+
development: ['.env.development'],
|
|
3
|
+
local: ['.env.local'],
|
|
4
|
+
preview: ['.env.staging'],
|
|
5
|
+
production: ['.env.production'],
|
|
6
|
+
staging: ['.env.staging'],
|
|
7
|
+
test: ['.env.test'],
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function asObject(value) {
|
|
11
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function asFiles(value) {
|
|
15
|
+
if (Array.isArray(value)) return value
|
|
16
|
+
if (typeof value === 'string' && value.trim()) return [value]
|
|
17
|
+
return []
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function environmentConfig(manifest, environment) {
|
|
21
|
+
const configured = asObject(asObject(manifest.env).environments)[environment]
|
|
22
|
+
if (typeof configured === 'string' || Array.isArray(configured)) {
|
|
23
|
+
return { files: asFiles(configured) }
|
|
24
|
+
}
|
|
25
|
+
return asObject(configured)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function environmentFiles(manifest, environment) {
|
|
29
|
+
const config = environmentConfig(manifest, environment)
|
|
30
|
+
const files = asFiles(config.files || config.sources || config.file)
|
|
31
|
+
if (files.length > 0) return files
|
|
32
|
+
return defaultEnvironmentFiles[environment] || [`.env.${environment}`]
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function environmentOutputFile(manifest, environment) {
|
|
36
|
+
const config = environmentConfig(manifest, environment)
|
|
37
|
+
return config.output || config.outputFile || defaultEnvironmentFiles[environment]?.[0] || `.env.${environment}`
|
|
38
|
+
}
|
package/src/core/hcl.mjs
CHANGED
|
@@ -81,10 +81,11 @@ export function renderGenericResources(resources = []) {
|
|
|
81
81
|
.join('\n\n')
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
export function renderLocals(manifest, environment) {
|
|
84
|
+
export function renderLocals(manifest, environment, deployment = {}) {
|
|
85
85
|
return block('locals', [], {
|
|
86
86
|
project_name: manifest.project.name,
|
|
87
87
|
environment,
|
|
88
|
+
deployment: deployment.name || undefined,
|
|
88
89
|
})
|
|
89
90
|
}
|
|
90
91
|
|
package/src/core/manifest.mjs
CHANGED
|
@@ -56,6 +56,20 @@ function validateProviderResources(providers) {
|
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
function validateProviderDeployments(manifest) {
|
|
60
|
+
for (const [provider, config] of Object.entries(manifest.providers)) {
|
|
61
|
+
const deployments = asObject(config.deployments)
|
|
62
|
+
for (const [name, deployment] of Object.entries(deployments)) {
|
|
63
|
+
const environment = asObject(deployment).environment
|
|
64
|
+
if (environment && !manifest.environments.includes(environment)) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`iac.json providers.${provider}.deployments.${name}.environment must be one of: ${manifest.environments.join(', ')}`,
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
59
73
|
export function validateManifest(manifest) {
|
|
60
74
|
if (manifest.version !== 1) {
|
|
61
75
|
throw new Error(`Unsupported iac.json version "${manifest.version}". Expected version 1.`)
|
|
@@ -65,6 +79,7 @@ export function validateManifest(manifest) {
|
|
|
65
79
|
}
|
|
66
80
|
assertStringArray(manifest.environments, 'environments')
|
|
67
81
|
validateProviderResources(manifest.providers)
|
|
82
|
+
validateProviderDeployments(manifest)
|
|
68
83
|
}
|
|
69
84
|
|
|
70
85
|
export function normalizeManifest(rawManifest) {
|
|
@@ -80,7 +95,6 @@ export function normalizeManifest(rawManifest) {
|
|
|
80
95
|
providers: asObject(manifest.providers),
|
|
81
96
|
apps: Array.isArray(manifest.apps) ? manifest.apps.map(normalizeApp) : [],
|
|
82
97
|
domains: Array.isArray(manifest.domains) ? manifest.domains : [],
|
|
83
|
-
infraDir: manifest.infraDir,
|
|
84
98
|
objectStorage: normalizeKeyedList(manifest, 'objectStorage'),
|
|
85
99
|
databases: normalizeKeyedList(manifest, 'databases'),
|
|
86
100
|
queues: normalizeKeyedList(manifest, 'queues'),
|
package/src/core/render.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import fs from 'node:fs/promises'
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
import { targetWorkspace } from './context.mjs'
|
|
4
|
+
import { resolveDeployment } from './deployments.mjs'
|
|
5
|
+
import { assertEnvironment } from './manifest.mjs'
|
|
4
6
|
import { renderTerraform as renderAws } from '../providers/aws/index.mjs'
|
|
5
7
|
import { renderTerraform as renderDigitalOcean } from '../providers/digitalocean/index.mjs'
|
|
6
8
|
import { renderTerraform as renderVercel } from '../providers/vercel/index.mjs'
|
|
@@ -11,28 +13,37 @@ const renderers = {
|
|
|
11
13
|
vercel: renderVercel,
|
|
12
14
|
}
|
|
13
15
|
|
|
14
|
-
export function renderTarget({ manifest, environment, target }) {
|
|
16
|
+
export function renderTarget({ manifest, environment, target, deploymentName = '' }) {
|
|
15
17
|
const render = renderers[target]
|
|
16
18
|
if (!render) throw new Error(`No renderer registered for target "${target}".`)
|
|
17
|
-
|
|
19
|
+
const deployment = resolveDeployment({ manifest, target, environment, deploymentName })
|
|
20
|
+
assertEnvironment(manifest, deployment.environment)
|
|
21
|
+
return render({ manifest, environment: deployment.environment, deployment })
|
|
18
22
|
}
|
|
19
23
|
|
|
20
|
-
export async function writeTarget({ context, manifest, environment, target }) {
|
|
21
|
-
const
|
|
22
|
-
|
|
24
|
+
export async function writeTarget({ context, manifest, environment, target, deploymentName = '' }) {
|
|
25
|
+
const deployment = resolveDeployment({ manifest, target, environment, deploymentName })
|
|
26
|
+
assertEnvironment(manifest, deployment.environment)
|
|
27
|
+
const workspace = targetWorkspace(context, target, deployment.name)
|
|
28
|
+
const files = renderTarget({
|
|
29
|
+
manifest,
|
|
30
|
+
environment: deployment.environment,
|
|
31
|
+
target,
|
|
32
|
+
deploymentName: deployment.name,
|
|
33
|
+
})
|
|
23
34
|
await fs.mkdir(workspace, { recursive: true })
|
|
24
35
|
|
|
25
36
|
for (const [name, contents] of Object.entries(files)) {
|
|
26
37
|
await fs.writeFile(path.join(workspace, name), contents)
|
|
27
38
|
}
|
|
28
39
|
|
|
29
|
-
return { workspace, files }
|
|
40
|
+
return { workspace, files, deployment }
|
|
30
41
|
}
|
|
31
42
|
|
|
32
|
-
export async function writeTargets({ context, manifest, environment, targets }) {
|
|
43
|
+
export async function writeTargets({ context, manifest, environment, targets, deploymentName = '' }) {
|
|
33
44
|
const rendered = []
|
|
34
45
|
for (const target of targets) {
|
|
35
|
-
rendered.push(await writeTarget({ context, manifest, environment, target }))
|
|
46
|
+
rendered.push(await writeTarget({ context, manifest, environment, target, deploymentName }))
|
|
36
47
|
}
|
|
37
48
|
return rendered
|
|
38
49
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
|
+
import { envSourceDir } from './env-source.mjs'
|
|
2
3
|
import { readManifest } from './manifest.mjs'
|
|
3
4
|
|
|
4
5
|
const projectSettingKeys = [
|
|
@@ -50,16 +51,6 @@ function teamSlugFromManifest(manifest) {
|
|
|
50
51
|
return config.teamSlug || config.team || ''
|
|
51
52
|
}
|
|
52
53
|
|
|
53
|
-
function envSourceDir(manifest) {
|
|
54
|
-
return (
|
|
55
|
-
manifest.env.sourceDir ||
|
|
56
|
-
manifest.env.dir ||
|
|
57
|
-
manifest.env.infraDir ||
|
|
58
|
-
manifest.infraDir ||
|
|
59
|
-
'infrastructure'
|
|
60
|
-
)
|
|
61
|
-
}
|
|
62
|
-
|
|
63
54
|
function configuredProjects(manifest) {
|
|
64
55
|
return manifest.apps.map((app) => {
|
|
65
56
|
const values = appVercelValues(app)
|
|
@@ -130,9 +121,12 @@ function projectDomains(manifest) {
|
|
|
130
121
|
}
|
|
131
122
|
|
|
132
123
|
export function vercelEnvManifestFromIac(manifest, context = {}) {
|
|
124
|
+
const config = vercelConfig(manifest)
|
|
133
125
|
return {
|
|
134
126
|
teamSlug: teamSlugFromManifest(manifest),
|
|
135
|
-
|
|
127
|
+
sourceDir: envSourceDir(manifest),
|
|
128
|
+
environmentFiles: manifest.env?.environments || {},
|
|
129
|
+
targets: config.env?.targets || {},
|
|
136
130
|
projects: configuredProjects(manifest),
|
|
137
131
|
}
|
|
138
132
|
}
|
package/src/plan.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import process from 'node:process'
|
|
4
4
|
import { parseTargetOption, readOption } from './core/args.mjs'
|
|
5
5
|
import { resolvePlatformContext } from './core/context.mjs'
|
|
6
|
-
import {
|
|
6
|
+
import { readManifest } from './core/manifest.mjs'
|
|
7
7
|
import { writeTargets } from './core/render.mjs'
|
|
8
8
|
import { terraformPlan } from './core/terraform.mjs'
|
|
9
9
|
|
|
@@ -12,11 +12,10 @@ async function main() {
|
|
|
12
12
|
const context = resolvePlatformContext(argv)
|
|
13
13
|
const manifest = readManifest(context.manifestPath)
|
|
14
14
|
const environment = readOption(argv, '--env') || 'production'
|
|
15
|
+
const deploymentName = readOption(argv, '--deployment') || ''
|
|
15
16
|
const targets = parseTargetOption(argv)
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const rendered = await writeTargets({ context, manifest, environment, targets })
|
|
18
|
+
const rendered = await writeTargets({ context, manifest, environment, targets, deploymentName })
|
|
20
19
|
for (const item of rendered) {
|
|
21
20
|
console.log(`Planning ${item.workspace}`)
|
|
22
21
|
terraformPlan({
|
|
@@ -16,38 +16,54 @@ import {
|
|
|
16
16
|
terraformVariableName,
|
|
17
17
|
} from '../../core/concepts.mjs'
|
|
18
18
|
|
|
19
|
-
function
|
|
19
|
+
function deploymentLabel(environment, deployment = {}) {
|
|
20
|
+
return deployment.name || environment
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function providerBlock(manifest, environment, deployment = {}) {
|
|
20
24
|
const config = manifest.providers.aws || {}
|
|
25
|
+
const deploymentValues = deployment.values || {}
|
|
21
26
|
const body = {
|
|
22
|
-
region: config.region,
|
|
27
|
+
region: deploymentValues.region || config.region,
|
|
28
|
+
profile: deploymentValues.profile || config.profile,
|
|
23
29
|
}
|
|
24
|
-
const tags = {
|
|
30
|
+
const tags = compactBody({
|
|
25
31
|
Project: manifest.project.name,
|
|
26
32
|
Environment: environment,
|
|
33
|
+
Deployment: deployment.name || undefined,
|
|
27
34
|
...(config.defaultTags || {}),
|
|
28
|
-
|
|
35
|
+
...(deploymentValues.tags || {}),
|
|
36
|
+
})
|
|
29
37
|
|
|
30
38
|
const provider = block('provider', ['aws'], body)
|
|
31
|
-
|
|
39
|
+
const assumeRole = deploymentValues.assumeRole || deploymentValues.assume_role || config.assumeRole || config.assume_role
|
|
40
|
+
const assumeRoleBlock = assumeRole
|
|
41
|
+
? `\n ${nestedBlock('assume_role', assumeRole).replace(/\n/g, '\n ')}`
|
|
42
|
+
: ''
|
|
43
|
+
const defaultTagsBlock = Object.keys(tags).length > 0
|
|
44
|
+
? `\n ${nestedBlock('default_tags', { tags }).replace(/\n/g, '\n ')}`
|
|
45
|
+
: ''
|
|
32
46
|
|
|
33
|
-
return `${provider.slice(0, -1)}
|
|
47
|
+
return `${provider.slice(0, -1)}${assumeRoleBlock}${defaultTagsBlock}\n}`
|
|
34
48
|
}
|
|
35
49
|
|
|
36
|
-
function objectStorageBlocks(manifest, environment) {
|
|
50
|
+
function objectStorageBlocks(manifest, environment, deployment = {}) {
|
|
51
|
+
const nameEnvironment = deploymentLabel(environment, deployment)
|
|
37
52
|
return manifest.objectStorage.map((item) => {
|
|
38
53
|
const values = providerValues(item, 'aws')
|
|
39
54
|
return block('resource', ['aws_s3_bucket', resourceName('object_storage', item.key)], compactBody({
|
|
40
|
-
bucket: values.bucket || providerResourceName(manifest,
|
|
55
|
+
bucket: values.bucket || providerResourceName(manifest, nameEnvironment, item),
|
|
41
56
|
force_destroy: values.forceDestroy,
|
|
42
57
|
}))
|
|
43
58
|
})
|
|
44
59
|
}
|
|
45
60
|
|
|
46
|
-
function queueBlocks(manifest, environment) {
|
|
61
|
+
function queueBlocks(manifest, environment, deployment = {}) {
|
|
62
|
+
const nameEnvironment = deploymentLabel(environment, deployment)
|
|
47
63
|
return manifest.queues.map((item) => {
|
|
48
64
|
const values = providerValues(item, 'aws')
|
|
49
65
|
const fifo = values.fifo || item.kind === 'fifo'
|
|
50
|
-
const baseName = values.name || providerResourceName(manifest,
|
|
66
|
+
const baseName = values.name || providerResourceName(manifest, nameEnvironment, item)
|
|
51
67
|
return block('resource', ['aws_sqs_queue', resourceName('queue', item.key)], compactBody({
|
|
52
68
|
name: fifo && !baseName.endsWith('.fifo') ? `${baseName}.fifo` : baseName,
|
|
53
69
|
fifo_queue: fifo || undefined,
|
|
@@ -57,12 +73,13 @@ function queueBlocks(manifest, environment) {
|
|
|
57
73
|
})
|
|
58
74
|
}
|
|
59
75
|
|
|
60
|
-
function databaseBlocks(manifest, environment) {
|
|
76
|
+
function databaseBlocks(manifest, environment, deployment = {}) {
|
|
77
|
+
const nameEnvironment = deploymentLabel(environment, deployment)
|
|
61
78
|
return manifest.databases.map((item) => {
|
|
62
79
|
const values = providerValues(item, 'aws')
|
|
63
80
|
const variable = terraformVariableName('database', item.key, 'password')
|
|
64
81
|
return block('resource', ['aws_db_instance', resourceName('database', item.key)], compactBody({
|
|
65
|
-
identifier: values.identifier || providerResourceName(manifest,
|
|
82
|
+
identifier: values.identifier || providerResourceName(manifest, nameEnvironment, item),
|
|
66
83
|
engine: values.engine || item.engine || 'postgres',
|
|
67
84
|
engine_version: values.engineVersion,
|
|
68
85
|
instance_class: values.instanceClass || 'db.t4g.micro',
|
|
@@ -101,19 +118,21 @@ function amazonLinuxDataSource() {
|
|
|
101
118
|
].join('\n')
|
|
102
119
|
}
|
|
103
120
|
|
|
104
|
-
function computeBlocks(manifest, environment, field, prefix) {
|
|
121
|
+
function computeBlocks(manifest, environment, deployment, field, prefix) {
|
|
122
|
+
const nameEnvironment = deploymentLabel(environment, deployment)
|
|
105
123
|
return manifest[field].map((item) => {
|
|
106
124
|
const values = providerValues(item, 'aws')
|
|
107
125
|
return block('resource', ['aws_instance', resourceName(prefix, item.key)], compactBody({
|
|
108
126
|
ami: values.ami ? values.ami : raw('data.aws_ami.amazon_linux.id'),
|
|
109
127
|
instance_type: values.instanceType || 't3.micro',
|
|
110
128
|
count: field === 'clusters' ? values.size || item.size || 1 : undefined,
|
|
111
|
-
tags: {
|
|
112
|
-
Name: values.name || providerResourceName(manifest,
|
|
129
|
+
tags: compactBody({
|
|
130
|
+
Name: values.name || providerResourceName(manifest, nameEnvironment, item),
|
|
113
131
|
Project: manifest.project.name,
|
|
114
132
|
Environment: environment,
|
|
133
|
+
Deployment: deployment.name || undefined,
|
|
115
134
|
Kind: prefix,
|
|
116
|
-
},
|
|
135
|
+
}),
|
|
117
136
|
}))
|
|
118
137
|
})
|
|
119
138
|
}
|
|
@@ -127,19 +146,19 @@ function outputBlocks(manifest) {
|
|
|
127
146
|
))
|
|
128
147
|
}
|
|
129
148
|
|
|
130
|
-
export function renderTerraform({ manifest, environment }) {
|
|
149
|
+
export function renderTerraform({ manifest, environment, deployment = {} }) {
|
|
131
150
|
const config = manifest.providers.aws || {}
|
|
132
151
|
const resources = renderGenericResources(config.resources)
|
|
133
152
|
const needsAmazonLinux = manifest.sandboxes.length > 0 || manifest.clusters.length > 0
|
|
134
153
|
const mainBlocks = [
|
|
135
|
-
renderLocals(manifest, environment),
|
|
136
|
-
providerBlock(manifest, environment),
|
|
154
|
+
renderLocals(manifest, environment, deployment),
|
|
155
|
+
providerBlock(manifest, environment, deployment),
|
|
137
156
|
needsAmazonLinux ? amazonLinuxDataSource() : '',
|
|
138
|
-
...objectStorageBlocks(manifest, environment),
|
|
139
|
-
...queueBlocks(manifest, environment),
|
|
140
|
-
...databaseBlocks(manifest, environment),
|
|
141
|
-
...computeBlocks(manifest, environment, 'sandboxes', 'sandbox'),
|
|
142
|
-
...computeBlocks(manifest, environment, 'clusters', 'cluster'),
|
|
157
|
+
...objectStorageBlocks(manifest, environment, deployment),
|
|
158
|
+
...queueBlocks(manifest, environment, deployment),
|
|
159
|
+
...databaseBlocks(manifest, environment, deployment),
|
|
160
|
+
...computeBlocks(manifest, environment, deployment, 'sandboxes', 'sandbox'),
|
|
161
|
+
...computeBlocks(manifest, environment, deployment, 'clusters', 'cluster'),
|
|
143
162
|
resources,
|
|
144
163
|
].filter(Boolean)
|
|
145
164
|
const variableBlocks = databaseVariables(manifest)
|
package/src/provision-env.mjs
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import fs from 'node:fs'
|
|
4
4
|
import path from 'node:path'
|
|
5
5
|
import process from 'node:process'
|
|
6
|
+
import { environmentFiles } from './core/env-files.mjs'
|
|
6
7
|
import { readVercelEnvManifest } from './core/vercel-manifests.mjs'
|
|
7
8
|
import { resolveIacContext } from './shared.mjs'
|
|
8
9
|
|
|
@@ -140,6 +141,20 @@ function parseEnvFile(filePath) {
|
|
|
140
141
|
return entries
|
|
141
142
|
}
|
|
142
143
|
|
|
144
|
+
function mergeEntries(layers) {
|
|
145
|
+
const order = []
|
|
146
|
+
const values = new Map()
|
|
147
|
+
|
|
148
|
+
for (const layer of layers) {
|
|
149
|
+
for (const { key, value } of layer) {
|
|
150
|
+
if (!values.has(key)) order.push(key)
|
|
151
|
+
values.set(key, value)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return order.map((key) => ({ key, value: values.get(key) }))
|
|
156
|
+
}
|
|
157
|
+
|
|
143
158
|
function readTokenFromFile(filePath) {
|
|
144
159
|
if (!fs.existsSync(filePath)) return ''
|
|
145
160
|
|
|
@@ -162,24 +177,50 @@ function readTokenFromFile(filePath) {
|
|
|
162
177
|
return ''
|
|
163
178
|
}
|
|
164
179
|
|
|
165
|
-
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
180
|
+
function targetEnvironment(manifest, target) {
|
|
181
|
+
const configured = manifest.targets?.[target]
|
|
182
|
+
if (typeof configured === 'string') return configured
|
|
183
|
+
if (configured && typeof configured === 'object' && configured.environment) {
|
|
184
|
+
return configured.environment
|
|
185
|
+
}
|
|
186
|
+
return target === 'preview' ? 'staging' : target
|
|
170
187
|
}
|
|
171
188
|
|
|
172
|
-
function
|
|
173
|
-
|
|
189
|
+
function targetEnvFiles(manifest, environment) {
|
|
190
|
+
return environmentFiles({ env: { environments: manifest.environmentFiles || {} } }, environment)
|
|
191
|
+
}
|
|
174
192
|
|
|
175
|
-
|
|
193
|
+
function readEnvFiles(baseDir, files, { requireFiles = false } = {}) {
|
|
194
|
+
const filePaths = files.map((file) => path.join(baseDir, file))
|
|
195
|
+
const missing = filePaths.filter((filePath) => !fs.existsSync(filePath))
|
|
176
196
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
197
|
+
if (requireFiles && missing.length > 0) {
|
|
198
|
+
throw new Error(
|
|
199
|
+
`Missing required env source file(s) for --reconcile-delete: ${missing.map((filePath) => path.relative(rootDir, filePath)).join(', ')}`,
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return mergeEntries(filePaths.map(parseEnvFile))
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function readSourceScopedEntries(sourceDir, environment, projects, files, options = {}) {
|
|
207
|
+
const {
|
|
208
|
+
readTeam = true,
|
|
209
|
+
readProjects = true,
|
|
210
|
+
...readOptions
|
|
211
|
+
} = options
|
|
212
|
+
const teamEntries = readTeam
|
|
213
|
+
? readEnvFiles(path.join(rootDir, sourceDir, 'shared'), files, readOptions)
|
|
214
|
+
: []
|
|
215
|
+
|
|
216
|
+
const projectEntries = readProjects
|
|
217
|
+
? Object.fromEntries(
|
|
218
|
+
projects.map((project) => [
|
|
219
|
+
project.key,
|
|
220
|
+
readEnvFiles(path.join(rootDir, sourceDir, project.key), files, readOptions),
|
|
221
|
+
]),
|
|
222
|
+
)
|
|
223
|
+
: {}
|
|
183
224
|
|
|
184
225
|
return { teamEntries, projectEntries }
|
|
185
226
|
}
|
|
@@ -631,13 +672,13 @@ async function main() {
|
|
|
631
672
|
const teamSlug = manifest.teamSlug
|
|
632
673
|
const projects = Array.isArray(manifest.projects) ? manifest.projects : []
|
|
633
674
|
const configuredProjects = [...projects]
|
|
634
|
-
const
|
|
675
|
+
const sourceDir = manifest.sourceDir
|
|
635
676
|
|
|
636
677
|
if (!teamSlug) {
|
|
637
678
|
throw new Error('Missing "teamSlug" in env manifest')
|
|
638
679
|
}
|
|
639
|
-
if (!
|
|
640
|
-
throw new Error('Missing "
|
|
680
|
+
if (!sourceDir) {
|
|
681
|
+
throw new Error('Missing "sourceDir" in env manifest')
|
|
641
682
|
}
|
|
642
683
|
|
|
643
684
|
let teamId = ''
|
|
@@ -746,12 +787,16 @@ async function main() {
|
|
|
746
787
|
`${c.bold('Mode:')} ${dryRun ? c.yellow('dry-run') : c.green('apply')} ${c.gray('|')} scope=${c.cyan(args.scope)} ${c.gray('|')} targets=${c.cyan(args.targets.join(','))} ${c.gray('|')} reconcile-delete=${c.cyan(args.reconcileDelete ? 'on' : 'off')}`,
|
|
747
788
|
)
|
|
748
789
|
|
|
749
|
-
// preview deployments read from the staging env files
|
|
750
|
-
const resolvedTarget = (target) => (target === 'preview' ? 'staging' : target)
|
|
751
|
-
|
|
752
790
|
if (args.scope === 'team' || args.scope === 'all') {
|
|
753
791
|
for (const target of args.targets) {
|
|
754
|
-
const
|
|
792
|
+
const environment = targetEnvironment(manifest, target)
|
|
793
|
+
const parsed = readSourceScopedEntries(
|
|
794
|
+
sourceDir,
|
|
795
|
+
environment,
|
|
796
|
+
[],
|
|
797
|
+
targetEnvFiles(manifest, environment),
|
|
798
|
+
{ requireFiles: args.reconcileDelete, readProjects: false },
|
|
799
|
+
)
|
|
755
800
|
await upsertTeamShared({
|
|
756
801
|
token,
|
|
757
802
|
dryRun,
|
|
@@ -767,10 +812,13 @@ async function main() {
|
|
|
767
812
|
if (args.scope === 'projects' || args.scope === 'all') {
|
|
768
813
|
const parsedByTarget = {}
|
|
769
814
|
for (const target of args.targets) {
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
815
|
+
const environment = targetEnvironment(manifest, target)
|
|
816
|
+
parsedByTarget[target] = readSourceScopedEntries(
|
|
817
|
+
sourceDir,
|
|
818
|
+
environment,
|
|
773
819
|
selectedProjects,
|
|
820
|
+
targetEnvFiles(manifest, environment),
|
|
821
|
+
{ requireFiles: args.reconcileDelete, readTeam: false },
|
|
774
822
|
)
|
|
775
823
|
}
|
|
776
824
|
|
package/src/render.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import process from 'node:process'
|
|
4
4
|
import { parseTargetOption, readOption } from './core/args.mjs'
|
|
5
5
|
import { resolvePlatformContext } from './core/context.mjs'
|
|
6
|
-
import {
|
|
6
|
+
import { readManifest } from './core/manifest.mjs'
|
|
7
7
|
import { writeTargets } from './core/render.mjs'
|
|
8
8
|
|
|
9
9
|
async function main() {
|
|
@@ -11,11 +11,10 @@ async function main() {
|
|
|
11
11
|
const context = resolvePlatformContext(argv)
|
|
12
12
|
const manifest = readManifest(context.manifestPath)
|
|
13
13
|
const environment = readOption(argv, '--env') || 'production'
|
|
14
|
+
const deploymentName = readOption(argv, '--deployment') || ''
|
|
14
15
|
const targets = parseTargetOption(argv)
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const rendered = await writeTargets({ context, manifest, environment, targets })
|
|
17
|
+
const rendered = await writeTargets({ context, manifest, environment, targets, deploymentName })
|
|
19
18
|
for (const item of rendered) {
|
|
20
19
|
console.log(`Rendered ${item.workspace}`)
|
|
21
20
|
}
|
package/src/shared.mjs
CHANGED
|
@@ -55,8 +55,6 @@ export function resolveIacContext(argv, defaults = {}) {
|
|
|
55
55
|
const projectSettingsArg = readOption(argv, '--project-settings')
|
|
56
56
|
const projectDomainsArg = readOption(argv, '--project-domains')
|
|
57
57
|
const iacManifestArg = readOption(argv, '--iac-manifest')
|
|
58
|
-
const infraDir =
|
|
59
|
-
readOption(argv, '--infra-dir') || defaults.infraDir || ''
|
|
60
58
|
|
|
61
59
|
const autoCreateKeys = new Set([
|
|
62
60
|
...splitList(defaults.autoCreateKeys || ''),
|
|
@@ -70,7 +68,6 @@ export function resolveIacContext(argv, defaults = {}) {
|
|
|
70
68
|
return {
|
|
71
69
|
repoRoot,
|
|
72
70
|
iacDir,
|
|
73
|
-
infraDir,
|
|
74
71
|
manifestPath: resolveFrom(
|
|
75
72
|
repoRoot,
|
|
76
73
|
manifestArg || path.relative(repoRoot, path.join(iacDir, 'env-manifest.json')),
|
|
@@ -109,7 +106,6 @@ export function sharedOptionsHelp() {
|
|
|
109
106
|
--project-settings <path> Project settings manifest path.
|
|
110
107
|
--project-domains <path> Project domains manifest path.
|
|
111
108
|
--iac-manifest <path> Unified IaC manifest path. Defaults to <iac-dir>/iac.json.
|
|
112
|
-
--infra-dir <path> Override manifest infraDir.
|
|
113
109
|
--token-file <path> Token file. Defaults to <repo-root>/.vercel.token.
|
|
114
110
|
--auto-create-keys <a,b> Project keys allowed to be created in apply mode.
|
|
115
111
|
--auto-create-prefixes <a,b> Project key prefixes allowed to be created in apply mode.`
|