Skip to content

feat(aws): storage.rds DatabaseInstance — instance.ts + attach() protocol [stacked on #146] - #147

Open
so0k wants to merge 1 commit into
rds-engine-tablesfrom
rds-instance
Open

feat(aws): storage.rds DatabaseInstance — instance.ts + attach() protocol [stacked on #146]#147
so0k wants to merge 1 commit into
rds-engine-tablesfrom
rds-instance

Conversation

@so0k

@so0k so0k commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Storage slice PR 4/N — storage.rds DatabaseInstance (v2.263.0)

Stacked on #146. Lands instance.ts (the 1,708-LOC upstream file) with the full ported test suite: DatabaseInstance, DatabaseInstanceFromSnapshot, DatabaseInstanceReadReplica, plus instance-from-snapshot tests — 398 rds-suite tests green.

The attach() protocol ships for real

DatabaseInstanceBase now implements ISecretAttachmentTarget properly — the reference implementation replacing the TEST-ONLY adapters in integ/aws/encryption/apps/*. asSecretAttachmentTarget() contributes engine/host/port/dbInstanceIdentifier (+ dbname from DatabaseInstanceSource) as connectionFields that Secret.attach() merges into the secret value — including dbInstanceIdentifier for CloudFormation SecretTargetAttachment parity. encryption/secret.ts's interface docs updated to point at it.

Key deviations (all documented in-code)

  • Generated-password plumbing: upstream feeds the secret's password into the instance via a CFN dynamic reference (secretValueFromJson('password').unsafeUnwrap()), which has no Terraform equivalent. Here Secret._generatedPassword (new @internal getter) surfaces the same aws_secretsmanager_random_password token so the instance's password argument and the stored secret can never diverge — and both sides freeze it (lifecycle.ignore_changes on password + secret_string; single accumulated list per resource, mirroring Secret.toTerraform()).
  • removalPolicy → Terraform-native: skipFinalSnapshot / finalSnapshotIdentifier / deletionProtection props. The provider default (skip_final_snapshot: false with no identifier) makes terraform destroy fail at apply time — unlike CFN, which auto-names the snapshot — so the constructor emits a synth-time Annotations warning when neither prop is set.
  • manageMasterUserPassword: instance.secret is a Secret.fromSecretAttributes wrapper over the RDS-managed master_user_secret computed block; grantRead() renders the IAM statement + kms:Decrypt with the kms:ViaService condition (fully asserted in tests).
  • Credentials.fromSecret (existing-secret credentials) is TODO-omitted (needs secretValueFromJson); hand-built Credentials literals with a secret are rejected with a ValidationError rather than silently deploying an unset password.
  • fromLookup (ContextProvider) TODO-omitted; Lambda-backed per-export log retention TODO-omitted (native enabled_cloudwatch_logs_exports is wired).

Verification

  • Conversion workflow: 13 agents, prop-by-prop mapping ledger against the provider's db-instance config (every DatabaseInstanceProps member maps, throws, or is TODO-commented — audited by gates and the adversarial verifier; spot-checks included sentinel-default and token-in-string hazard classes).
  • Live integ (make rds.instance): a real Postgres 16 db.t3.micro deployed through the L2 in an isolated VPC with Credentials.fromGeneratedSecret — validates instance read-back (status/engine/class/endpoint), the attached secret's merged connection fields including dbInstanceIdentifier, the post-apply drift oracle (proving the double ignore_changes design), and clean destroy via skipFinalSnapshot. Receipts in thread.

RDS PR 2c: DatabaseInstance / DatabaseInstanceFromSnapshot /
DatabaseInstanceReadReplica with full upstream test suite (398 rds tests).
attach() protocol reference implementation on DatabaseInstanceBase
(engine/host/port/dbname/dbInstanceIdentifier — CFN SecretTargetAttachment
parity; port intentionally a JSON number as CFN writes it).
Secret._generatedPassword internal getter feeds the SAME random-password
token into aws_db_instance.password and the secret version, both frozen via
accumulated lifecycle.ignore_changes. removalPolicy → skipFinalSnapshot/
finalSnapshotIdentifier/deletionProtection with synth-time Annotations
warning (provider default fails destroy). fromLookup + Credentials.fromSecret
TODO-omitted. Live integ rds.instance: real Postgres 16 db.t3.micro.
@so0k

so0k commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

Live apply/verify/destroy receipts — make rds.instance (aws-vault → tcons-vincent, us-east-1)

Run 2 (final) — PASS 589.90s:

TestRdsInstance 20:43:02 Apply complete! Resources: 12 added, 0 changed, 0 destroyed.
rds_instance_test.go:39: rdsinstancedatabased6dd4e9e available (postgres db.t3.micro at rdsinstancedatabased6dd4e9e....rds.amazonaws.com:5432)
rds_instance_test.go:60: attached secret carries full connection details incl. dbInstanceIdentifier=rdsinstancedatabased6dd4e9e
(drift oracle: tofu plan -detailed-exitcode == 0 — silent = clean)
TestRdsInstance 20:47:26 Destroy complete! Resources: 12 destroyed.   <-- skipFinalSnapshot honored, clean teardown
--- PASS: TestRdsInstance (589.90s)

What this proves live:

  • The L2 deploys a real Postgres 16 db.t3.micro (isolated VPC, auto-created gridUUID-named subnet group) and reads back with the expected engine/class/endpoint.
  • The attach() protocol's shipped reference implementation merges engine/host/port (as a JSON NUMBER — CFN SecretTargetAttachment parity)/dbname/dbInstanceIdentifier into the generated DatabaseSecret. Run 1 'failed' only in the Go validator, which had assumed string-typed port — the construct was right, the validator now locks the number-typed parity in.
  • The generated-password double-freeze design (_generatedPassword token into aws_db_instance.password + secret version, both under ignore_changes) survives the post-apply drift oracle — zero drift.
  • Destroy is clean with skipFinalSnapshot: true (and the synth-time Annotations warning fires when neither snapshot prop is set — the provider default would otherwise fail destroy, unlike CFN's auto-named snapshots).

Fixture note: the auto-generated DatabaseSecret has a deterministic name and no recovery-window override — re-runs within 30 days of a destroy need aws secretsmanager delete-secret --force-delete-without-recovery first (documented in the app header).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant