/** * Injectable cloud I/O boundary for the AWS-leaf capability implementations * (#557, epic #551), living in the aws lexicon so a project's cloud verbs are * contributed by its active lexicon rather than baked into core (see * docs/components/cloud-boundary). Every AWS-leaf capability here goes through * a `CloudExecutor` instead of shelling out directly, so: * * - production code gets a real executor that shells out to the `aws` and * `docker` CLIs (this codebase shells out to native CLIs rather than * depending on AWS SDK v3 packages); * - tests get a `MockCloudExecutor` (./__tests__/mock-cloud-executor.ts) that * records calls and returns canned results — no live AWS, no live docker. * * The `docker` client is genuinely agnostic and shared: its type + real * implementation (`realDocker`) come from core; only the AWS-specific clients * (ECR, CloudFormation, ECS, CodeDeploy, Lambda, EMR, SSM host, S3, CloudFront, * snapshot) are defined here. */ import { type DockerClient } from "@intentius/chant/components/verbs/cloud-executor"; export type { DockerClient }; export interface EcrClient { /** Authenticate the local docker client against a registry (`aws ecr get-login-password | docker login`). */ login(registry: string): Promise; } export interface CfnChange { action: "Add" | "Modify" | "Remove" | "Import" | "Dynamic"; logicalResourceId: string; resourceType: string; /** True when CloudFormation must replace (destroy + recreate) this resource to apply the change. */ replacement: boolean; /** Resource-specific detail, when the executor's changeset backend provides it (e.g. property/scope). */ details?: string[]; } export interface CfnChangeSet { changeSetName: string; stackName: string; status: string; /** CloudFormation's `StatusReason` for the change set, when present. A FAILED * change set whose reason names an empty diff ("didn't contain changes" / "No * updates are to be performed") is a no-op, not a real failure — see * {@link isNoopChangeSet}. */ statusReason?: string; /** True the first time this stack is created (no prior stack exists). */ isCreate: boolean; changes: CfnChange[]; } export interface CfnCreateChangeSetArgs { stackName: string; templatePath: string; parameters?: Record; } export interface CfnExecuteChangeSetArgs { stackName: string; changeSetName: string; } export interface CfnStackStatus { stackStatus: string; outputs: Record; } export interface CloudFormationClient { /** Create (but do not execute) a changeset, returning its proposed changes for preview/safety checks. */ createChangeSet(args: CfnCreateChangeSetArgs): Promise; /** Execute a previously created changeset. */ executeChangeSet(args: CfnExecuteChangeSetArgs): Promise; /** Delete a changeset without executing it (used when a safety policy blocks the apply). */ deleteChangeSet(args: CfnExecuteChangeSetArgs): Promise; /** Poll a stack until it reaches a terminal status (`*_COMPLETE`/`*_FAILED`), returning final status + outputs. */ waitForStack(stackName: string, opts?: { intervalMs?: number; timeoutMs?: number; }): Promise; /** Current stack status + outputs, without waiting. */ describeStack(stackName: string): Promise; /** Trigger CloudFormation's native rollback-to-last-known-good-state for a stack (saga compensation). */ rollbackStack(stackName: string): Promise; } export interface EcsUpdateServiceArgs { cluster: string; service: string; taskDefinition?: string; desiredCount?: number; forceNewDeployment?: boolean; } export interface EcsServiceState { runningCount: number; desiredCount: number; /** True once `runningCount === desiredCount` and no deployments are in flight. */ stable: boolean; } export interface EcsRunTaskArgs { cluster: string; /** Task definition family (`:revision` optional) to run. */ taskDefinition: string; /** Container to apply the command override to. Defaults to the task def's single container when omitted. */ container?: string; /** Command (argv) override — e.g. the migration runner's invocation. */ command?: string[]; /** Launch type. Default: "FARGATE". */ launchType?: "FARGATE" | "EC2"; /** awsvpc subnets (required for FARGATE). */ subnets?: string[]; /** awsvpc security groups. */ securityGroups?: string[]; /** Whether the task gets a public IP (needed for FARGATE tasks in a public subnet pulling images). Default: false. */ assignPublicIp?: boolean; } export interface EcsRunTaskResult { /** Terminal task status (`STOPPED`). */ lastStatus: string; /** Exit code of the (first) container, once the task has stopped; undefined if the container never started (e.g. image pull failure). */ exitCode: number | undefined; /** Reason the task stopped, when it did not run to a clean container exit. */ stoppedReason?: string; } export interface EcsClient { /** Roll a new task definition/desired count out to a service; returns the new deployment's id. */ updateService(args: EcsUpdateServiceArgs): Promise<{ deploymentId: string; }>; /** Current running/desired counts for a service, used by `wait-steady-state`. */ describeService(cluster: string, service: string): Promise; /** Roll a service back to a previously recorded task definition/count (saga compensation). */ rollbackService(args: EcsUpdateServiceArgs): Promise; /** Run a one-off task (e.g. a DB migration) and return its arn for `waitForTask`. */ runTask(args: EcsRunTaskArgs): Promise<{ taskArn: string; }>; /** Wait for a one-off task to stop, returning its terminal status and container exit code. */ waitForTask(cluster: string, taskArn: string): Promise; } export interface CodeDeployCreateArgs { application: string; deploymentGroup: string; revision: { type: "s3"; uri: string; } | { type: "github"; repository: string; commitId: string; }; strategy?: "in-place" | "blue-green"; } export interface CodeDeployStatus { status: string; /** True for a terminal status (`Succeeded`, `Failed`, `Stopped`). */ terminal: boolean; } export interface CodeDeployClient { /** Create a deployment of a revision to a deployment group; returns the deployment id. */ createDeployment(args: CodeDeployCreateArgs): Promise<{ deploymentId: string; }>; /** Poll a deployment until it reaches a terminal status. */ waitForDeployment(deploymentId: string, opts?: { intervalMs?: number; timeoutMs?: number; }): Promise; /** Stop an in-flight deployment and roll the deployment group back to the last known-good revision (native auto-rollback, invoked as saga compensation). */ stopAndRollback(deploymentId: string): Promise; } export interface HostCopyFileArgs { /** Target host (SSM instance id, hostname, or host group — same identifier space as `copy-to-host`/`remote-exec`). */ host: string; /** Source path of the file to copy (typically an archive-relative image tarball path). */ from: string; /** Destination path on the host. */ to: string; } export interface HostDockerLoadArgs { /** Target host the tarball was copied to. */ host: string; /** Path of the tarball on the host (matches `HostCopyFileArgs.to`). */ path: string; } export interface HostClient { /** Copy a file (an image tarball from the build archive) onto a host, via SSM Run Command / SCP-over-SSH depending on config — the same transport `copy-to-host` documents. */ copyFile(args: HostCopyFileArgs): Promise; /** Run `docker load` on the host against a previously copied tarball; returns the digest `docker load` reports, straight into the host's local Docker store (no registry involved). */ dockerLoad(args: HostDockerLoadArgs): Promise<{ digest: string; }>; /** Run a shell command on a host via SSM Run Command, waiting for it to finish; returns captured stdout and exit code (rejects on a non-zero SSM invocation). */ exec(args: { host: string; command: string; cwd?: string; }): Promise<{ stdout: string; exitCode: number; }>; } export interface LambdaUpdateCodeArgs { functionName: string; /** Container image URI (registry/repo@sha256:...) for an image-package Lambda. */ imageUri: string; } export interface LambdaPublishVersionArgs { functionName: string; } export interface LambdaUpdateAliasArgs { functionName: string; alias: string; version: string; } export interface LambdaClient { /** Point the function at a new container image; returns the function ARN (unqualified). */ updateFunctionCode(args: LambdaUpdateCodeArgs): Promise<{ functionArn: string; }>; /** Wait for the function's last update to finish applying (`Successful`/`Failed`). */ waitForUpdate(functionName: string): Promise<{ status: string; }>; /** Publish an immutable numbered version from the function's current `$LATEST`. */ publishVersion(args: LambdaPublishVersionArgs): Promise<{ version: string; functionArn: string; }>; /** Repoint a named alias at a published version (e.g. "live" -> "42"). */ updateAlias(args: LambdaUpdateAliasArgs): Promise<{ aliasArn: string; }>; /** Current published version an alias points at, so rollback can restore it. */ getAliasVersion(functionName: string, alias: string): Promise; /** Synchronously invoke a function (e.g. a migration runner) with an optional JSON payload; returns the status code, response payload text, and any function error. */ invoke(args: { functionName: string; payload?: string; }): Promise<{ statusCode: number; payload: string; functionError?: string; }>; } export interface EmrStartJobRunArgs { /** EMR Serverless application id, or EMR-on-EC2 cluster id. */ clusterOrApplicationId: string; /** Entry point artifact reference (resolved by the graph before this executor is called, e.g. an S3 URI). */ jar: string; args?: string[]; executionRoleArn?: string; } export interface EmrJobRunStatus { /** Terminal job state (`COMPLETED`, `FAILED`, `CANCELLED`), or an in-flight state (`RUNNING`, `PENDING`) while polling. */ state: string; } export interface EmrAddStepArgs { /** EMR-on-EC2 cluster id to submit the step to. */ clusterId: string; /** Step name (shown in the EMR console). */ name: string; /** Jar the step runs — an S3 jar, or `command-runner.jar` with a `spark-submit …` arg list. */ jar: string; args?: string[]; /** What EMR does if the step fails. Default: "CONTINUE". */ actionOnFailure?: "CONTINUE" | "CANCEL_AND_WAIT" | "TERMINATE_CLUSTER"; } export interface EmrClient { /** Start a job run (EMR Serverless application or EMR-on-EC2 cluster) against a published artifact; returns the run id for polling. */ startJobRun(args: EmrStartJobRunArgs): Promise<{ runId: string; }>; /** Submit a step to a long-running EMR-on-EC2 cluster; returns the step id for polling. */ addStep(args: EmrAddStepArgs): Promise<{ stepId: string; }>; /** Poll a job run until it reaches a terminal state (`COMPLETED`/`FAILED`/`CANCELLED`). */ waitForJobRun(runId: string, opts?: { intervalMs?: number; timeoutMs?: number; }): Promise; /** Current state of a job run, without waiting. */ describeJobRun(runId: string): Promise; /** Cancel an in-flight job run (saga compensation). */ cancelJobRun(runId: string): Promise; } /** * The full injectable cloud I/O surface the AWS-leaf capabilities depend on. * A capability module never imports `node:child_process`/an AWS SDK/`net` * directly — it takes a `CloudExecutor` (defaulted to `realCloudExecutor()` * at module scope, overridable via each capability family's * `create*Capability(executor)` factory) so tests can swap in a full mock. */ export interface S3SyncArgs { from: string; to: string; /** Delete destination keys not present in the source. */ delete?: boolean; } export interface S3Client { /** `aws s3 sync from to [--delete]`; returns how many objects were uploaded/deleted. */ sync(args: S3SyncArgs): Promise<{ uploaded: number; deleted: number; }>; /** `aws s3 cp from to` — upload a single local file to an S3 URI. */ cp(args: { from: string; to: string; }): Promise; } export interface CloudFrontInvalidateArgs { distributionId: string; paths: string[]; } export interface CloudFrontClient { /** `aws cloudfront create-invalidation`; returns the invalidation batch id. */ createInvalidation(args: CloudFrontInvalidateArgs): Promise<{ invalidationId: string; }>; } export type SnapshotResourceKind = "dynamodb-table" | "rds-instance" | "opensearch-domain" | "ebs-volume"; export interface SnapshotClient { /** Take an on-demand snapshot/backup of a resource, dispatched by kind; returns the backup/snapshot identifier `rollback-previous` restores from. */ create(args: { resource: string; resourceKind: SnapshotResourceKind; }): Promise<{ snapshotId: string; }>; /** Restore a resource from a prior snapshot/backup, dispatched by the snapshot-id shape (DynamoDB/RDS ARN); waits for the restore to become available. */ restore(args: { resource: string; snapshotId: string; }): Promise; } export interface CloudExecutor { docker: DockerClient; ecr: EcrClient; cloudformation: CloudFormationClient; ecs: EcsClient; codeDeploy: CodeDeployClient; lambda: LambdaClient; emr: EmrClient; host: HostClient; s3: S3Client; cloudfront: CloudFrontClient; snapshot: SnapshotClient; } /** * Inject `--endpoint-url` into an `aws …` command when an endpoint is set, so the * same component can target a local AWS emulator (Floci, LocalStack, …) or any * custom endpoint without a wrapper. We add the flag ourselves rather than rely * on the CLI reading `AWS_ENDPOINT_URL` — older `aws` v2 releases (<2.13) don't. * Non-`aws` commands (docker, …) pass through untouched. */ export declare function applyAwsEndpoint(command: string, endpoint: string | undefined): string; /** * argv-form of {@link applyAwsEndpoint}: insert `--endpoint-url ` right * after `aws` (argv[0]) when an endpoint is set, so a *spawned* `aws …` call (live * acquisition, `graph --live`) targets a local emulator instead of real AWS. * Non-`aws` argvs pass through untouched. Same reason as the string form — older * `aws` v2 (<2.13) ignores `AWS_ENDPOINT_URL`. */ export declare function applyAwsEndpointArgv(argv: string[], endpoint: string | undefined): string[]; /** * The `--capabilities` a deploy needs for a template. `CAPABILITY_NAMED_IAM` is * always required (chant emits named IAM roles); `CAPABILITY_AUTO_EXPAND` is added * whenever the template declares a top-level `Transform` macro (e.g. * `AWS::SecretsManager-2020-07-23` for a HostedRotationLambda rotation, or SAM) — * the macro expands into nested stacks/resources during change-set creation, and * CloudFormation refuses without the acknowledgement. */ export declare function awsDeployCapabilities(template: { Transform?: unknown; }): string; /** {@link awsDeployCapabilities} as a list, for the CFN API's `Capabilities.member.N` params. */ export declare function awsDeployCapabilityList(template: { Transform?: unknown; }): string[]; /** * {@link awsDeployCapabilityList} for a raw template body. A body that isn't * JSON gets the default list; the deploy itself reports the real problem. */ export declare function awsDeployCapabilitiesForBody(body: string): string[]; /** Deployment id from an ECS `update-service` response. Tolerant of Floci, whose * `service` omits `deployments` entirely (real AWS always includes it) — guards * the index, not just `.id` (#937). Exported for testing. */ export declare function ecsDeploymentId(described: { service: { deployments?: Array<{ id: string; }>; }; }): string; /** Whether an ECS service is steady (running == desired, ≤1 active deployment). * Tolerant of Floci's missing `deployments` field. Exported for testing. */ export declare function ecsServiceStable(svc: { runningCount?: number; desiredCount?: number; deployments?: unknown[]; } | undefined): boolean; /** Build a `CloudExecutor` that shells out to real `docker`/`aws` CLIs and probes real bolt ports. Never used in tests. */ export declare function realCloudExecutor(): CloudExecutor; /** The default `CloudExecutor` each capability factory falls back to when none is supplied. */ export declare function defaultCloudExecutor(): CloudExecutor; /** Sleep for `ms`. Shared by every polling capability (`wait-*`) between attempts. */ export declare function sleep(ms: number): Promise; //# sourceMappingURL=cloud-executor.d.ts.map