import type { CloudFormationEventMonitor, ResourceEvent } from "../../aws/utils/cloudformationEvents.js"; import { type EventFloor, type StackEventsPageRead } from "../../aws/utils/cloudformationEventTypes.js"; import type { CancelInFlightOutcome } from "./cancelInFlightStack.js"; import { startHungUpdateWatch, type HungUpdateCancel, type HungUpdateWatchOptions } from "./hungUpdateWatch.js"; import { type Result } from "@fjall/generator"; import { CloudFormationError } from "./cloudFormationError.js"; /** What a caller hands a stack update so it is followed as a deploy follows a stack. */ export interface StackUpdateWatch { /** Each of the stack's resource events from the update on: the ECS tail reads them. */ readonly onResourceProgress: (event: ResourceEvent) => void; /** The engine's hung-update watch over the tail's verdicts. */ readonly hungUpdate: HungUpdateWatchOptions; /** How long to follow the update: the deploy's own budget. */ readonly timeoutMs: number; } /** * Where a followed update got to. `status` is the stack's last status read; * `reason` is CloudFormation's text or the follow's own, unmasked, for the * caller to mask where it builds its message. A wait that ran out reports the * monitor's own text, which can carry a reason it read from the stack, and * the caller masks it with every other reason. */ export type FollowedStackUpdate = { readonly kind: "not-started"; } | { readonly kind: "completed"; readonly status: string; } | { readonly kind: "failed"; readonly status: string | undefined; readonly reason: string; } | { readonly kind: "cancelled"; readonly cancel: HungUpdateCancel; readonly status: string | undefined; readonly stillRunning: boolean; } | { readonly kind: "still-running"; readonly status: string | undefined; readonly reason: string; } | { readonly kind: "aborted"; }; export interface StackUpdateFollowDeps { readonly monitor: Pick; /** Sends CancelUpdateStack with the watch's token, signed as the caller's reads are. */ readonly cancel: (stackName: string, clientRequestToken: string) => Promise; /** Test seam. */ readonly startWatch?: typeof startHungUpdateWatch; } /** * Whether the stack's events have shown the followed operation's start yet, * waiting up to `timeoutMs` for them to: true as soon as they have, false * when the time runs out or the caller cancels first. */ export type OperationSeen = (timeoutMs: number) => Promise; /** * Starts `stackName`'s update with `start` once the monitor has absorbed the * events already there, and follows it to its end or for the watch's budget. * `start` answers whether an update began; when none did, nothing is * followed. It is handed `seen`, which waits on the stream this follow reads * for the update's first event, so a start whose answer was lost can be * settled from the events the monitor reads rather than one read of its own. * * Only the update sent with `clientRequestToken` is followed: the monitor * dispatches nothing before an event carries that token, the update's own * start, and every event from it on, its rollback's included * (`PollContext.operation`). So no event of another operation reaches the * caller's tail, whose verdicts the watch acts on, and none ends the wait, * an operation that ended after the floor was read included. Every event of * the update reaches the caller, its services' first UPDATE_IN_PROGRESS * included, which is what the ECS tail adopts a service from. Events the * monitor reads while `start` runs are held until it answers, and dropped * when no update began. Events at or before `floor` belong to the operation * before it, should the monitor's first read fail. When the budget runs out * while the update still runs and the tail holds a rollback the watch has * not acted on, the follow gets `HUNG_UPDATE_REATTACH_BUDGET_MS` more, as a * deploy's re-attach does, so the watch can end the update. A cancelled * caller stops following and leaves the update running: the watch never * fires the caller's signal, and the caller's signal never cancels the stack. */ export declare function followStackUpdate(stackName: string, floor: EventFloor | undefined, watch: StackUpdateWatch, deps: StackUpdateFollowDeps, clientRequestToken: string, start: (seen: OperationSeen) => Promise, abortSignal?: AbortSignal): Promise; /** * How long a followed update whose answer was lost is waited on for the * stack's events to show its start, before the stack's newest events are * read to settle it. Several of the monitor's polls, five seconds apart, * fall inside it, so a first event DescribeStackEvents lists a few seconds * late is still seen. How late it can list one has not been measured. */ export declare const UNANSWERED_UPDATE_SETTLE_MS = 30000; /** * What the stack's newest events say of an update whose answer was lost. * `reason` is pre-masked at construction: the read's own, masked where * `fetchNewestStackEvents` built it, or a fixed literal. */ export type UnansweredUpdateVerdict = { readonly kind: "started"; } | { readonly kind: "not-started"; } | { readonly kind: "unknown"; readonly reason: string; }; /** * Whether an update sent with `clientRequestToken`, whose answer was lost, * started. CloudFormation's API reference (`StackEvent.ClientRequestToken`) * gives every event of an operation the request token the operation was sent * with, so it started when the stack's newest stack-level event carries the * token. A newest stack-level event carrying another token, or none, is * another operation's, and a page that reaches `floor`'s own event before any * stack-level event shows no operation that logged its start after the floor: * either way the events show nothing of this update, and so too when the * stack does not exist, so the send's failure stands. A read that failed, and * a page that reaches neither, say nothing. * Event times are never compared with the floor's, which may be the local * clock's: the token, not the time, says whose an event is. */ export declare function unansweredUpdateVerdict(read: StackEventsPageRead, stackName: string, floor: EventFloor | undefined, clientRequestToken: string): UnansweredUpdateVerdict; /** * The parameter update's result, from where following it got to. Every * message is pre-masked at construction. A cancelled update's message is * masked whole, as `maskSensitiveOutput(cancelFailure)`: ECS's status reason * in it comes from the verdict source the caller handed the watch, which may * not have masked it. */ export declare function parameterUpdateResult(stackName: string, skippedKeys: string[], followed: FollowedStackUpdate): Result<{ updated: boolean; skippedKeys: string[]; }, CloudFormationError>;