< Summary - Envilder CLI

Information
Class: src/envilder/core/application/pullSecretsToEnv/PullSecretsToEnvCommandHandler.ts
Assembly: Default
File(s): src/envilder/core/application/pullSecretsToEnv/PullSecretsToEnvCommandHandler.ts
Tag: 484_34168995013
Line coverage
100%
Covered lines: 61
Uncovered lines: 0
Coverable lines: 61
Total lines: 238
Line coverage: 100%
Branch coverage
100%
Covered branches: 18
Total branches: 18
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

File(s)

src/envilder/core/application/pullSecretsToEnv/PullSecretsToEnvCommandHandler.ts

#LineLine coverage
 1import { inject, injectable } from 'inversify';
 2import pc from 'picocolors';
 3import { EnvironmentVariable } from '../../domain/EnvironmentVariable.js';
 4import {
 5  ExpiredCredentialsError,
 6  SecretsFetchError,
 7  SsoSessionExpiredError,
 8} from '../../domain/errors/DomainErrors.js';
 9import type { ILogger } from '../../domain/ports/ILogger.js';
 10import type { ISecretMasker } from '../../domain/ports/ISecretMasker.js';
 11import type { ISecretProvider } from '../../domain/ports/ISecretProvider.js';
 12import type { IVariableStore } from '../../domain/ports/IVariableStore.js';
 13import { describeError } from '../../infrastructure/describeError.js';
 14import { TYPES } from '../../types.js';
 15import type { PullSecretsToEnvCommand } from './PullSecretsToEnvCommand.js';
 16
 17type ResolvedOutcome = { status: 'resolved'; envVar: string; masked: string };
 18type WarningOutcome = {
 19  status: 'warning';
 20  envVar: string;
 21  path: string;
 22  reason: 'not-found' | 'empty';
 23};
 24type ErrorOutcome = {
 25  status: 'error';
 26  envVar: string;
 27  path: string;
 28  reason: string;
 29};
 30type SecretOutcome = ResolvedOutcome | WarningOutcome | ErrorOutcome;
 31
 32@injectable()
 833export class PullSecretsToEnvCommandHandler {
 834  private static readonly LABEL_WIDTH = 20;
 835  private static readonly RULE = pc.yellow('\u2501'.repeat(60));
 36
 37  constructor(
 38    @inject(TYPES.ISecretProvider)
 3439    private readonly secretProvider: ISecretProvider,
 40    @inject(TYPES.IVariableStore)
 3441    private readonly variableStore: IVariableStore,
 3442    @inject(TYPES.ILogger) private readonly logger: ILogger,
 43    @inject(TYPES.ISecretMasker)
 3444    private readonly secretMasker: ISecretMasker,
 45  ) {}
 46
 47  /**
 48   * Handles the PullSecretsToEnvCommand which orchestrates the process of fetching
 49   * environment variable values from a secret store and writing them to a local environment file.
 50   *
 51   * @param command - The PullSecretsToEnvCommand containing mapPath and envFilePath
 52   */
 53  async handle(command: PullSecretsToEnvCommand): Promise<void> {
 54    const { requestVariables, currentVariables } =
 1655      await this.loadVariables(command);
 1656    const { variables, resolvedCount, totalCount } = await this.envild(
 57      requestVariables,
 58      currentVariables,
 59    );
 1160    await this.saveEnvFile(command.envFilePath, variables);
 61
 1162    this.logger.info(
 63      PullSecretsToEnvCommandHandler.buildSummary(
 64        resolvedCount,
 65        totalCount,
 66        command.envFilePath,
 67      ),
 68    );
 69  }
 70
 71  private async loadVariables(command: PullSecretsToEnvCommand): Promise<{
 72    requestVariables: Record<string, string>;
 73    currentVariables: Record<string, string>;
 74  }> {
 1675    const requestVariables = await this.variableStore.getMapping(
 76      command.mapPath,
 77    );
 1678    const currentVariables = await this.variableStore.getEnvironment(
 79      command.envFilePath,
 80    );
 81
 1682    return { requestVariables, currentVariables };
 83  }
 84
 85  private async saveEnvFile(
 86    envFilePath: string,
 87    variables: Record<string, string>,
 88  ): Promise<void> {
 1189    await this.variableStore.saveEnvironment(envFilePath, variables);
 90  }
 91
 92  private async envild(
 93    paramMap: Record<string, string>,
 94    existingEnvVariables: Record<string, string>,
 95  ): Promise<{
 96    variables: Record<string, string>;
 97    resolvedCount: number;
 98    totalCount: number;
 99  }> {
 16100    const outcomes = await Promise.all(
 101      Object.entries(paramMap).map(([envVar, secretName]) =>
 19102        this.processSecret(envVar, secretName, existingEnvVariables),
 103      ),
 104    );
 105
 14106    const resolved = outcomes.filter(
 17107      (outcome): outcome is ResolvedOutcome => outcome.status === 'resolved',
 108    );
 14109    const warnings = outcomes.filter(
 17110      (outcome): outcome is WarningOutcome => outcome.status === 'warning',
 111    );
 14112    const errors = outcomes.filter(
 17113      (outcome): outcome is ErrorOutcome => outcome.status === 'error',
 114    );
 115
 14116    this.logSecretsSection(resolved, warnings);
 117
 14118    if (errors.length > 0) {
 3119      throw new SecretsFetchError(
 3120        errors.map((error) => ({
 121          envVar: error.envVar,
 122          path: error.path,
 123          reason: error.reason,
 124        })),
 125      );
 126    }
 127
 11128    return {
 129      variables: existingEnvVariables,
 130      resolvedCount: resolved.length,
 131      totalCount: Object.keys(paramMap).length,
 132    };
 133  }
 134
 135  private async processSecret(
 136    envVar: string,
 137    secretName: string,
 138    existingEnvVariables: Record<string, string>,
 139  ): Promise<SecretOutcome> {
 19140    try {
 19141      const value = await this.secretProvider.getSecret(secretName);
 14142      if (value === undefined) {
 2143        return {
 144          status: 'warning',
 145          envVar,
 146          path: secretName,
 147          reason: 'not-found',
 148        };
 149      }
 12150      if (value === '') {
 4151        return { status: 'warning', envVar, path: secretName, reason: 'empty' };
 152      }
 153
 8154      this.secretMasker.mask(value);
 8155      existingEnvVariables[envVar] = value;
 8156      const masked = new EnvironmentVariable(envVar, value, true).maskedValue;
 157
 8158      return { status: 'resolved', envVar, masked };
 159    } catch (error) {
 5160      if (
 161        error instanceof ExpiredCredentialsError ||
 162        error instanceof SsoSessionExpiredError
 163      ) {
 2164        throw error;
 165      }
 3166      const maskedPath = EnvironmentVariable.maskSecretPath(secretName);
 3167      const reason = PullSecretsToEnvCommandHandler.describeErrorReason(
 168        error,
 169        maskedPath,
 170      );
 3171      return { status: 'error', envVar, path: maskedPath, reason };
 172    }
 173  }
 174
 175  private static describeErrorReason(
 176    error: unknown,
 177    maskedPath: string,
 178  ): string {
 3179    const message = describeError(error);
 3180    const duplicatedPrefix = `${maskedPath}: `;
 3181    return message.startsWith(duplicatedPrefix)
 182      ? message.slice(duplicatedPrefix.length)
 183      : message;
 184  }
 185
 186  private logSecretsSection(
 187    resolved: ResolvedOutcome[],
 188    warnings: WarningOutcome[],
 189  ): void {
 14190    if (resolved.length === 0 && warnings.length === 0) {
 2191      return;
 192    }
 193
 12194    this.logger.info(`\n${pc.bold(pc.yellow('\u{1FA99}  RESOLVING SECRETS'))}`);
 12195    this.logger.info(PullSecretsToEnvCommandHandler.RULE);
 12196    for (const outcome of resolved) {
 8197      this.logger.info(
 198        `  ${pc.green('\u2713 ')}${pc.bold(
 199          PullSecretsToEnvCommandHandler.pad(outcome.envVar),
 200        )}${pc.dim('\u2192 ')}${pc.dim(outcome.masked)}`,
 201      );
 202    }
 12203    this.logWarnings(warnings);
 204  }
 205
 206  private logWarnings(warnings: WarningOutcome[]): void {
 12207    for (const outcome of warnings) {
 6208      const maskedPath = EnvironmentVariable.maskSecretPath(outcome.path);
 6209      if (outcome.reason === 'not-found') {
 2210        this.logger.warn(
 211          `  ${pc.red('\u2717 ')}${pc.bold(
 212            pc.red(PullSecretsToEnvCommandHandler.pad(outcome.envVar)),
 213          )} ${pc.red(`secret not found (path: ${maskedPath}) \u2014 skipped`)}`,
 214        );
 2215        continue;
 216      }
 4217      this.logger.warn(
 218        `  ${pc.yellow('\u26A0 ')}${pc.bold(
 219          PullSecretsToEnvCommandHandler.pad(outcome.envVar),
 220        )} ${pc.dim(`no value found (path: ${maskedPath}) \u2014 skipped`)}`,
 221      );
 222    }
 223  }
 224
 225  private static pad(name: string): string {
 14226    return name.padEnd(PullSecretsToEnvCommandHandler.LABEL_WIDTH);
 227  }
 228
 229  private static buildSummary(
 230    resolvedCount: number,
 231    totalCount: number,
 232    envFilePath: string,
 233  ): string {
 11234    return `\n${pc.bold(pc.green('\u2B50 LEVEL CLEARED'))}${pc.dim(
 235      `  \u2014  ${resolvedCount}/${totalCount} secrets loaded \u00B7 `,
 236    )}${pc.bold(envFilePath)}${pc.dim(' written')}\n`;
 237  }
 238}