Ctrl K
ComunidadGuías…

src/utils.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import * as fs from 'fs';
import * as path from 'path';
import { aws_ec2 as ec2, aws_iam as iam, aws_lambda as lambda, aws_logs as logs } from 'aws-cdk-lib';
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';


/**
 * Initialize or return a singleton Lambda function instance.
 *
 * @internal
 */
export function singletonLambda<FunctionType extends lambda.Function>(
  functionType: new (s: Construct, i: string, p?: lambda.FunctionOptions) => FunctionType,
  scope: Construct, id: string, props?: lambda.FunctionOptions): FunctionType {

  const constructName = `${id}-dcc036c8-876b-451e-a2c1-552f9e06e9e1`;
  const existing = cdk.Stack.of(scope).node.tryFindChild(constructName);
  if (existing) {
    // Just assume this is true
    return existing as FunctionType;
  }

  return new functionType(cdk.Stack.of(scope), constructName, props);
}

/**
 * Initialize or return a singleton role instance.
 *
 * @internal
 */
export function singletonRole(scope: Construct, id: string, assumedBy: iam.IPrincipal): iam.IRole {
  const constructName = `${id}-dcc036c8-876b-451e-a2c1-552f9e06e9e1`;
  const existing = cdk.Stack.of(scope).node.tryFindChild(constructName);
  if (existing) {
    // Just assume this is true
    return existing as iam.Role;
  }

  return new iam.Role(cdk.Stack.of(scope), constructName, { assumedBy });
}

/**
 * Central log group type.
 *
 * @internal
 */
export enum SingletonLogType {
  RUNNER_IMAGE_BUILD = 'Runner Image Build Helpers Log',
  ORCHESTRATOR = 'Orchestrator Log',
  SETUP = 'Setup Log',
}

/**
 * Initialize or return central log group instance.
 *
 * @internal
 */
export function singletonLogGroup(scope: Construct, type: SingletonLogType): logs.ILogGroup {
  const existing = cdk.Stack.of(scope).node.tryFindChild(type);
  if (existing) {
    // Just assume this is true
    return existing as logs.ILogGroup;
  }

  return new logs.LogGroup(cdk.Stack.of(scope), type, {
    retention: logs.RetentionDays.ONE_MONTH,
    removalPolicy: cdk.RemovalPolicy.DESTROY,
  });
}

/**
 * The absolute minimum permissions required for SSM Session Manager to work. Unlike `AmazonSSMManagedInstanceCore`, it doesn't give permission to read all SSM parameters.
 *
 * @internal
 */
export const MINIMAL_SSM_SESSION_MANAGER_POLICY_STATEMENT = new iam.PolicyStatement({
  actions: [
    'ssmmessages:CreateControlChannel',
    'ssmmessages:CreateDataChannel',
    'ssmmessages:OpenControlChannel',
    'ssmmessages:OpenDataChannel',
  ],
  resources: ['*'],
});

/**
 * The absolute minimum permissions required for SSM Session Manager on ECS to work. Unlike `AmazonSSMManagedInstanceCore`, it doesn't give permission to read all SSM parameters.
 *
 * @internal
 */
export const MINIMAL_ECS_SSM_SESSION_MANAGER_POLICY_STATEMENT = new iam.PolicyStatement({
  actions: [
    'ssmmessages:CreateControlChannel',
    'ssmmessages:CreateDataChannel',
    'ssmmessages:OpenControlChannel',
    'ssmmessages:OpenDataChannel',
    's3:GetEncryptionConfiguration',
  ],
  resources: ['*'],
});

/**
 * The absolute minimum permissions required for SSM Session Manager on EC2 to work. Unlike `AmazonSSMManagedInstanceCore`, it doesn't give permission to read all SSM parameters.
 *
 * @internal
 */
export const MINIMAL_EC2_SSM_SESSION_MANAGER_POLICY_STATEMENT = new iam.PolicyStatement({
  actions: [
    'ssmmessages:CreateControlChannel',
    'ssmmessages:CreateDataChannel',
    'ssmmessages:OpenControlChannel',
    'ssmmessages:OpenDataChannel',
    's3:GetEncryptionConfiguration',
    'ssm:UpdateInstanceInformation',
  ],
  resources: ['*'],
});

/**
 * Discovers certificate files from a given path (file or directory).
 *
 * If the path is a directory, finds all .pem and .crt files in it.
 * If the path is a file, returns it as a single certificate file.
 *
 * @param sourcePath path to a certificate file or directory containing certificate files
 * @returns array of certificate file paths, sorted alphabetically
 * @throws Error if path doesn't exist, is neither file nor directory, or directory has no certificate files
 *
 * @internal
 */
export function discoverCertificateFiles(sourcePath: string): string[] {
  let certificateFiles: string[] = [];

  try {
    const stat = fs.statSync(sourcePath);
    if (stat.isDirectory()) {
      // Read directory and find all .pem and .crt files
      const files = fs.readdirSync(sourcePath);
      certificateFiles = files
        .filter(file => file.endsWith('.pem') || file.endsWith('.crt'))
        .map(file => path.join(sourcePath, file))
        .sort(); // Sort for consistent ordering

      if (certificateFiles.length === 0) {
        throw new Error(`No certificate files (.pem or .crt) found in directory: ${sourcePath}`);
      }
    } else if (stat.isFile()) {
      // Single file - backwards compatible
      certificateFiles = [sourcePath];
    } else {
      throw new Error(`Certificate source path is neither a file nor a directory: ${sourcePath}`);
    }
  } catch (error) {
    if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
      throw new Error(`Certificate source path does not exist: ${sourcePath}`);
    }
    throw error;
  }

  return certificateFiles;
}

/**
 * De-duplicate repeated CloudFormation tokens (shared subnet ids, cluster ARNs, cross-stack `Fn::ImportValue`s, ...) found in a Step Functions
 * definition fragment into a `definitionSubstitutions` map. Each distinct token gets a single `${__sfnsub_N}` placeholder, so its intrinsic renders
 * once in the template instead of once per occurrence (e.g. once per provider sharing a VPC). `obj` is mutated in place, replacing every token leaf
 * with its placeholder; the returned map is meant to be passed straight to `StateMachine`'s `definitionSubstitutions`.
 *
 * This is safe only because the definition fragment contains no other `${...}` sequences: JSONata uses `{% %}`, and the EC2 user data escapes its
 * braces (rendered as `$\{...\}`), so Step Functions' substitution leaves everything but our synthetic placeholders untouched. A literal `${`
 * reaching this function can only come from user input (a tag value, a construct id, ...), so it's reported as an error instead of silently producing
 * a definition that Step Functions rejects with an unhelpful message at deploy time.
 *
 * @internal
 */
export function dedupeStateMachineTokens(scope: Construct, node: any): Record<string, string> {
  const stack = cdk.Stack.of(scope);
  const substitutions: Record<string, string> = {};
  const keyByToken = new Map<string, string>();

  // first pass. nothing has been rewritten yet, so any `${` we find came from user input. tokens are spelled
  // `${Token[...]}` themselves, so skip those here and let the second pass give them a placeholder
  const check = (value: any): void => {
    if (typeof value === 'string') {
      if (!cdk.Token.isUnresolved(value) && value.includes('${')) {
        cdk.Annotations.of(scope).addError(
          `A runner provider value contains "\${", which collides with the state machine definition substitutions: ${JSON.stringify(value.slice(0, 100))}`,
        );
      }
    } else if (Array.isArray(value)) {
      value.forEach(check);
    } else if (value && typeof value === 'object') {
      for (const [objKey, objValue] of Object.entries(value)) {
        if (objKey.includes('${')) {
          cdk.Annotations.of(scope).addError(
            `A runner provider path contains "\${", which collides with the state machine definition substitutions: ${JSON.stringify(objKey.slice(0, 100))}`,
          );
        }
        check(objValue);
      }
    }
  };

  // second pass. key the cache by the resolved intrinsic, so identical imports collapse even when their token
  // strings differ
  const substitute = (value: any): any => {
    if (typeof value === 'string') {
      if (!cdk.Token.isUnresolved(value)) {
        return value;
      }
      const id = JSON.stringify(stack.resolve(value));
      let key = keyByToken.get(id);
      if (!key) {
        key = `__sfnsub_${keyByToken.size}`;
        keyByToken.set(id, key);
        substitutions[key] = value;
      }
      return `\${${key}}`;
    }
    if (Array.isArray(value)) {
      return value.map(substitute);
    }
    if (value && typeof value === 'object') {
      for (const objKey of Object.keys(value)) {
        value[objKey] = substitute(value[objKey]);
      }
    }
    return value;
  };

  check(node);
  substitute(node);

  return substitutions;
}

/**
 * Returns true if the instance type has an NVIDIA GPU.
 *
 * Uses AWS naming convention: most NVIDIA GPU instances use 'g' (g4dn, g5, g6, g9...) or 'p' (p3, p4d, p5, p6...)
 * prefix followed by a digit. Explicitly excludes known non-NVIDIA GPU families such as g4ad (AMD).
 *
 * @internal
 */
export function isGpuInstanceType(instanceType: ec2.InstanceType): boolean {
  const s = instanceType.toString().toLowerCase();
  // Match GPU instance families starting with g/p + digit, but exclude known non-NVIDIA GPU families.
  return /^[gp]\d+(?!ad)/.test(s);
}