@swovohq/fuel 0.2.2-alpha.87 → 0.2.2-alpha.88
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/dist/bin/fuel.js +10 -31
- package/dist/bin/fuel.js.map +1 -1
- package/dist/commands/config-verify.d.ts.map +1 -1
- package/dist/commands/config-verify.js +14 -6
- package/dist/commands/config-verify.js.map +1 -1
- package/dist/commands/create-app.d.ts.map +1 -1
- package/dist/commands/create-app.js +1 -0
- package/dist/commands/create-app.js.map +1 -1
- package/dist/commands/infra-deploy.d.ts +4 -0
- package/dist/commands/infra-deploy.d.ts.map +1 -1
- package/dist/commands/infra-deploy.js +72 -43
- package/dist/commands/infra-deploy.js.map +1 -1
- package/dist/commands/infra-destroy.js +3 -3
- package/dist/commands/infra-destroy.js.map +1 -1
- package/dist/commands/infra-init.d.ts +91 -0
- package/dist/commands/infra-init.d.ts.map +1 -1
- package/dist/commands/infra-init.js +451 -78
- package/dist/commands/infra-init.js.map +1 -1
- package/dist/commands/infra-pull.d.ts.map +1 -1
- package/dist/commands/infra-pull.js +6 -5
- package/dist/commands/infra-pull.js.map +1 -1
- package/dist/infra/azure-defaults.d.ts +49 -0
- package/dist/infra/azure-defaults.d.ts.map +1 -0
- package/dist/infra/azure-defaults.js +52 -0
- package/dist/infra/azure-defaults.js.map +1 -0
- package/dist/infra/azure-keyvault-client.d.ts +4 -0
- package/dist/infra/azure-keyvault-client.d.ts.map +1 -0
- package/dist/infra/azure-keyvault-client.js +57 -0
- package/dist/infra/azure-keyvault-client.js.map +1 -0
- package/dist/infra/azure-rbac.d.ts +11 -0
- package/dist/infra/azure-rbac.d.ts.map +1 -0
- package/dist/infra/azure-rbac.js +47 -0
- package/dist/infra/azure-rbac.js.map +1 -0
- package/dist/infra/azure-state-storage.d.ts +20 -0
- package/dist/infra/azure-state-storage.d.ts.map +1 -0
- package/dist/infra/azure-state-storage.js +229 -0
- package/dist/infra/azure-state-storage.js.map +1 -0
- package/dist/infra/credentials.d.ts +2 -2
- package/dist/infra/credentials.d.ts.map +1 -1
- package/dist/infra/credentials.js +3 -2
- package/dist/infra/credentials.js.map +1 -1
- package/dist/infra/deployment-detect.d.ts +9 -0
- package/dist/infra/deployment-detect.d.ts.map +1 -0
- package/dist/infra/deployment-detect.js +53 -0
- package/dist/infra/deployment-detect.js.map +1 -0
- package/dist/infra/github-client.d.ts.map +1 -1
- package/dist/infra/github-client.js +1 -4
- package/dist/infra/github-client.js.map +1 -1
- package/dist/infra/orchestrator.d.ts.map +1 -1
- package/dist/infra/orchestrator.js +193 -65
- package/dist/infra/orchestrator.js.map +1 -1
- package/dist/infra/preflight.js +11 -11
- package/dist/infra/preflight.js.map +1 -1
- package/dist/infra/remote-secret.d.ts +14 -0
- package/dist/infra/remote-secret.d.ts.map +1 -0
- package/dist/infra/remote-secret.js +42 -0
- package/dist/infra/remote-secret.js.map +1 -0
- package/dist/infra/template-engine.d.ts +13 -0
- package/dist/infra/template-engine.d.ts.map +1 -1
- package/dist/infra/template-engine.js +106 -38
- package/dist/infra/template-engine.js.map +1 -1
- package/dist/infra/tofu-runner.d.ts +4 -3
- package/dist/infra/tofu-runner.d.ts.map +1 -1
- package/dist/infra/tofu-runner.js +36 -27
- package/dist/infra/tofu-runner.js.map +1 -1
- package/dist/infra/types.d.ts +239 -16
- package/dist/infra/types.d.ts.map +1 -1
- package/dist/infra/types.js +153 -17
- package/dist/infra/types.js.map +1 -1
- package/package.json +10 -2
- package/schema/fuel.schema.json +528 -83
|
@@ -36,8 +36,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
36
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
exports.CONFIG_TEMPLATE = void 0;
|
|
39
|
+
exports.CONFIG_TEMPLATE_AZURE = exports.CONFIG_TEMPLATE = void 0;
|
|
40
40
|
exports.applyInfraInitAnswers = applyInfraInitAnswers;
|
|
41
|
+
exports.applyAzureInfraInitAnswers = applyAzureInfraInitAnswers;
|
|
41
42
|
exports.infraInit = infraInit;
|
|
42
43
|
const fs = __importStar(require("fs-extra"));
|
|
43
44
|
const path = __importStar(require("path"));
|
|
@@ -47,6 +48,9 @@ const promises_1 = require("readline/promises");
|
|
|
47
48
|
const child_process_1 = require("child_process");
|
|
48
49
|
const util_1 = require("util");
|
|
49
50
|
const https = __importStar(require("https"));
|
|
51
|
+
const identity_1 = require("@azure/identity");
|
|
52
|
+
const arm_resources_1 = require("@azure/arm-resources");
|
|
53
|
+
const azure_defaults_1 = require("../infra/azure-defaults");
|
|
50
54
|
const execAsync = (0, util_1.promisify)(child_process_1.exec);
|
|
51
55
|
function httpsGetJson(url, headers) {
|
|
52
56
|
return new Promise((resolve, reject) => {
|
|
@@ -98,6 +102,44 @@ exports.CONFIG_TEMPLATE = {
|
|
|
98
102
|
},
|
|
99
103
|
],
|
|
100
104
|
};
|
|
105
|
+
exports.CONFIG_TEMPLATE_AZURE = {
|
|
106
|
+
name: 'my-app',
|
|
107
|
+
deployment: 'azure',
|
|
108
|
+
slack_webhook_url: '',
|
|
109
|
+
location: 'eastus2',
|
|
110
|
+
monitor: { daily_data_cap_gb: 1, retention_days: 90, dashboard_count: 0 },
|
|
111
|
+
front_door: { sku_name: 'Standard_AzureFrontDoor' },
|
|
112
|
+
storage_account: { account_kind: 'BlockBlobStorage', account_tier: 'Premium', account_replication_type: 'LRS', access_tier: 'Hot' },
|
|
113
|
+
key_vault: { sku_name: 'standard' },
|
|
114
|
+
apps: [
|
|
115
|
+
{
|
|
116
|
+
name: 'my-app-api',
|
|
117
|
+
technology: 'nest',
|
|
118
|
+
dockerfile: 'apps/api/Dockerfile',
|
|
119
|
+
health_check_path: '/health',
|
|
120
|
+
domain: 'api.example.com',
|
|
121
|
+
port: 3000,
|
|
122
|
+
branch: 'main',
|
|
123
|
+
database: { enabled: true, engine: 'mssql', sku_name: 'GP_S_Gen5_4' },
|
|
124
|
+
redis: { enabled: false, sku_name: 'Balanced_B0' },
|
|
125
|
+
container_app: { cpu: 1, memory: '2Gi', concurrent_requests: 20, min_replicas: 0, max_replicas: 3 },
|
|
126
|
+
variables: {},
|
|
127
|
+
workers: [],
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
name: 'my-app-web',
|
|
131
|
+
technology: 'react',
|
|
132
|
+
dockerfile: 'apps/web/Dockerfile',
|
|
133
|
+
health_check_path: '/health',
|
|
134
|
+
domain: 'app.example.com',
|
|
135
|
+
port: 3000,
|
|
136
|
+
branch: 'main',
|
|
137
|
+
build_env: {
|
|
138
|
+
NEXT_PUBLIC_API_HOST: 'https://api.example.com',
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
],
|
|
142
|
+
};
|
|
101
143
|
// --- Private prompt helpers ---
|
|
102
144
|
async function ask(rl, label, defaultVal) {
|
|
103
145
|
const hint = defaultVal ? ` [${defaultVal}]` : '';
|
|
@@ -134,6 +176,14 @@ async function askSelect(rl, label, options, defaultIdx = 1) {
|
|
|
134
176
|
console.log(` Please enter a number between 1 and ${options.length}.`);
|
|
135
177
|
}
|
|
136
178
|
}
|
|
179
|
+
/** Prompts which cloud provider to provision infrastructure for. */
|
|
180
|
+
async function askCloudChoice(rl) {
|
|
181
|
+
console.log(' Cloud provider');
|
|
182
|
+
console.log(' ────────────────────────────────────────────────────');
|
|
183
|
+
const choice = await askSelect(rl, 'Deploy to', ['AWS (ECS)', 'Azure (Container Apps)'], 1);
|
|
184
|
+
console.log('');
|
|
185
|
+
return choice === 'AWS (ECS)' ? 'aws' : 'azure';
|
|
186
|
+
}
|
|
137
187
|
/** Prompts for KEY=VALUE pairs until the user submits an empty line. */
|
|
138
188
|
async function askKeyValues(rl, label) {
|
|
139
189
|
console.log(`\n ${label} (KEY=VALUE format, empty line to finish):`);
|
|
@@ -236,6 +286,19 @@ async function tryGetAcmCertificate(arn, credentials) {
|
|
|
236
286
|
return { error: firstLine };
|
|
237
287
|
}
|
|
238
288
|
}
|
|
289
|
+
/** Verifies Azure credentials by acquiring a token and listing resource groups (cheap, works with just Reader access). */
|
|
290
|
+
async function tryGetAzureAccountInfo(credentials) {
|
|
291
|
+
try {
|
|
292
|
+
const credential = new identity_1.ClientSecretCredential(credentials.AZURE_TENANT_ID, credentials.AZURE_CLIENT_ID, credentials.AZURE_CLIENT_SECRET);
|
|
293
|
+
const client = new arm_resources_1.ResourceManagementClient(credential, credentials.AZURE_SUBSCRIPTION_ID);
|
|
294
|
+
const iterator = client.resourceGroups.list();
|
|
295
|
+
await iterator.next();
|
|
296
|
+
return { subscriptionId: credentials.AZURE_SUBSCRIPTION_ID };
|
|
297
|
+
}
|
|
298
|
+
catch (err) {
|
|
299
|
+
return { error: err instanceof Error ? err.message : String(err) };
|
|
300
|
+
}
|
|
301
|
+
}
|
|
239
302
|
/** Displays credential verification results and throws if any check failed. */
|
|
240
303
|
function assertCredentialResults(stsResult, ghUserResult, ghOrgResult) {
|
|
241
304
|
const failures = [];
|
|
@@ -250,6 +313,20 @@ function assertCredentialResults(stsResult, ghUserResult, ghOrgResult) {
|
|
|
250
313
|
failures.map((f) => ` • ${f}`).join('\n'));
|
|
251
314
|
}
|
|
252
315
|
}
|
|
316
|
+
/** Displays Azure credential verification results and throws if any check failed. */
|
|
317
|
+
function assertAzureCredentialResults(azResult, ghUserResult, ghOrgResult) {
|
|
318
|
+
const failures = [];
|
|
319
|
+
if ('error' in azResult)
|
|
320
|
+
failures.push(`Azure credentials: ${azResult.error}`);
|
|
321
|
+
if ('error' in ghUserResult)
|
|
322
|
+
failures.push(`GitHub token: ${ghUserResult.error}`);
|
|
323
|
+
if ('error' in ghOrgResult)
|
|
324
|
+
failures.push(`GitHub org: ${ghOrgResult.error}`);
|
|
325
|
+
if (failures.length > 0) {
|
|
326
|
+
throw new Error('Credential verification failed:\n' +
|
|
327
|
+
failures.map((f) => ` • ${f}`).join('\n'));
|
|
328
|
+
}
|
|
329
|
+
}
|
|
253
330
|
/** Derives a sensible domain default: cliswovotest-api.swovo.com (production), cliswovotest-api-dev.swovo.com (others) */
|
|
254
331
|
function defaultDomain(projectName, suffix, env, baseDomain) {
|
|
255
332
|
const label = suffix === 'web' ? 'app' : suffix;
|
|
@@ -462,6 +539,177 @@ async function collectAnswers(rl) {
|
|
|
462
539
|
console.log('');
|
|
463
540
|
return { credentials, projectName, ssl, slackWebhookUrl, appTypes, environments, baseDomain, domains };
|
|
464
541
|
}
|
|
542
|
+
// --- collectAzureAnswers ---
|
|
543
|
+
async function collectAzureAnswers(rl) {
|
|
544
|
+
// --- Credentials ---
|
|
545
|
+
console.log(' Credentials');
|
|
546
|
+
console.log(' ────────────────────────────────────────────────────');
|
|
547
|
+
let credentials = {};
|
|
548
|
+
let useExistingCreds = false;
|
|
549
|
+
const credFilePath = path.resolve('.fuel-credentials');
|
|
550
|
+
if (await fs.pathExists(credFilePath)) {
|
|
551
|
+
try {
|
|
552
|
+
const existing = await fs.readJson(credFilePath);
|
|
553
|
+
console.log('\n Found existing .fuel-credentials');
|
|
554
|
+
const [azResult, ghUserResult, ghOrgResult] = await Promise.all([
|
|
555
|
+
tryGetAzureAccountInfo(existing),
|
|
556
|
+
tryGetGithubUser(existing),
|
|
557
|
+
tryGetGithubOrg(existing),
|
|
558
|
+
]);
|
|
559
|
+
if ('subscriptionId' in azResult) {
|
|
560
|
+
console.log(` Azure Subscription: ${chalk_1.default.green(azResult.subscriptionId)}`);
|
|
561
|
+
}
|
|
562
|
+
else {
|
|
563
|
+
console.log(' ' + chalk_1.default.yellow('⚠') + ' Azure credentials invalid:');
|
|
564
|
+
console.log(` ${azResult.error}`);
|
|
565
|
+
}
|
|
566
|
+
if ('login' in ghUserResult) {
|
|
567
|
+
console.log(` GitHub User: ${chalk_1.default.green(ghUserResult.login)}`);
|
|
568
|
+
}
|
|
569
|
+
else {
|
|
570
|
+
console.log(' ' + chalk_1.default.yellow('⚠') + ' GitHub token invalid:');
|
|
571
|
+
console.log(` ${ghUserResult.error}`);
|
|
572
|
+
}
|
|
573
|
+
if ('id' in ghOrgResult) {
|
|
574
|
+
console.log(` GitHub Org: ${chalk_1.default.green(ghOrgResult.login)} (ID: ${ghOrgResult.id})`);
|
|
575
|
+
}
|
|
576
|
+
else {
|
|
577
|
+
console.log(' ' + chalk_1.default.yellow('⚠') + ' Could not verify org:');
|
|
578
|
+
console.log(` ${ghOrgResult.error}`);
|
|
579
|
+
}
|
|
580
|
+
assertAzureCredentialResults(azResult, ghUserResult, ghOrgResult);
|
|
581
|
+
console.log('');
|
|
582
|
+
useExistingCreds = await askYesNo(rl, 'Use these credentials?', true);
|
|
583
|
+
if (useExistingCreds)
|
|
584
|
+
credentials = existing;
|
|
585
|
+
}
|
|
586
|
+
catch (e) {
|
|
587
|
+
if (e instanceof Error && e.message.startsWith('Credential verification failed'))
|
|
588
|
+
throw e;
|
|
589
|
+
console.log(' Could not read .fuel-credentials — prompting for credentials.');
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
if (!useExistingCreds) {
|
|
593
|
+
credentials.AZURE_SUBSCRIPTION_ID = await ask(rl, 'AZURE_SUBSCRIPTION_ID');
|
|
594
|
+
credentials.AZURE_TENANT_ID = await ask(rl, 'AZURE_TENANT_ID');
|
|
595
|
+
credentials.AZURE_CLIENT_ID = await ask(rl, 'AZURE_CLIENT_ID');
|
|
596
|
+
credentials.AZURE_CLIENT_SECRET = await ask(rl, 'AZURE_CLIENT_SECRET');
|
|
597
|
+
credentials.GITHUB_TOKEN = await ask(rl, 'GITHUB_TOKEN');
|
|
598
|
+
credentials.GITHUB_USERNAME = await ask(rl, 'GITHUB_USERNAME');
|
|
599
|
+
credentials.GITHUB_ORGANIZATION = await ask(rl, 'GITHUB_ORGANIZATION');
|
|
600
|
+
console.log('\n Verifying credentials...');
|
|
601
|
+
const [azResult, ghUserResult, ghOrgResult] = await Promise.all([
|
|
602
|
+
tryGetAzureAccountInfo(credentials),
|
|
603
|
+
tryGetGithubUser(credentials),
|
|
604
|
+
tryGetGithubOrg(credentials),
|
|
605
|
+
]);
|
|
606
|
+
if ('subscriptionId' in azResult) {
|
|
607
|
+
console.log(` Azure Subscription: ${chalk_1.default.green(azResult.subscriptionId)}`);
|
|
608
|
+
}
|
|
609
|
+
else {
|
|
610
|
+
console.log(' ' + chalk_1.default.yellow('⚠') + ' Azure credentials invalid:');
|
|
611
|
+
console.log(` ${azResult.error}`);
|
|
612
|
+
}
|
|
613
|
+
if ('login' in ghUserResult) {
|
|
614
|
+
console.log(` GitHub User: ${chalk_1.default.green(ghUserResult.login)}`);
|
|
615
|
+
}
|
|
616
|
+
else {
|
|
617
|
+
console.log(' ' + chalk_1.default.yellow('⚠') + ' GitHub token invalid:');
|
|
618
|
+
console.log(` ${ghUserResult.error}`);
|
|
619
|
+
}
|
|
620
|
+
if ('id' in ghOrgResult) {
|
|
621
|
+
console.log(` GitHub Org: ${chalk_1.default.green(ghOrgResult.login)} (ID: ${ghOrgResult.id})`);
|
|
622
|
+
}
|
|
623
|
+
else {
|
|
624
|
+
console.log(' ' + chalk_1.default.yellow('⚠') + ' Could not verify org:');
|
|
625
|
+
console.log(` ${ghOrgResult.error}`);
|
|
626
|
+
}
|
|
627
|
+
assertAzureCredentialResults(azResult, ghUserResult, ghOrgResult);
|
|
628
|
+
}
|
|
629
|
+
console.log('');
|
|
630
|
+
// --- Project ---
|
|
631
|
+
console.log(' Project');
|
|
632
|
+
console.log(' ────────────────────────────────────────────────────');
|
|
633
|
+
const projectName = await ask(rl, 'Project name', 'my-app');
|
|
634
|
+
const location = await ask(rl, 'Azure region', 'eastus2');
|
|
635
|
+
const slackWebhookUrl = await ask(rl, 'Slack webhook URL for deploy/alarm notifications (leave empty to skip)', '');
|
|
636
|
+
console.log('');
|
|
637
|
+
// --- App configuration ---
|
|
638
|
+
console.log(' App configuration');
|
|
639
|
+
console.log(' ────────────────────────────────────────────────────');
|
|
640
|
+
const appChoice = await askSelect(rl, 'Include which apps?', ['API only (NestJS)', 'Web only (React/Next.js)', 'API + Web'], 3);
|
|
641
|
+
const includeApi = appChoice !== 'Web only (React/Next.js)';
|
|
642
|
+
const includeWeb = appChoice !== 'API only (NestJS)';
|
|
643
|
+
const appTypes = [];
|
|
644
|
+
if (includeApi) {
|
|
645
|
+
console.log('\n API (NestJS):');
|
|
646
|
+
const dockerfile = await ask(rl, 'Dockerfile', 'apps/api/Dockerfile');
|
|
647
|
+
const port = await askInt(rl, 'Port', 3000);
|
|
648
|
+
const healthCheckPath = await ask(rl, 'Health check path', '/health');
|
|
649
|
+
const databaseEnabled = await askYesNo(rl, 'Include a database?', true);
|
|
650
|
+
let databaseEngine = 'mssql';
|
|
651
|
+
if (databaseEnabled) {
|
|
652
|
+
const engineChoice = await askSelect(rl, ' Database engine', ['Azure SQL Database (mssql)', 'PostgreSQL Flexible Server (postgres)'], 1);
|
|
653
|
+
databaseEngine = engineChoice.includes('mssql') ? 'mssql' : 'postgres';
|
|
654
|
+
}
|
|
655
|
+
const redisEnabled = await askYesNo(rl, 'Include Redis?', false);
|
|
656
|
+
appTypes.push({
|
|
657
|
+
suffix: 'api', dockerfile, port, healthCheckPath,
|
|
658
|
+
databaseEnabled, databaseEngine, redisEnabled, variables: {},
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
if (includeWeb) {
|
|
662
|
+
console.log('\n Web (React/Next.js):');
|
|
663
|
+
const dockerfile = await ask(rl, 'Dockerfile', 'apps/web/Dockerfile');
|
|
664
|
+
const port = await askInt(rl, 'Port', 3000);
|
|
665
|
+
const healthCheckPath = await ask(rl, 'Health check path', '/health');
|
|
666
|
+
appTypes.push({
|
|
667
|
+
suffix: 'web', dockerfile, port, healthCheckPath,
|
|
668
|
+
databaseEnabled: false, databaseEngine: 'mssql', redisEnabled: false, variables: {},
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
console.log('');
|
|
672
|
+
// --- Environments ---
|
|
673
|
+
console.log(' Environments');
|
|
674
|
+
console.log(' ────────────────────────────────────────────────────');
|
|
675
|
+
const envPresets = {
|
|
676
|
+
'production only': ['production'],
|
|
677
|
+
'dev + production': ['dev', 'production'],
|
|
678
|
+
'dev + staging + production': ['dev', 'staging', 'production'],
|
|
679
|
+
};
|
|
680
|
+
const envChoice = await askSelect(rl, 'Environments', Object.keys(envPresets), 3);
|
|
681
|
+
const environments = envPresets[envChoice];
|
|
682
|
+
console.log('');
|
|
683
|
+
// --- Base domain + per-app domains ---
|
|
684
|
+
console.log(' Domains');
|
|
685
|
+
console.log(' ────────────────────────────────────────────────────');
|
|
686
|
+
const baseDomain = await ask(rl, 'Base domain');
|
|
687
|
+
console.log('');
|
|
688
|
+
const domains = {};
|
|
689
|
+
for (const appType of appTypes) {
|
|
690
|
+
domains[appType.suffix] = {};
|
|
691
|
+
for (const env of environments) {
|
|
692
|
+
const suggestion = baseDomain ? defaultDomain(projectName, appType.suffix, env, baseDomain) : '';
|
|
693
|
+
const appFullName = `${projectName}-${appType.suffix}-${env}`;
|
|
694
|
+
domains[appType.suffix][env] = await ask(rl, appFullName, suggestion || undefined);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
console.log('');
|
|
698
|
+
// --- Environment variables (per app × per environment) ---
|
|
699
|
+
console.log(' Environment variables');
|
|
700
|
+
console.log(' ────────────────────────────────────────────────────');
|
|
701
|
+
for (const appType of appTypes) {
|
|
702
|
+
const isApi = appType.suffix === 'api';
|
|
703
|
+
for (const env of environments) {
|
|
704
|
+
const label = isApi
|
|
705
|
+
? `${projectName}-api-${env} runtime variables (e.g. TWILIO_TOKEN=xxx)`
|
|
706
|
+
: `${projectName}-web-${env} build variables (NEXT_PUBLIC_API_HOST and NEXT_PUBLIC_APP_HOST are already included)`;
|
|
707
|
+
appType.variables[env] = await askKeyValues(rl, label);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
console.log('');
|
|
711
|
+
return { credentials, projectName, slackWebhookUrl, location, appTypes, environments, baseDomain, domains };
|
|
712
|
+
}
|
|
465
713
|
// --- applyInfraInitAnswers ---
|
|
466
714
|
async function applyInfraInitAnswers(answers, opts) {
|
|
467
715
|
// 1. Write .fuel-credentials
|
|
@@ -555,96 +803,210 @@ async function applyInfraInitAnswers(answers, opts) {
|
|
|
555
803
|
console.log(' Review and edit fuel.yml to add workers/variables, then:');
|
|
556
804
|
console.log(` fuel create:app ${answers.projectName} --config fuel.yml\n`);
|
|
557
805
|
}
|
|
558
|
-
// ---
|
|
559
|
-
async function
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
credentials.GITHUB_ORGANIZATION = await ask(rl, 'GITHUB_ORGANIZATION');
|
|
605
|
-
console.log('\n Verifying credentials...');
|
|
606
|
-
const [stsResult, ghUserResult, ghOrgResult] = await Promise.all([
|
|
607
|
-
tryGetAwsAccountId(credentials),
|
|
608
|
-
tryGetGithubUser(credentials),
|
|
609
|
-
tryGetGithubOrg(credentials),
|
|
610
|
-
]);
|
|
611
|
-
if ('accountId' in stsResult) {
|
|
612
|
-
console.log(` AWS Account: ${chalk_1.default.green(stsResult.accountId)}`);
|
|
613
|
-
}
|
|
614
|
-
if ('login' in ghUserResult) {
|
|
615
|
-
console.log(` GitHub User: ${chalk_1.default.green(ghUserResult.login)}`);
|
|
806
|
+
// --- applyAzureInfraInitAnswers ---
|
|
807
|
+
async function applyAzureInfraInitAnswers(answers, opts) {
|
|
808
|
+
// 1. Write .fuel-credentials
|
|
809
|
+
const credPath = path.resolve('.fuel-credentials');
|
|
810
|
+
await fs.writeFile(credPath, JSON.stringify(answers.credentials, null, 2));
|
|
811
|
+
console.log(chalk_1.default.green('✓') + ' Credentials written to .fuel-credentials');
|
|
812
|
+
// 2. Ensure .gitignore contains .fuel-credentials
|
|
813
|
+
const gitignorePath = path.resolve('.gitignore');
|
|
814
|
+
const CRED_LINE = '.fuel-credentials';
|
|
815
|
+
if (await fs.pathExists(gitignorePath)) {
|
|
816
|
+
const content = await fs.readFile(gitignorePath, 'utf-8');
|
|
817
|
+
if (!content.split('\n').some((l) => l.trim() === CRED_LINE)) {
|
|
818
|
+
await fs.appendFile(gitignorePath, `\n${CRED_LINE}\n`);
|
|
819
|
+
console.log(chalk_1.default.green('✓') + ' .fuel-credentials added to .gitignore');
|
|
820
|
+
}
|
|
821
|
+
else {
|
|
822
|
+
console.log(' .fuel-credentials already in .gitignore');
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
else {
|
|
826
|
+
await fs.writeFile(gitignorePath, `${CRED_LINE}\n`);
|
|
827
|
+
console.log(chalk_1.default.green('✓') + ' .fuel-credentials added to .gitignore');
|
|
828
|
+
}
|
|
829
|
+
// 3. Build config (cartesian product: appTypes × environments)
|
|
830
|
+
const apps = [];
|
|
831
|
+
for (const appType of answers.appTypes) {
|
|
832
|
+
const isApi = appType.suffix === 'api';
|
|
833
|
+
const technology = isApi ? 'nest' : 'react';
|
|
834
|
+
for (const env of answers.environments) {
|
|
835
|
+
const branch = env === 'production' ? 'main' : env;
|
|
836
|
+
const app = {
|
|
837
|
+
name: `${answers.projectName}-${appType.suffix}-${env}`,
|
|
838
|
+
technology,
|
|
839
|
+
dockerfile: appType.dockerfile,
|
|
840
|
+
domain: answers.domains[appType.suffix][env],
|
|
841
|
+
port: appType.port,
|
|
842
|
+
health_check_path: appType.healthCheckPath,
|
|
843
|
+
branch,
|
|
844
|
+
variables: {},
|
|
845
|
+
build_env: {},
|
|
846
|
+
workers: [],
|
|
847
|
+
};
|
|
848
|
+
if (isApi) {
|
|
849
|
+
app.variables = appType.variables[env] ?? {};
|
|
850
|
+
app.database = { enabled: appType.databaseEnabled, engine: appType.databaseEngine };
|
|
851
|
+
app.redis = { enabled: appType.redisEnabled };
|
|
616
852
|
}
|
|
617
|
-
|
|
618
|
-
|
|
853
|
+
else {
|
|
854
|
+
// Auto-populate build_env from domains; user-provided vars override
|
|
855
|
+
const autoBuildEnv = {};
|
|
856
|
+
const webDomain = answers.domains['web']?.[env];
|
|
857
|
+
if (webDomain)
|
|
858
|
+
autoBuildEnv['NEXT_PUBLIC_APP_HOST'] = `https://${webDomain}`;
|
|
859
|
+
const apiDomain = answers.domains['api']?.[env];
|
|
860
|
+
if (apiDomain)
|
|
861
|
+
autoBuildEnv['NEXT_PUBLIC_API_HOST'] = `https://${apiDomain}`;
|
|
862
|
+
app.build_env = { ...autoBuildEnv, ...(appType.variables[env] ?? {}) };
|
|
863
|
+
app.database = { enabled: false, engine: azure_defaults_1.AZURE_DEFAULTS.database.engine };
|
|
864
|
+
app.redis = { enabled: false };
|
|
619
865
|
}
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
866
|
+
app.container_app = {
|
|
867
|
+
cpu: azure_defaults_1.AZURE_DEFAULTS.containerApp.cpu,
|
|
868
|
+
memory: azure_defaults_1.AZURE_DEFAULTS.containerApp.memory,
|
|
869
|
+
concurrent_requests: azure_defaults_1.AZURE_DEFAULTS.containerApp.concurrentRequests,
|
|
870
|
+
min_replicas: azure_defaults_1.AZURE_DEFAULTS.containerApp.minReplicas,
|
|
871
|
+
max_replicas: azure_defaults_1.AZURE_DEFAULTS.containerApp.maxReplicas,
|
|
872
|
+
};
|
|
873
|
+
apps.push(app);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
const config = {
|
|
877
|
+
name: answers.projectName,
|
|
878
|
+
deployment: 'azure',
|
|
879
|
+
location: answers.location,
|
|
880
|
+
slack_webhook_url: answers.slackWebhookUrl || '',
|
|
881
|
+
apps,
|
|
882
|
+
};
|
|
883
|
+
// 4. Guard against overwriting existing config
|
|
884
|
+
const outputPath = opts.output ? path.resolve(opts.output) : path.resolve('fuel.yml');
|
|
885
|
+
if (await fs.pathExists(outputPath)) {
|
|
886
|
+
throw new Error(`${outputPath} already exists. Remove it first or use --output <path>.`);
|
|
887
|
+
}
|
|
888
|
+
// 5. Write fuel.yml
|
|
889
|
+
const schemaComment = '# yaml-language-server: $schema=node_modules/@swovohq/fuel/schema/fuel.schema.json\n';
|
|
890
|
+
await fs.writeFile(outputPath, schemaComment + YAML.stringify(config));
|
|
891
|
+
const totalApps = answers.appTypes.length * answers.environments.length;
|
|
892
|
+
console.log(chalk_1.default.green('✓') +
|
|
893
|
+
` fuel.yml written — ${totalApps} apps (${answers.appTypes.length} types × ${answers.environments.length} environments)`);
|
|
894
|
+
// 6. Next steps
|
|
895
|
+
console.log('\n Next steps:');
|
|
896
|
+
console.log(' Review and edit fuel.yml to add workers/variables, then:');
|
|
897
|
+
console.log(` fuel create:app ${answers.projectName} --config fuel.yml\n`);
|
|
898
|
+
}
|
|
899
|
+
// --- infraInit ---
|
|
900
|
+
/** Interactive credentials-only flow (used by `auth:init` and `infra:init --credentials-only`) for the given cloud. */
|
|
901
|
+
async function collectCredentialsOnly(rl, cloud) {
|
|
902
|
+
console.log('\n Credentials');
|
|
903
|
+
console.log(' ────────────────────────────────────────────────────');
|
|
904
|
+
const credentials = {};
|
|
905
|
+
const credFilePath = path.resolve('.fuel-credentials');
|
|
906
|
+
if (await fs.pathExists(credFilePath)) {
|
|
907
|
+
try {
|
|
908
|
+
const existing = await fs.readJson(credFilePath);
|
|
909
|
+
console.log('\n Found existing .fuel-credentials');
|
|
910
|
+
if (cloud === 'aws') {
|
|
911
|
+
const [stsResult, ghUserResult, ghOrgResult] = await Promise.all([
|
|
912
|
+
tryGetAwsAccountId(existing), tryGetGithubUser(existing), tryGetGithubOrg(existing),
|
|
913
|
+
]);
|
|
914
|
+
if ('accountId' in stsResult)
|
|
915
|
+
console.log(` AWS Account: ${chalk_1.default.green(stsResult.accountId)}`);
|
|
916
|
+
if ('login' in ghUserResult)
|
|
917
|
+
console.log(` GitHub User: ${chalk_1.default.green(ghUserResult.login)}`);
|
|
918
|
+
if ('login' in ghOrgResult)
|
|
919
|
+
console.log(` GitHub Org: ${chalk_1.default.green(ghOrgResult.login)}`);
|
|
920
|
+
assertCredentialResults(stsResult, ghUserResult, ghOrgResult);
|
|
630
921
|
}
|
|
631
922
|
else {
|
|
632
|
-
await
|
|
923
|
+
const [azResult, ghUserResult, ghOrgResult] = await Promise.all([
|
|
924
|
+
tryGetAzureAccountInfo(existing), tryGetGithubUser(existing), tryGetGithubOrg(existing),
|
|
925
|
+
]);
|
|
926
|
+
if ('subscriptionId' in azResult)
|
|
927
|
+
console.log(` Azure Subscription: ${chalk_1.default.green(azResult.subscriptionId)}`);
|
|
928
|
+
if ('login' in ghUserResult)
|
|
929
|
+
console.log(` GitHub User: ${chalk_1.default.green(ghUserResult.login)}`);
|
|
930
|
+
if ('login' in ghOrgResult)
|
|
931
|
+
console.log(` GitHub Org: ${chalk_1.default.green(ghOrgResult.login)}`);
|
|
932
|
+
assertAzureCredentialResults(azResult, ghUserResult, ghOrgResult);
|
|
633
933
|
}
|
|
634
934
|
console.log('');
|
|
935
|
+
const useExisting = await askYesNo(rl, 'Use these credentials?', true);
|
|
936
|
+
if (useExisting) {
|
|
937
|
+
console.log(chalk_1.default.green('\n ✓ Existing credentials verified.\n'));
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
635
940
|
}
|
|
636
|
-
|
|
637
|
-
|
|
941
|
+
catch (e) {
|
|
942
|
+
if (e instanceof Error && e.message.startsWith('Credential verification failed'))
|
|
943
|
+
throw e;
|
|
944
|
+
console.log(' Could not read .fuel-credentials — prompting for credentials.');
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
if (cloud === 'aws') {
|
|
948
|
+
credentials.AWS_ACCESS_KEY_ID = await ask(rl, 'AWS_ACCESS_KEY_ID');
|
|
949
|
+
credentials.AWS_SECRET_ACCESS_KEY = await ask(rl, 'AWS_SECRET_ACCESS_KEY');
|
|
950
|
+
credentials.AWS_REGION = await ask(rl, 'AWS_REGION', 'us-east-1');
|
|
951
|
+
}
|
|
952
|
+
else {
|
|
953
|
+
credentials.AZURE_SUBSCRIPTION_ID = await ask(rl, 'AZURE_SUBSCRIPTION_ID');
|
|
954
|
+
credentials.AZURE_TENANT_ID = await ask(rl, 'AZURE_TENANT_ID');
|
|
955
|
+
credentials.AZURE_CLIENT_ID = await ask(rl, 'AZURE_CLIENT_ID');
|
|
956
|
+
credentials.AZURE_CLIENT_SECRET = await ask(rl, 'AZURE_CLIENT_SECRET');
|
|
957
|
+
}
|
|
958
|
+
credentials.GITHUB_TOKEN = await ask(rl, 'GITHUB_TOKEN');
|
|
959
|
+
credentials.GITHUB_USERNAME = await ask(rl, 'GITHUB_USERNAME');
|
|
960
|
+
credentials.GITHUB_ORGANIZATION = await ask(rl, 'GITHUB_ORGANIZATION');
|
|
961
|
+
console.log('\n Verifying credentials...');
|
|
962
|
+
if (cloud === 'aws') {
|
|
963
|
+
const [stsResult, ghUserResult, ghOrgResult] = await Promise.all([
|
|
964
|
+
tryGetAwsAccountId(credentials), tryGetGithubUser(credentials), tryGetGithubOrg(credentials),
|
|
965
|
+
]);
|
|
966
|
+
if ('accountId' in stsResult)
|
|
967
|
+
console.log(` AWS Account: ${chalk_1.default.green(stsResult.accountId)}`);
|
|
968
|
+
if ('login' in ghUserResult)
|
|
969
|
+
console.log(` GitHub User: ${chalk_1.default.green(ghUserResult.login)}`);
|
|
970
|
+
if ('login' in ghOrgResult)
|
|
971
|
+
console.log(` GitHub Org: ${chalk_1.default.green(ghOrgResult.login)}`);
|
|
972
|
+
assertCredentialResults(stsResult, ghUserResult, ghOrgResult);
|
|
973
|
+
}
|
|
974
|
+
else {
|
|
975
|
+
const [azResult, ghUserResult, ghOrgResult] = await Promise.all([
|
|
976
|
+
tryGetAzureAccountInfo(credentials), tryGetGithubUser(credentials), tryGetGithubOrg(credentials),
|
|
977
|
+
]);
|
|
978
|
+
if ('subscriptionId' in azResult)
|
|
979
|
+
console.log(` Azure Subscription: ${chalk_1.default.green(azResult.subscriptionId)}`);
|
|
980
|
+
if ('login' in ghUserResult)
|
|
981
|
+
console.log(` GitHub User: ${chalk_1.default.green(ghUserResult.login)}`);
|
|
982
|
+
if ('login' in ghOrgResult)
|
|
983
|
+
console.log(` GitHub Org: ${chalk_1.default.green(ghOrgResult.login)}`);
|
|
984
|
+
assertAzureCredentialResults(azResult, ghUserResult, ghOrgResult);
|
|
985
|
+
}
|
|
986
|
+
await fs.writeFile(credFilePath, JSON.stringify(credentials, null, 2));
|
|
987
|
+
console.log(chalk_1.default.green('\n ✓') + ' Credentials written to .fuel-credentials');
|
|
988
|
+
const gitignorePath = path.resolve('.gitignore');
|
|
989
|
+
const CRED_LINE = '.fuel-credentials';
|
|
990
|
+
if (await fs.pathExists(gitignorePath)) {
|
|
991
|
+
const content = await fs.readFile(gitignorePath, 'utf-8');
|
|
992
|
+
if (!content.split('\n').some((l) => l.trim() === CRED_LINE)) {
|
|
993
|
+
await fs.appendFile(gitignorePath, `\n${CRED_LINE}\n`);
|
|
638
994
|
}
|
|
639
|
-
return;
|
|
640
995
|
}
|
|
996
|
+
else {
|
|
997
|
+
await fs.writeFile(gitignorePath, `${CRED_LINE}\n`);
|
|
998
|
+
}
|
|
999
|
+
console.log('');
|
|
1000
|
+
}
|
|
1001
|
+
async function infraInit(opts) {
|
|
641
1002
|
if (opts.template) {
|
|
642
1003
|
const outputPath = opts.output ? path.resolve(opts.output) : path.resolve('fuel.yml');
|
|
643
1004
|
if (await fs.pathExists(outputPath)) {
|
|
644
1005
|
throw new Error(`${outputPath} already exists. Remove it first or use --output <path>.`);
|
|
645
1006
|
}
|
|
646
1007
|
const schemaComment = '# yaml-language-server: $schema=node_modules/@swovohq/fuel/schema/fuel.schema.json\n';
|
|
647
|
-
|
|
1008
|
+
const template = opts.azure ? exports.CONFIG_TEMPLATE_AZURE : exports.CONFIG_TEMPLATE;
|
|
1009
|
+
await fs.writeFile(outputPath, schemaComment + YAML.stringify(template));
|
|
648
1010
|
console.log(chalk_1.default.green('✓') + ` Config template written to ${outputPath}`);
|
|
649
1011
|
console.log('\nEdit the file with your project details, then run:\n' +
|
|
650
1012
|
' fuel create:app my-app --config fuel.yml\n');
|
|
@@ -652,8 +1014,19 @@ async function infraInit(opts) {
|
|
|
652
1014
|
}
|
|
653
1015
|
const rl = (0, promises_1.createInterface)({ input: process.stdin, output: process.stdout });
|
|
654
1016
|
try {
|
|
655
|
-
const
|
|
656
|
-
|
|
1017
|
+
const cloud = opts.azure ? 'azure' : await askCloudChoice(rl);
|
|
1018
|
+
if (opts.credentialsOnly) {
|
|
1019
|
+
await collectCredentialsOnly(rl, cloud);
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
if (cloud === 'azure') {
|
|
1023
|
+
const answers = await collectAzureAnswers(rl);
|
|
1024
|
+
await applyAzureInfraInitAnswers(answers, opts);
|
|
1025
|
+
}
|
|
1026
|
+
else {
|
|
1027
|
+
const answers = await collectAnswers(rl);
|
|
1028
|
+
await applyInfraInitAnswers(answers, opts);
|
|
1029
|
+
}
|
|
657
1030
|
}
|
|
658
1031
|
finally {
|
|
659
1032
|
rl.close();
|