/** * AWS deep observation (#1015) — the reference implementation of the epic's * deep-observe contract (#1014). * * `describeResources` reads `cloudformation describe-stack-resources`, which * returns a status, a physical id and a timestamp per resource. That is * CloudFormation's view of the world, and CloudFormation only compares * properties it was told about. A property somebody edited in the console — an * inline policy, a bucket setting, a security-group rule — is invisible to it. * That gap is why go-to-k/cdk-real-drift exists, and it is what this reader * closes: the live resource model comes from the **Cloud Control API**, which * bypasses CloudFormation entirely and returns the resource as the service * actually holds it. * * Correlation is unchanged: Cloud Control is addressed by the physical id that * `describe-stack-resources` already reports per logical id, so the results * line up with the same IR node ids `live-attrs.ts` relies on. * * ## One reader, several sources (#1269) * * Cloud Control is the default source, not the only one. It has the property * this reader wants most — it returns the *CloudFormation resource model*, so * its payload lines up with the declared side without translation — but its * coverage is not the same as the provider's. A security group read through * Cloud Control comes back as identity and a description; read through * `ec2 describe-security-groups` it comes back with every ingress and egress * rule, which is the property people actually edit out of band. * * So each type names its source in {@link DEEP_SOURCES}, and a source that is * not Cloud Control supplies a `toModel` that maps the provider's shape onto * the CloudFormation one. Translation is the price of the richer read, and it * belongs next to the type it translates rather than inside the diff. * * A declared resource of a type with no source reports NOT-OBSERVED with * `unsupported-kind` — it may well exist, and saying nothing about it is the * only honest answer. * * ## Nothing here talks to real AWS on its own terms * * Every call goes through `./api/read-client.ts` — the applier's own transport, * pointed at the read APIs (#1206) — so `AWS_ENDPOINT_URL` redirects the whole * reader at a local emulator, with no CLI to spawn and typed failures instead of * parsed stderr. */ import type { DeepNormalizationHooks, DeepObservationResult } from "@intentius/chant/lexicon"; import { type AwsReadHttp } from "./api/read-client.js"; /** * Where each type's live model comes from. * * `cloud-control` is addressed by the physical id `describe-stack-resources` * already reports, and needs no translation. An `ec2` source names a bulk * describe — one call for every id of that type, not one per resource — plus * the mapping from the EC2 shape onto the CloudFormation model the declared * side is written in. */ export type DeepSource = { via: "cloud-control"; } | { via: "ec2"; /** Bulk describe argv, minus the ids and the region. */ argv: string[]; /** Flag the physical ids are passed under. */ idFlag: string; /** Top-level key of the result array. */ key: string; /** Field on each row carrying the physical id, for the join back. */ id: string; /** EC2 row -> CloudFormation resource model. */ toModel: (row: Record) => Record; }; export declare const DEEP_SOURCES: Record; /** * Types this reader can read live. Derived from {@link DEEP_SOURCES} so the two * cannot drift apart — the shape of bug #1280 is about. */ export declare const DEEP_READABLE_TYPES: ReadonlySet; /** * A child type this reader enumerates under one declared parent (#1015's * out-of-band case) — the console-added SNS subscription or inline role * policy that no property diff can see, because it is not a property of * anything declared: it is a whole resource CloudFormation never made. * * Cloud Control's `ListResources` takes the parent as a `ResourceModel` * scope, which is what `model` builds from the physical id the stack read * already resolved. Every listed child is then sorted into declared (it is * some stack resource's physical id, or some declared entity's spelled * identity) or out-of-band; the out-of-band ones ride into the observation * as live resources nobody declared, which the deep diff reports as * undeclared entities. */ export interface DeepChildSource { /** The child's CloudFormation type — what `ListResources` is asked for. */ childType: string; /** The `ResourceModel` scoping the listing to one parent. */ model: (parentPhysicalId: string) => Record; /** * Legacy declared forms of the same live child. `AWS::IAM::Policy` attached * to a role creates the same inline policy `AWS::IAM::RolePolicy` lists, but * under its own physical id — the policy name, which is the second part of * the RolePolicy identifier. Without this leg every legacy inline policy * reads as out-of-band on a stack that declared it. */ declaredVia?: Array<{ type: string; /** The part of the child identifier that carries that type's physical id. */ part: (identifier: string) => string | undefined; }>; } /** * Parent type → the child types worth listing under it. Scoped like * {@link DEEP_SOURCES}: the high-signal cases first — the issue's own * examples — and widened per type, not by wildcard. A child type Cloud * Control cannot list parent-scoped does not belong here. */ export declare const DEEP_CHILD_SOURCES: Record; /** * The name an out-of-band child reports under. There is no chant entity name * to use — nobody declared it — so the name has to carry the finding on its * own: what kind of thing, and which one. */ export declare function outOfBandChildName(childType: string, identifier: string): string; /** * `describe-security-groups` -> the `AWS::EC2::SecurityGroup` resource model. * * The two surfaces disagree about names and about nesting. EC2 says * `Description` where the template says `GroupDescription`, and nests rule * sources under `IpRanges[]` / `Ipv6Ranges[]` / `UserIdGroupPairs[]` where the * template writes one flat rule per source. `toIngressRules` already resolves * the nesting for the topology fold (#1273); this reuses it so there is one * translation of that shape in the lexicon, not two. * * Rules are matched by their whole canonical value (the `orderKey` hook below), * so a field the template carries and this mapping drops is not a smaller diff — * it is every rule reported twice, once absent and once undeclared. That is why * a range's own `Description` is carried through. */ export declare function securityGroupToModel(row: Record): Record; /** * Property names that are server-populated wherever they appear — identifiers * the service mints, timestamps it stamps, counters it maintains. Matched on * the final path segment, because AWS repeats these names at every nesting * depth and a per-type list of full paths would be a maintenance trap. * * Deliberately excludes ambiguous names like `Id` and `Name`: `VpcId` and * `BucketName` are declared inputs, and pruning a declared input is how a * normalization pass starts hiding real drift. */ export declare const AWS_READ_ONLY_NAMES: ReadonlySet; /** * Service defaults, per type, as index-erased property paths. A live value * equal to its default is subtracted **only when source never declared that * property** — cdk-real-drift's default subtraction, and the reason * {@link DeepNode.counterpart} exists. Declaring the default explicitly keeps * the property in the diff, so a later change to it still reports. */ export declare const AWS_SERVICE_DEFAULTS: Record>; /** * Names the service mints when a template does not supply one (#1269). * * Unlike {@link AWS_SERVICE_DEFAULTS} there is no fixed value to compare: a * CloudFormation-generated security-group name is a different random string on * every deploy. Pruned on the live side only, and only where source is silent — * a template that names the group keeps the property in the diff, so a later * rename still reports. */ export declare const AWS_GENERATED_NAMES: Record>; /** * The read-only properties of one type, as index-erased patterns, straight from * the CloudFormation schema's `readOnlyProperties` (#1641). * * The codegen already turns that list into each class's GetAtt attributes * (`attrs` in `lexicon-aws.json`), so this reads the same registry from the * other side: a path the schema says only the service can write is an * attribute, and an attribute the live read reports is not property drift. No * declaration can contain one, so ` -> value` is the shape every * clean apply would otherwise produce. `AWS::IAM::ManagedPolicy.PolicyArn`, * which is also the type's Cloud Control primary identifier, is the case that * surfaced it. * * The manifest spells an array element `Subscribers.*.Status`; the * normalization pass spells the same thing `Subscribers[].Status`. * * Complements {@link AWS_READ_ONLY_NAMES} rather than replacing it: the * name-based list also covers translated models (an EC2 row mapped onto the * CloudFormation shape) and nested documents the schema never enumerates. */ export declare function schemaReadOnlyPatterns(entityType: string): ReadonlySet; /** * The aws lexicon's noise rules. The three classes the epic names for AWS — * server-populated fields, unstable ordering (tags, policy statements), and * provider defaults — plus nothing else: a rule that is not one of those is a * rule that hides drift. */ export declare const awsDeepNormalizationHooks: DeepNormalizationHooks; /** One live resource as Cloud Control returns it. Exported for tests. */ export interface CloudControlResource { identifier: string; properties: Record; } /** True when the live property tree carries chant's ownership marker tag. */ export declare function hasOwnershipMarker(properties: Record): boolean; export interface AwsDeepObserveOptions { environment: string; entityNames: string[]; entities?: Map; }>; stack?: string; /** Region this stack is deployed in (#1267). Same reason the thin path takes * one (#1261): without it a multi-region estate reads every stack against the * ambient region, the out-of-region ones come back empty, and a deep snapshot * silently records no properties for them. */ region?: string; owned?: boolean; /** Injectable transport, mirroring `awsApply`'s `http` — tests reach the reader without a network. */ http?: AwsReadHttp; } /** * Read the live property tree for each declared entity via Cloud Control. * * Two reads per run plus one per readable resource: `DescribeStackResources` * resolves logical id → (type, physical id), then `GetResource` fetches each * model. The first read's failure modes are the thin path's, verbatim — a stack * that does not exist yet is a real absence (nothing is deployed, so there are * no properties to drift), anything else is a hole for every declared entity. * * The per-resource reads run concurrently (#1201/#1206). They were serial when * each one was a process spawn, which made a deep snapshot of a large stack * cost N round trips end to end. */ export declare function observeResourcesDeepAws(options: AwsDeepObserveOptions): Promise; //# sourceMappingURL=deep-observe.d.ts.map