project-devkit 2.0.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/cli.mjs +173 -0
- package/core/loader.mjs +151 -0
- package/core/utils.mjs +259 -0
- package/package.json +27 -0
- package/plugins/backups/README.md +88 -0
- package/plugins/backups/backups.mjs +342 -0
- package/plugins/backups/config.yml +31 -0
- package/plugins/backups/index.mjs +53 -0
- package/plugins/directus/config.yml +18 -0
- package/plugins/directus/directus.mjs +179 -0
- package/plugins/directus/index.mjs +54 -0
- package/plugins/docker/config.yml +5 -0
- package/plugins/docker/index.mjs +42 -0
- package/plugins/env/config.yml +40 -0
- package/plugins/env/env.mjs +249 -0
- package/plugins/env/index.mjs +93 -0
- package/plugins/env/infisical.mjs +152 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
function handleError(e) {
|
|
4
|
+
console.error(`Error: ${e.message}`);
|
|
5
|
+
process.exit(1);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Register docker plugin commands.
|
|
10
|
+
* @param {import('commander').Command} program
|
|
11
|
+
* @param {object} context
|
|
12
|
+
*/
|
|
13
|
+
export function register(program, { config, utils, projectRoot, devkitRoot }) {
|
|
14
|
+
const dockerCmd = program
|
|
15
|
+
.command('docker')
|
|
16
|
+
.description(config.description || 'Docker environment management');
|
|
17
|
+
|
|
18
|
+
dockerCmd
|
|
19
|
+
.command('setup')
|
|
20
|
+
.description('Create required Docker networks (external networks from compose)')
|
|
21
|
+
.action(() => {
|
|
22
|
+
try {
|
|
23
|
+
const composeFile = config.compose_file || 'docker-compose.dev.yml';
|
|
24
|
+
const networks = utils.getComposeExternalNetworks(projectRoot, composeFile);
|
|
25
|
+
if (networks.length === 0) {
|
|
26
|
+
console.log('No external networks found in compose file.');
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const created = utils.ensureDockerNetworks(projectRoot, composeFile);
|
|
30
|
+
for (const net of networks) {
|
|
31
|
+
if (created.includes(net)) {
|
|
32
|
+
console.log(chalk.green(` Created network: ${net}`));
|
|
33
|
+
} else {
|
|
34
|
+
console.log(` Network exists: ${net}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
console.log(chalk.green('Done!'));
|
|
38
|
+
} catch (e) {
|
|
39
|
+
handleError(e);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
name: env
|
|
2
|
+
description: Manage .env files from examples + Infisical secrets
|
|
3
|
+
|
|
4
|
+
# Infisical configuration — override in .devkit.d/env.yml
|
|
5
|
+
infisical:
|
|
6
|
+
project_id: ~
|
|
7
|
+
site_url: ~
|
|
8
|
+
client_id_env: INFISICAL_UNIVERSAL_AUTH_CLIENT_ID
|
|
9
|
+
client_secret_env: INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET
|
|
10
|
+
|
|
11
|
+
# Subprojects — override in .devkit.d/env.yml
|
|
12
|
+
# Example:
|
|
13
|
+
# subprojects:
|
|
14
|
+
# myproject:
|
|
15
|
+
# path: .
|
|
16
|
+
# variants: [inner, outer]
|
|
17
|
+
# config_dir: config
|
|
18
|
+
# infisical_path: "/{project}/{variant}"
|
|
19
|
+
subprojects: {}
|
|
20
|
+
|
|
21
|
+
# Servers for deploy command — override in .devkit.d/env.yml
|
|
22
|
+
# Example:
|
|
23
|
+
# servers:
|
|
24
|
+
# test:
|
|
25
|
+
# ssh: user@host
|
|
26
|
+
# projects_path: /opt/projects
|
|
27
|
+
# confirm: false
|
|
28
|
+
# stage:
|
|
29
|
+
# ssh: user@host
|
|
30
|
+
# projects_path: /opt/projects
|
|
31
|
+
# confirm: true
|
|
32
|
+
servers: {}
|
|
33
|
+
|
|
34
|
+
# Containers for deploy — override in .devkit.d/env.yml
|
|
35
|
+
# Example:
|
|
36
|
+
# containers:
|
|
37
|
+
# myproject:
|
|
38
|
+
# inner: container-inner
|
|
39
|
+
# outer: container-outer
|
|
40
|
+
containers: {}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { resolve } from 'path';
|
|
2
|
+
import { existsSync, readFileSync, copyFileSync, writeFileSync } from 'fs';
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Get Infisical credentials from config + environment variables.
|
|
8
|
+
*/
|
|
9
|
+
function getInfisicalCredentials(config) {
|
|
10
|
+
const infisical = config.infisical || {};
|
|
11
|
+
const clientIdEnv = infisical.client_id_env || 'INFISICAL_UNIVERSAL_AUTH_CLIENT_ID';
|
|
12
|
+
const clientSecretEnv = infisical.client_secret_env || 'INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET';
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
siteUrl: infisical.site_url,
|
|
16
|
+
projectId: infisical.project_id,
|
|
17
|
+
clientId: process.env[clientIdEnv],
|
|
18
|
+
clientSecret: process.env[clientSecretEnv],
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function hasInfisical(config) {
|
|
23
|
+
const creds = getInfisicalCredentials(config);
|
|
24
|
+
return !!(creds.siteUrl && creds.projectId && creds.clientId && creds.clientSecret);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Optionally validate .env against .env.schema if envschema is installed.
|
|
29
|
+
*/
|
|
30
|
+
function validateWithEnvSchema(envFile, schemaFile) {
|
|
31
|
+
if (!existsSync(schemaFile)) return;
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
execSync('command -v envschema', { stdio: 'pipe' });
|
|
35
|
+
} catch {
|
|
36
|
+
console.log(chalk.dim(' hint: install envschema-dsl for .env validation'));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
execSync(`envschema validate --schema "${schemaFile}" --env "${envFile}"`, {
|
|
42
|
+
stdio: 'pipe', encoding: 'utf-8',
|
|
43
|
+
});
|
|
44
|
+
console.log(chalk.green(' .env.schema validation passed'));
|
|
45
|
+
} catch (e) {
|
|
46
|
+
console.log(chalk.yellow(` .env.schema validation warnings:\n${e.stdout || e.stderr || e.message}`));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// --- Commands ---
|
|
51
|
+
|
|
52
|
+
export async function status({ config, utils, projectRoot }) {
|
|
53
|
+
const subprojects = config.subprojects || {};
|
|
54
|
+
|
|
55
|
+
if (Object.keys(subprojects).length === 0) {
|
|
56
|
+
console.log(chalk.yellow('No subprojects configured. Add them to .devkit.d/env.yml'));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
for (const [name, cfg] of Object.entries(subprojects)) {
|
|
61
|
+
console.log(chalk.cyan.bold(`\n${name}`));
|
|
62
|
+
const basePath = resolve(projectRoot, cfg.path || '.');
|
|
63
|
+
console.log(` path: ${basePath}`);
|
|
64
|
+
|
|
65
|
+
if (!existsSync(basePath)) {
|
|
66
|
+
console.log(chalk.red(' directory not found'));
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
for (const variant of (cfg.variants || [])) {
|
|
71
|
+
const configDir = resolve(basePath, cfg.config_dir || 'config');
|
|
72
|
+
const exampleFile = resolve(configDir, `.env.${variant}.example`);
|
|
73
|
+
const envFile = resolve(configDir, `.env.${variant}`);
|
|
74
|
+
|
|
75
|
+
const parts = [];
|
|
76
|
+
parts.push(existsSync(exampleFile) ? chalk.green('example') : chalk.yellow('no example'));
|
|
77
|
+
parts.push(existsSync(envFile) ? chalk.green('.env') : chalk.red('no .env'));
|
|
78
|
+
|
|
79
|
+
console.log(` ${variant}: ${parts.join(' | ')}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function init({ config, utils, projectRoot, envName = 'dev', force = false }) {
|
|
85
|
+
const subprojects = config.subprojects || {};
|
|
86
|
+
|
|
87
|
+
if (Object.keys(subprojects).length === 0) {
|
|
88
|
+
console.log(chalk.yellow('No subprojects configured. Add them to .devkit.d/env.yml'));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Root .env (devkit secrets from Infisical path /)
|
|
93
|
+
if (hasInfisical(config)) {
|
|
94
|
+
const exampleFile = resolve(projectRoot, '.env.example');
|
|
95
|
+
const envFile = resolve(projectRoot, '.env');
|
|
96
|
+
|
|
97
|
+
console.log(chalk.cyan.bold('\ndevkit (root)'));
|
|
98
|
+
|
|
99
|
+
if (existsSync(exampleFile)) {
|
|
100
|
+
if (!existsSync(envFile) || force) {
|
|
101
|
+
copyFileSync(exampleFile, envFile);
|
|
102
|
+
const action = force && existsSync(envFile) ? 'overwritten' : 'created';
|
|
103
|
+
console.log(` .env ${action} from .env.example`);
|
|
104
|
+
} else {
|
|
105
|
+
console.log(` .env exists (skip)`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (existsSync(envFile)) {
|
|
110
|
+
try {
|
|
111
|
+
const creds = getInfisicalCredentials(config);
|
|
112
|
+
const secrets = await utils.fetchInfisicalSecrets({
|
|
113
|
+
...creds, envSlug: envName, secretPath: '/',
|
|
114
|
+
});
|
|
115
|
+
if (secrets.length > 0) {
|
|
116
|
+
const secretsMap = {};
|
|
117
|
+
for (const s of secrets) secretsMap[s.secretKey] = s.secretValue;
|
|
118
|
+
const envContent = readFileSync(envFile, 'utf-8');
|
|
119
|
+
const { content, updated } = utils.mergeSecretsIntoEnv(envContent, secretsMap);
|
|
120
|
+
writeFileSync(envFile, content);
|
|
121
|
+
console.log(chalk.green(` +${updated} secrets from Infisical (/)`));
|
|
122
|
+
}
|
|
123
|
+
} catch (e) {
|
|
124
|
+
console.log(chalk.red(` Infisical error: ${e.message}`));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
for (const [name, cfg] of Object.entries(subprojects)) {
|
|
130
|
+
const basePath = resolve(projectRoot, cfg.path || '.');
|
|
131
|
+
if (!existsSync(basePath)) {
|
|
132
|
+
console.log(chalk.red(` ${name}: directory not found (${basePath})`));
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
console.log(chalk.cyan.bold(`\n${name}`));
|
|
137
|
+
|
|
138
|
+
for (const variant of (cfg.variants || [])) {
|
|
139
|
+
const configDir = resolve(basePath, cfg.config_dir || 'config');
|
|
140
|
+
const exampleFile = resolve(configDir, `.env.${variant}.example`);
|
|
141
|
+
const envFile = resolve(configDir, `.env.${variant}`);
|
|
142
|
+
|
|
143
|
+
// Step 1: copy example → .env
|
|
144
|
+
if (existsSync(exampleFile)) {
|
|
145
|
+
if (!existsSync(envFile) || force) {
|
|
146
|
+
copyFileSync(exampleFile, envFile);
|
|
147
|
+
const action = force && existsSync(envFile) ? 'overwritten' : 'created';
|
|
148
|
+
console.log(` ${variant}: .env.${variant} ${action} from example`);
|
|
149
|
+
} else {
|
|
150
|
+
console.log(` ${variant}: .env.${variant} exists (skip)`);
|
|
151
|
+
}
|
|
152
|
+
} else {
|
|
153
|
+
console.log(chalk.yellow(` ${variant}: .env.${variant}.example not found`));
|
|
154
|
+
if (!existsSync(envFile)) continue;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Step 2: pull secrets from Infisical
|
|
158
|
+
if (hasInfisical(config)) {
|
|
159
|
+
try {
|
|
160
|
+
const infisicalPath = (cfg.infisical_path || '/{project}/{variant}')
|
|
161
|
+
.replace('{project}', name)
|
|
162
|
+
.replace('{variant}', variant);
|
|
163
|
+
const creds = getInfisicalCredentials(config);
|
|
164
|
+
const secrets = await utils.fetchInfisicalSecrets({
|
|
165
|
+
...creds, envSlug: envName, secretPath: infisicalPath,
|
|
166
|
+
});
|
|
167
|
+
if (secrets.length > 0) {
|
|
168
|
+
const secretsMap = {};
|
|
169
|
+
for (const s of secrets) secretsMap[s.secretKey] = s.secretValue;
|
|
170
|
+
if (secretsMap.DB_PASSWORD) {
|
|
171
|
+
secretsMap.POSTGRES_PASSWORD = secretsMap.DB_PASSWORD;
|
|
172
|
+
}
|
|
173
|
+
const envContent = readFileSync(envFile, 'utf-8');
|
|
174
|
+
const { content, updated } = utils.mergeSecretsIntoEnv(envContent, secretsMap);
|
|
175
|
+
writeFileSync(envFile, content);
|
|
176
|
+
console.log(chalk.green(` ${variant}: +${updated} secrets from Infisical (${infisicalPath})`));
|
|
177
|
+
} else {
|
|
178
|
+
console.log(` ${variant}: no secrets in Infisical (${infisicalPath})`);
|
|
179
|
+
}
|
|
180
|
+
} catch (e) {
|
|
181
|
+
console.log(chalk.red(` ${variant}: Infisical error: ${e.message}`));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Step 3: validate with env-schema
|
|
186
|
+
const schemaFile = resolve(configDir, '.env.schema');
|
|
187
|
+
validateWithEnvSchema(envFile, schemaFile);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
console.log(chalk.green('\nDone!'));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export async function pullSecrets({ config, utils, projectRoot, envName = 'dev' }) {
|
|
195
|
+
const subprojects = config.subprojects || {};
|
|
196
|
+
|
|
197
|
+
if (!hasInfisical(config)) {
|
|
198
|
+
console.log(chalk.yellow('Infisical not configured. Add credentials to .devkit.d/env.yml'));
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
for (const [name, cfg] of Object.entries(subprojects)) {
|
|
203
|
+
const basePath = resolve(projectRoot, cfg.path || '.');
|
|
204
|
+
if (!existsSync(basePath)) continue;
|
|
205
|
+
|
|
206
|
+
console.log(chalk.cyan.bold(`\n${name}`));
|
|
207
|
+
|
|
208
|
+
for (const variant of (cfg.variants || [])) {
|
|
209
|
+
const configDir = resolve(basePath, cfg.config_dir || 'config');
|
|
210
|
+
const envFile = resolve(configDir, `.env.${variant}`);
|
|
211
|
+
|
|
212
|
+
if (!existsSync(envFile)) {
|
|
213
|
+
console.log(chalk.yellow(` ${variant}: .env.${variant} not found (run env init)`));
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
const infisicalPath = (cfg.infisical_path || '/{project}/{variant}')
|
|
219
|
+
.replace('{project}', name)
|
|
220
|
+
.replace('{variant}', variant);
|
|
221
|
+
const creds = getInfisicalCredentials(config);
|
|
222
|
+
const secrets = await utils.fetchInfisicalSecrets({
|
|
223
|
+
...creds, envSlug: envName, secretPath: infisicalPath,
|
|
224
|
+
});
|
|
225
|
+
if (secrets.length > 0) {
|
|
226
|
+
const secretsMap = {};
|
|
227
|
+
for (const s of secrets) secretsMap[s.secretKey] = s.secretValue;
|
|
228
|
+
if (secretsMap.DB_PASSWORD) {
|
|
229
|
+
secretsMap.POSTGRES_PASSWORD = secretsMap.DB_PASSWORD;
|
|
230
|
+
}
|
|
231
|
+
const envContent = readFileSync(envFile, 'utf-8');
|
|
232
|
+
const { content, updated } = utils.mergeSecretsIntoEnv(envContent, secretsMap);
|
|
233
|
+
writeFileSync(envFile, content);
|
|
234
|
+
console.log(chalk.green(` ${variant}: updated ${updated} secrets`));
|
|
235
|
+
} else {
|
|
236
|
+
console.log(` ${variant}: no secrets in ${infisicalPath}`);
|
|
237
|
+
}
|
|
238
|
+
} catch (e) {
|
|
239
|
+
console.log(chalk.red(` ${variant}: error: ${e.message}`));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Validate
|
|
243
|
+
const schemaFile = resolve(configDir, '.env.schema');
|
|
244
|
+
validateWithEnvSchema(envFile, schemaFile);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
console.log(chalk.green('\nDone!'));
|
|
249
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import * as env from './env.mjs';
|
|
2
|
+
import * as infisical from './infisical.mjs';
|
|
3
|
+
|
|
4
|
+
function handleError(e) {
|
|
5
|
+
console.error(`Error: ${e.message}`);
|
|
6
|
+
process.exit(1);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Register env plugin commands.
|
|
11
|
+
* @param {import('commander').Command} program
|
|
12
|
+
* @param {object} context
|
|
13
|
+
*/
|
|
14
|
+
export function register(program, { config, utils, projectRoot, devkitRoot }) {
|
|
15
|
+
const envCmd = program
|
|
16
|
+
.command('env')
|
|
17
|
+
.description(config.description || 'Manage .env files');
|
|
18
|
+
|
|
19
|
+
envCmd
|
|
20
|
+
.command('status')
|
|
21
|
+
.description('Show .env file status')
|
|
22
|
+
.action(() => env.status({ config, utils, projectRoot }).catch(handleError));
|
|
23
|
+
|
|
24
|
+
envCmd
|
|
25
|
+
.command('init')
|
|
26
|
+
.description('Initialize .env from examples + Infisical secrets')
|
|
27
|
+
.option('--env-name <env>', 'Infisical environment', 'dev')
|
|
28
|
+
.option('--force', 'Overwrite existing .env')
|
|
29
|
+
.action(opts => env.init({
|
|
30
|
+
config, utils, projectRoot,
|
|
31
|
+
envName: opts.envName, force: opts.force,
|
|
32
|
+
}).catch(handleError));
|
|
33
|
+
|
|
34
|
+
envCmd
|
|
35
|
+
.command('pull-secrets')
|
|
36
|
+
.description('Update only secrets in .env files')
|
|
37
|
+
.option('--env-name <env>', 'Infisical environment', 'dev')
|
|
38
|
+
.action(opts => env.pullSecrets({
|
|
39
|
+
config, utils, projectRoot,
|
|
40
|
+
envName: opts.envName,
|
|
41
|
+
}).catch(handleError));
|
|
42
|
+
|
|
43
|
+
// --- infisical subcommand ---
|
|
44
|
+
|
|
45
|
+
const infisicalCmd = envCmd
|
|
46
|
+
.command('infisical')
|
|
47
|
+
.description('Manage Infisical secrets');
|
|
48
|
+
|
|
49
|
+
infisicalCmd
|
|
50
|
+
.command('list')
|
|
51
|
+
.description('List secrets')
|
|
52
|
+
.option('--env <env>', 'Environment', 'dev')
|
|
53
|
+
.option('--path <path>', 'Secret path', '/')
|
|
54
|
+
.action(opts => infisical.list({
|
|
55
|
+
config, utils, env: opts.env, path: opts.path,
|
|
56
|
+
}).catch(handleError));
|
|
57
|
+
|
|
58
|
+
infisicalCmd
|
|
59
|
+
.command('get <name>')
|
|
60
|
+
.description('Get secret value')
|
|
61
|
+
.option('--env <env>', 'Environment', 'dev')
|
|
62
|
+
.option('--path <path>', 'Secret path', '/')
|
|
63
|
+
.action((name, opts) => infisical.get({
|
|
64
|
+
config, utils, name, env: opts.env, path: opts.path,
|
|
65
|
+
}).catch(handleError));
|
|
66
|
+
|
|
67
|
+
infisicalCmd
|
|
68
|
+
.command('set <keyValue>')
|
|
69
|
+
.description('Set secret (KEY=VALUE)')
|
|
70
|
+
.option('--env <env>', 'Environment', 'dev')
|
|
71
|
+
.option('--path <path>', 'Secret path', '/')
|
|
72
|
+
.action((kv, opts) => infisical.set({
|
|
73
|
+
config, utils, keyValue: kv, env: opts.env, path: opts.path,
|
|
74
|
+
}).catch(handleError));
|
|
75
|
+
|
|
76
|
+
infisicalCmd
|
|
77
|
+
.command('delete <name>')
|
|
78
|
+
.description('Delete secret')
|
|
79
|
+
.option('--env <env>', 'Environment', 'dev')
|
|
80
|
+
.option('--path <path>', 'Secret path', '/')
|
|
81
|
+
.action((name, opts) => infisical.del({
|
|
82
|
+
config, utils, name, env: opts.env, path: opts.path,
|
|
83
|
+
}).catch(handleError));
|
|
84
|
+
|
|
85
|
+
infisicalCmd
|
|
86
|
+
.command('deploy')
|
|
87
|
+
.description('Deploy secrets to server')
|
|
88
|
+
.requiredOption('--env <env>', 'Environment (test or stage)')
|
|
89
|
+
.option('--yes, -y', 'Skip confirmation')
|
|
90
|
+
.action(opts => infisical.deploy({
|
|
91
|
+
config, utils, env: opts.env, yes: opts.yes,
|
|
92
|
+
}).catch(handleError));
|
|
93
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { execSync } from 'child_process';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Get Infisical credentials from config + environment variables.
|
|
6
|
+
*/
|
|
7
|
+
function getInfisicalCredentials(config) {
|
|
8
|
+
const infisical = config.infisical || {};
|
|
9
|
+
const clientIdEnv = infisical.client_id_env || 'INFISICAL_UNIVERSAL_AUTH_CLIENT_ID';
|
|
10
|
+
const clientSecretEnv = infisical.client_secret_env || 'INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET';
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
siteUrl: infisical.site_url,
|
|
14
|
+
projectId: infisical.project_id,
|
|
15
|
+
clientId: process.env[clientIdEnv],
|
|
16
|
+
clientSecret: process.env[clientSecretEnv],
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function list({ config, utils, env = 'dev', path = '/' }) {
|
|
21
|
+
const creds = getInfisicalCredentials(config);
|
|
22
|
+
const secrets = await utils.fetchInfisicalSecrets({
|
|
23
|
+
...creds, envSlug: env, secretPath: path,
|
|
24
|
+
});
|
|
25
|
+
if (secrets.length === 0) { console.log('No secrets.'); return; }
|
|
26
|
+
|
|
27
|
+
for (const s of secrets) {
|
|
28
|
+
console.log(`${s.secretKey}=${s.secretValue}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function get({ config, utils, name, env = 'dev', path = '/' }) {
|
|
33
|
+
const creds = getInfisicalCredentials(config);
|
|
34
|
+
const client = await utils.getInfisicalClient(creds);
|
|
35
|
+
const secret = await client.getSecret({
|
|
36
|
+
projectId: creds.projectId,
|
|
37
|
+
environment: env,
|
|
38
|
+
path,
|
|
39
|
+
secretName: name,
|
|
40
|
+
});
|
|
41
|
+
console.log(secret.secretValue);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function set({ config, utils, keyValue, env = 'dev', path = '/' }) {
|
|
45
|
+
const idx = keyValue.indexOf('=');
|
|
46
|
+
if (idx === -1) throw new Error('Format: KEY=VALUE');
|
|
47
|
+
|
|
48
|
+
const key = keyValue.slice(0, idx);
|
|
49
|
+
const value = keyValue.slice(idx + 1);
|
|
50
|
+
|
|
51
|
+
const creds = getInfisicalCredentials(config);
|
|
52
|
+
const client = await utils.getInfisicalClient(creds);
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
await client.updateSecret({
|
|
56
|
+
projectId: creds.projectId,
|
|
57
|
+
environment: env,
|
|
58
|
+
path,
|
|
59
|
+
secretName: key,
|
|
60
|
+
secretValue: value,
|
|
61
|
+
});
|
|
62
|
+
} catch {
|
|
63
|
+
await client.createSecret({
|
|
64
|
+
projectId: creds.projectId,
|
|
65
|
+
environment: env,
|
|
66
|
+
path,
|
|
67
|
+
secretName: key,
|
|
68
|
+
secretValue: value,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
console.log(chalk.green(`\u2713 ${key} updated`));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function del({ config, utils, name, env = 'dev', path = '/' }) {
|
|
76
|
+
const creds = getInfisicalCredentials(config);
|
|
77
|
+
const client = await utils.getInfisicalClient(creds);
|
|
78
|
+
await client.deleteSecret({
|
|
79
|
+
projectId: creds.projectId,
|
|
80
|
+
environment: env,
|
|
81
|
+
path,
|
|
82
|
+
secretName: name,
|
|
83
|
+
});
|
|
84
|
+
console.log(chalk.green(`\u2713 ${name} deleted`));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function deploy({ config, utils, env, yes = false }) {
|
|
88
|
+
if (!env) throw new Error('Specify --env (test or stage)');
|
|
89
|
+
|
|
90
|
+
const servers = config.servers || {};
|
|
91
|
+
const server = servers[env];
|
|
92
|
+
if (!server) throw new Error(`No server for environment '${env}' (available: ${Object.keys(servers).join(', ')})`);
|
|
93
|
+
|
|
94
|
+
if (server.confirm && !yes) {
|
|
95
|
+
const readline = await import('readline');
|
|
96
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
97
|
+
const answer = await new Promise(resolve => {
|
|
98
|
+
rl.question(chalk.yellow(`Deploy to ${env} (${server.ssh}). Continue? (y/N) `), resolve);
|
|
99
|
+
});
|
|
100
|
+
rl.close();
|
|
101
|
+
if (answer.toLowerCase() !== 'y') { console.log('Cancelled'); return; }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
console.log(chalk.cyan(`\nDeploying secrets to ${env} (${server.ssh})...\n`));
|
|
105
|
+
|
|
106
|
+
const containers = config.containers || {};
|
|
107
|
+
const creds = getInfisicalCredentials(config);
|
|
108
|
+
|
|
109
|
+
for (const [project, variants] of Object.entries(containers)) {
|
|
110
|
+
for (const [variant, container] of Object.entries(variants)) {
|
|
111
|
+
const secretPath = `/${project}/${variant}`;
|
|
112
|
+
const remotePath = `${server.projects_path}/${project}/config/.env.${variant}`;
|
|
113
|
+
|
|
114
|
+
process.stdout.write(` ${project}/${variant}: `);
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const secrets = await utils.fetchInfisicalSecrets({
|
|
118
|
+
...creds, envSlug: env, secretPath,
|
|
119
|
+
});
|
|
120
|
+
if (secrets.length === 0) {
|
|
121
|
+
console.log(chalk.dim('no secrets — skip'));
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const secretsMap = {};
|
|
126
|
+
for (const s of secrets) secretsMap[s.secretKey] = s.secretValue;
|
|
127
|
+
|
|
128
|
+
const currentEnv = utils.sshReadFile(server.ssh, remotePath);
|
|
129
|
+
if (!currentEnv) {
|
|
130
|
+
console.log(chalk.yellow('file not found — skip'));
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const { content, updated } = utils.mergeSecretsIntoEnv(currentEnv, secretsMap);
|
|
135
|
+
utils.sshWriteFile(server.ssh, remotePath, content);
|
|
136
|
+
|
|
137
|
+
let restartOk = false;
|
|
138
|
+
try {
|
|
139
|
+
execSync(`ssh ${server.ssh} 'docker restart ${container}'`, { encoding: 'utf-8', timeout: 30000 });
|
|
140
|
+
restartOk = true;
|
|
141
|
+
} catch {}
|
|
142
|
+
|
|
143
|
+
const status = restartOk ? chalk.green(container) : chalk.red(`${container} not restarted`);
|
|
144
|
+
console.log(chalk.green(`${updated} secrets`) + chalk.dim(' \u2192 ') + status);
|
|
145
|
+
} catch (e) {
|
|
146
|
+
console.log(chalk.red(`error: ${e.message}`));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
console.log(chalk.green('\n\u2713 Deploy complete'));
|
|
152
|
+
}
|