UNPKG

@enspirit/emb

Version:

A replacement for our Makefile-for-monorepos

146 lines (145 loc) 5.64 kB
import { getContext } from '../../../index.js'; import { Flags } from '@oclif/core'; import { printTable } from '@oclif/table'; import { FlavoredCommand, TABLE_DEFAULTS } from '../../index.js'; import { aggregateSecrets, discoverSecrets, } from '../../../secrets/SecretDiscovery.js'; export default class SecretsValidate extends FlavoredCommand { static description = 'Validate that all secret references can be resolved (without showing values).'; static enableJsonFlag = true; static examples = [ '<%= config.bin %> <%= command.id %>', '<%= config.bin %> <%= command.id %> --fail-fast', '<%= config.bin %> <%= command.id %> --json', ]; static flags = { 'fail-fast': Flags.boolean({ default: false, description: 'Stop on first validation error', }), }; async run() { const { flags } = await this.parse(SecretsValidate); const context = getContext(); const { monorepo, secrets } = context; // Get registered secret providers dynamically const secretProviders = new Set(secrets.getProviderNames()); // Collect secrets from all configuration sources const allSecrets = []; // Scan monorepo-level config (env, vars, tasks, defaults, flavors) allSecrets.push(...discoverSecrets({ env: monorepo.config.env, vars: monorepo.config.vars, tasks: monorepo.config.tasks, defaults: monorepo.config.defaults, flavors: monorepo.config.flavors, }, { file: '.emb.yml' }, secretProviders)); // Scan each component's config for (const component of monorepo.components) { allSecrets.push(...discoverSecrets({ tasks: component.config.tasks, resources: component.config.resources, }, { file: `${component.name}/Embfile.yml`, component: component.name, }, secretProviders)); } // Aggregate by unique secret reference const aggregated = aggregateSecrets(allSecrets); if (aggregated.length === 0) { if (!flags.json) { this.log('No secret references found in configuration.'); } return []; } // Validate each secret const results = []; let hasErrors = false; for (const secret of aggregated) { // Sequential validation is intentional for fail-fast support // eslint-disable-next-line no-await-in-loop const result = await this.validateSecret(secret, secrets); results.push(result); if (result.status === 'error') { hasErrors = true; if (flags['fail-fast']) { break; } } } if (!flags.json) { printTable({ ...TABLE_DEFAULTS, columns: ['status', 'provider', 'path', 'key'], data: results.map((r) => ({ status: r.status === 'ok' ? '✔' : '✖', provider: r.provider, path: r.path, key: r.key || '-', })), }); const passed = results.filter((r) => r.status === 'ok').length; const failed = results.filter((r) => r.status === 'error').length; this.log(`\nValidation: ${passed} passed, ${failed} failed`); // Show error details const errors = results.filter((r) => r.status === 'error'); if (errors.length > 0) { this.log('\nError details:'); for (const error of errors) { const ref = error.key ? `${error.provider}:${error.path}#${error.key}` : `${error.provider}:${error.path}`; this.log(` - ${ref}: ${error.error}`); } } } // Exit with error code if validation failed if (hasErrors) { this.exit(1); } return results; } async validateSecret(secret, secrets) { const provider = secrets.get(secret.provider); if (!provider) { return { provider: secret.provider, path: secret.path, key: secret.key, status: 'error', error: `Provider '${secret.provider}' not configured`, }; } try { // Actually fetch the secret to verify access const result = await provider.get({ path: secret.path, key: secret.key, }); // If a key was specified, verify it exists if (secret.key && result === undefined) { return { provider: secret.provider, path: secret.path, key: secret.key, status: 'error', error: `Key '${secret.key}' not found in secret`, }; } return { provider: secret.provider, path: secret.path, key: secret.key, status: 'ok', }; } catch (error) { return { provider: secret.provider, path: secret.path, key: secret.key, status: 'error', error: error instanceof Error ? error.message : String(error), }; } } }