Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
76 commits
Select commit Hold shift + click to select a range
deac816
chore(ui-deps): upgrade frontend dependencies, gated green
Jul 5, 2026
f959827
chore(deps): full go get -u ./... upgrade, gated green
Jul 5, 2026
6528c97
chore: sync beads tracker, drop obsolete spa/.keep placeholder
Jul 5, 2026
a41e91c
chore: untrack .claude/settings.local.json, add to gitignore
Jul 5, 2026
8aad822
chore: gitignore transient checkpoint.md
Jul 5, 2026
ae10a86
chore(ci): add go fix modernize gate, apply remaining modernization
Jul 5, 2026
708d196
fix(s3): AWS-parity fixes — SSE persistence, ACL/replication/presign …
Jul 5, 2026
c18fa9b
fix(ec2): tag every taggable resource type; real instance-attribute/l…
Jul 5, 2026
f459c9f
fix(dynamodb): close Select/COUNT, batch-write duplicate-key, and tra…
Jul 5, 2026
5e8140e
chore: restore dashboard/static/spa/.keep placeholder
Jul 5, 2026
8baa462
fix(sns): wire SNS->Lambda and SNS->Firehose delivery in composition …
Jul 5, 2026
c3b5d46
fix(lambda): RemovePermission wire-shape, ESM qualifier ARN parsing, …
Jul 5, 2026
71cd544
fix(iam): disguised stubs, HTTP status codes, policy-doc encoding, pe…
Jul 5, 2026
58e50f3
docs(parity): add per-service PARITY.md manifests + schema template
Jul 5, 2026
647a8ce
fix(sns): PublishBatch attribute wire-shape, signed-envelope, replay …
Jul 5, 2026
17a215e
fix(sqs): duplicate XML decl, FIFO requeue ordering, RedriveAllowPoli…
Jul 5, 2026
58eec06
fix(cloudwatchlogs): drop deprecated seq-token validation, metric-fil…
Jul 5, 2026
ede7169
fix(cloudwatch): PutMetricData all-or-nothing, Values/Counts arrays, …
Jul 5, 2026
6548cf8
fix(kms): GrantTokens on 8 grant-valid ops, error class, grant-index …
Jul 5, 2026
86c2f9a
fix(cloudformation): idempotent DeleteStack, export-in-use enforcemen…
Jul 5, 2026
f2750d4
fix(ecs): rebuild serviceIndex on restore, persist resourceTags, real…
Jul 5, 2026
fba3c78
fix(cli): match cwBk.PutMetricData new single-return signature
Jul 5, 2026
23de3aa
fix(ecr): layer-upload FK/sequencing validation, PutImage digest veri…
Jul 5, 2026
f093a92
fix(rds): delete final-snapshot contract, DescribeDBInstances filters…
Jul 5, 2026
647d201
fix(secretsmanager): data race on lazy store, wire-field names, versi…
Jul 5, 2026
0b1194b
fix(ssm): parameter tier/version/hierarchy rules, document wire-shape…
Jul 5, 2026
e069888
fix(apigatewayv2): per-protocol integration timeout, stage-tag 404, m…
Jul 5, 2026
f222f37
fix(apigateway): rewrite PATCH semantics — multi-segment paths, value…
Jul 5, 2026
e5a9ac6
fix(kinesis): tag persistence data-loss, PutRecords not-found, ON_DEM…
Jul 5, 2026
9f33680
fix(stepfunctions): Map failure swallowing, empty history events, EXP…
Jul 5, 2026
d0ebe97
fix(eventbridge): target-type param structs, cron day-of-week, PutEve…
Jul 5, 2026
d118e0d
fix(autoscaling): revive dead lifecycle-hook timers, LaunchInstances …
Jul 5, 2026
017fc20
fix(elbv2): trust-store revocation wire shape, 400 error status, ALPN…
Jul 5, 2026
e5205db
fix(route53): tag routing/persistence, CallerReference idempotency, e…
Jul 5, 2026
e783037
fix(cloudfront): InconsistentQuantities validation, per-resource Alre…
Jul 5, 2026
7d0baf7
fix(elasticache): systematic error code/status, unreached sentinel, S…
Jul 5, 2026
a40e7cc
fix(cognitoidp): real TOTP MFA (was accept-any-code stub), user-exist…
Jul 5, 2026
0407b38
fix(ses): void-result XML wire shape, sending-paused/quota enforcemen…
Jul 5, 2026
704d7cd
fix(sts): TradeInToken expiry validation, signing algorithms, GetWebI…
Jul 5, 2026
7d16709
fix(glue): BatchGetPartition stub, error codes, orphan partitions, dr…
Jul 5, 2026
fb339b3
feat(store): generic typed datalayer store (Phase 3.1)
Jul 5, 2026
0f09d77
refactor(sqs): convert backend maps to pkgs/store (Phase 3.2 pilot)
Jul 5, 2026
12e611a
refactor(ec2): convert 147 backend maps to pkgs/store (Phase 3.3)
Jul 6, 2026
f2e87b4
refactor(sagemaker): convert 70 region-nested backend maps to pkgs/st…
Jul 6, 2026
1ffc93d
refactor(cloudformation): convert backend maps to pkgs/store (Phase 3.3)
Jul 6, 2026
2472bb7
refactor(dynamodb): convert 7 backend maps to pkgs/store (Phase 3.3)
Jul 6, 2026
496c52c
refactor(s3): convert backend maps to pkgs/store (Phase 3.3)
Jul 6, 2026
031a438
refactor(lambda): convert 9 backend maps to pkgs/store (Phase 3.3)
Jul 6, 2026
91c888d
refactor(iam): convert 14 backend maps to pkgs/store (Phase 3.3)
Jul 6, 2026
f5b6ec3
refactor(glue): convert 37 backend maps to pkgs/store (Phase 3.3)
Jul 6, 2026
6db6e2a
refactor(ecs): convert 15 backend maps to pkgs/store (Phase 3.3)
Jul 6, 2026
4179a2f
refactor(rds): convert 26 backend maps to pkgs/store (Phase 3.3)
Jul 6, 2026
4c68677
refactor(ssm): convert 18 backend maps to pkgs/store (Phase 3.3)
Jul 6, 2026
58b7152
refactor(iot): convert 35 backend maps to pkgs/store (Phase 3.3)
Jul 6, 2026
e7dbfa8
refactor(quicksight): convert 27 backend maps to pkgs/store (Phase 3.3)
Jul 6, 2026
6da0334
refactor(apigateway): convert 18 backend maps to pkgs/store (Phase 3.3)
Jul 6, 2026
be6963d
refactor(cognitoidp): convert 12 backend maps to pkgs/store + elimina…
Jul 6, 2026
490d797
refactor(cloudfront): convert 23 backend maps to pkgs/store (Phase 3.3)
Jul 6, 2026
82f8d5c
refactor(redshift): convert 29 backend maps to pkgs/store (Phase 3.3)
Jul 6, 2026
e12611d
refactor(cloudwatch): convert 8 backend maps to pkgs/store (Phase 3.3)
Jul 6, 2026
3506632
refactor(kms): convert 4 backend maps to pkgs/store + collapse grant …
Jul 6, 2026
3297f38
refactor(cloudwatchlogs): convert 26 backend maps to pkgs/store (Phas…
Jul 6, 2026
9e5d75b
refactor(bedrock): convert 34 backend maps to pkgs/store (Phase 3.3)
Jul 6, 2026
7a7bc74
refactor(pinpoint): convert 15 backend maps to pkgs/store (Phase 3.3)
Jul 6, 2026
b1a4815
refactor(eventbridge): convert 13 backend maps to pkgs/store (Phase 3.3)
Jul 6, 2026
0680631
refactor(elasticache): convert 12 backend maps to pkgs/store (Phase 3.3)
Jul 6, 2026
58a799d
refactor(sns): convert 5 backend maps to pkgs/store (Phase 3.3)
Jul 6, 2026
9c8d209
refactor(elbv2): convert 5 backend maps to pkgs/store (Phase 3.3)
Jul 6, 2026
870feb9
refactor(appsync): convert 12 backend maps to pkgs/store (Phase 3.3)
Jul 6, 2026
3a8aa26
refactor(apigatewayv2): convert 17 backend maps to pkgs/store (Phase …
Jul 6, 2026
6b78072
refactor(opensearch): convert 17 backend maps to pkgs/store (Phase 3.3)
Jul 6, 2026
848e055
refactor(memorydb): convert 10 backend maps to pkgs/store (Phase 3.3)
Jul 6, 2026
7cc61d2
refactor(ecr): convert 10 backend maps to pkgs/store (Phase 3.3)
Jul 6, 2026
df4bc6c
refactor(route53): convert 8 backend maps to pkgs/store (Phase 3.3)
Jul 6, 2026
328c34f
refactor(kinesis): convert streams map to pkgs/store (Phase 3.3)
Jul 6, 2026
de91ba9
refactor(secretsmanager): convert secrets map to pkgs/store (Phase 3.3)
Jul 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
130 changes: 129 additions & 1 deletion .beads/issues.jsonl

Large diffs are not rendered by default.

54 changes: 0 additions & 54 deletions .claude/settings.local.json

This file was deleted.

18 changes: 18 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,24 @@ jobs:
- name: golangci-lint
uses: golangci/golangci-lint-action@v9

modernize:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
token: ${{ env.GH_CI_TOKEN }}
ref: ${{ github.event.workflow_run.head_sha || github.sha }}

- name: Set up Go
uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true
cache: true

- name: go fix (modernize)
run: go fix -diff ./...

govulncheck:
runs-on: ubuntu-latest
steps:
Expand Down
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,9 @@ lambda.zip
.terraform.lock.hcl
.terraform.lock.hcl
/releaser

# personal local Claude Code settings (never sync bypass perms to origin)
.claude/settings.local.json

# session resume checkpoint (transient)
checkpoint.md
56 changes: 54 additions & 2 deletions cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -2584,6 +2584,10 @@
// Wire SNS→SQS delivery: when SNS publishes a message, deliver it to SQS queues.
wireSNSToSQS(byName["SNS"], byName["SQS"])

// Wire SNS→Lambda and SNS→Firehose subscription delivery, plus the SQS sender
// used for Lambda/Firehose subscription DLQ redelivery.
wireSNSToLambdaFirehose(byName["SNS"], byName["Lambda"], byName["Firehose"], byName["SQS"])

// Wire SQS → CloudWatch metric emission for NumberOfMessagesSent/Received/Deleted.
wireSQSMetrics(byName["SQS"], byName["CloudWatch"])

Expand Down Expand Up @@ -3145,6 +3149,54 @@
sqsBk.SubscribeToSNS(emitter)
}

// wireSNSToLambdaFirehose connects the SNS backend to Lambda and Firehose so that
// Lambda- and Firehose-protocol subscriptions actually receive published messages,
// and wires an SQS sender so failed Lambda/Firehose deliveries with a RedrivePolicy
// land in the subscription's dead-letter queue. This is independent of wireSNSToSQS:
// that function wires the SNS→SQS *subscription* delivery path via a publish emitter,
// while the SQS sender wired here only serves DLQ redelivery on failed Lambda/Firehose
// invocations (SNS backend's sqsSender field), so there is no overlap or double-wiring.
func wireSNSToLambdaFirehose(snsReg, lambdaReg, firehoseReg, sqsReg service.Registerable) {
snsH, ok := snsReg.(*snsbackend.Handler)
if !ok {
return
}

snsBk, ok := snsH.Backend.(*snsbackend.InMemoryBackend)
if !ok {
return
}

if lambdaH, lambdaOk := lambdaReg.(*lambdabackend.Handler); lambdaOk {
if lambdaBk, bkOk := lambdaH.Backend.(*lambdabackend.InMemoryBackend); bkOk {
snsBk.SetLambdaBackend(lambdaBk)
}
}

if firehoseH, firehoseOk := firehoseReg.(*firehosebackend.Handler); firehoseOk {
if firehoseBk, bkOk := firehoseH.Backend.(*firehosebackend.InMemoryBackend); bkOk {
snsBk.SetFirehoseBackend(&snsFirehosePutterAdapter{backend: firehoseBk})
}
}

if sqsH, sqsOk := sqsReg.(*sqsbackend.Handler); sqsOk {
if sqsBk, bkOk := sqsH.Backend.(*sqsbackend.InMemoryBackend); bkOk {
snsBk.SetSQSSender(&sqsSenderAdapter{backend: sqsBk})
}
}
}

// snsFirehosePutterAdapter adapts the Firehose backend to the sns.FirehosePutter
// interface, which omits the context parameter that the Firehose backend's
// PutRecordBatch requires.
type snsFirehosePutterAdapter struct {
backend *firehosebackend.InMemoryBackend
}

func (a *snsFirehosePutterAdapter) PutRecordBatch(streamName string, records [][]byte) (int, error) {
return a.backend.PutRecordBatch(context.Background(), streamName, records)
}

// wireSQSMetrics wires the CloudWatch metric emitter into the SQS backend so that
// SendMessage, ReceiveMessage, and DeleteMessage operations emit CloudWatch metrics.
func wireSQSMetrics(sqsReg, cwReg service.Registerable) {
Expand All @@ -3165,7 +3217,7 @@
sqsBk.SetMetricEmitter(
sqsbackend.MetricEmitterFunc(
func(namespace, name string, value float64, unit string) error {
_, err := cwBk.PutMetricData(namespace, []cwbackend.MetricDatum{
err := cwBk.PutMetricData(namespace, []cwbackend.MetricDatum{
{
MetricName: name,
Value: value,
Expand Down Expand Up @@ -3309,7 +3361,7 @@
ctx context.Context,
source, detailType, detail string,
) {
a.backend.PutEvents(ctx, []ebbackend.EventEntry{

Check failure on line 3364 in cli.go

View workflow job for this annotation

GitHub Actions / lint

Error return value of `a.backend.PutEvents` is not checked (errcheck)
{Source: source, DetailType: detailType, Detail: detail},
})
}
Expand Down Expand Up @@ -4204,7 +4256,7 @@
cwlogsBk.SetMetricEmitter(
cwlogsbackend.MetricEmitterFunc(
func(namespace, name string, value float64, unit string) error {
_, err := cwBk.PutMetricData(namespace, []cwbackend.MetricDatum{
err := cwBk.PutMetricData(namespace, []cwbackend.MetricDatum{
{
MetricName: name,
Namespace: namespace,
Expand Down
143 changes: 143 additions & 0 deletions cli_sns_wiring_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package main

import (
"context"
"encoding/json"
"io"
"testing"

"github.com/aws/aws-sdk-go-v2/aws"
s3sdk "github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/blackbirdworks/gopherstack/pkgs/arn"
"github.com/blackbirdworks/gopherstack/pkgs/config"
firehosebackend "github.com/blackbirdworks/gopherstack/services/firehose"
lambdabackend "github.com/blackbirdworks/gopherstack/services/lambda"
snsbackend "github.com/blackbirdworks/gopherstack/services/sns"
sqsbackend "github.com/blackbirdworks/gopherstack/services/sqs"
)

// wiringMockS3Storer implements firehose.S3Storer so the test can observe that a
// record delivered through the SNS→Firehose path actually reached S3.
type wiringMockS3Storer struct {
calls []wiringS3PutCall
}

type wiringS3PutCall struct {
bucket string
key string
body []byte
}

func (m *wiringMockS3Storer) PutObject(
_ context.Context,
input *s3sdk.PutObjectInput,
) (*s3sdk.PutObjectOutput, error) {
body, _ := io.ReadAll(input.Body)
m.calls = append(m.calls, wiringS3PutCall{
bucket: aws.ToString(input.Bucket),
key: aws.ToString(input.Key),
body: body,
})

return &s3sdk.PutObjectOutput{}, nil
}

// TestWireSNSToLambdaFirehose_EndToEndDelivery exercises the exact composition-root
// wiring used by cli.go's setup (wireSNSToLambdaFirehose) against REAL Lambda, Firehose,
// and SQS backends — not test doubles — to prove that subscribing a Lambda function or
// Firehose delivery stream to an SNS topic and publishing actually delivers, instead of
// silently no-opping as it did before SetLambdaBackend/SetFirehoseBackend/SetSQSSender
// were wired in the running binary.
func TestWireSNSToLambdaFirehose_EndToEndDelivery(t *testing.T) {
t.Parallel()

snsBk := snsbackend.NewInMemoryBackend()
snsH := snsbackend.NewHandler(snsBk)

lambdaBk := lambdabackend.NewInMemoryBackend(
nil, nil, lambdabackend.DefaultSettings(), config.DefaultAccountID, config.DefaultRegion,
)
lambdaH := lambdabackend.NewHandler(lambdaBk)

firehoseBk := firehosebackend.NewInMemoryBackend(config.DefaultAccountID, config.DefaultRegion)
firehoseH := firehosebackend.NewHandler(firehoseBk)

sqsBk := sqsbackend.NewInMemoryBackend()
sqsH := sqsbackend.NewHandler(sqsBk)

// This is the exact call cli.go's setupServices makes.
wireSNSToLambdaFirehose(snsH, lambdaH, firehoseH, sqsH)

ctx := context.Background()

// --- SNS -> Firehose: publish must reach the real Firehose backend and flush to S3. ---
s3mock := &wiringMockS3Storer{}
firehoseBk.SetS3Backend(s3mock)

_, err := firehoseBk.CreateDeliveryStream(ctx, firehosebackend.CreateDeliveryStreamInput{
Name: "wiring-test-stream",
S3Destination: &firehosebackend.S3DestinationDescription{
BucketARN: "arn:aws:s3:::wiring-test-bucket",
BufferingHints: &firehosebackend.BufferingHints{
SizeInMBs: 128,
IntervalInSeconds: 900,
},
},
})
require.NoError(t, err)

firehoseTopic, err := snsBk.CreateTopic("firehose-topic", nil)
require.NoError(t, err)

firehoseStreamARN := arn.Build(
"firehose", config.DefaultRegion, config.DefaultAccountID, "deliverystream/wiring-test-stream",
)
_, err = snsBk.Subscribe(firehoseTopic.TopicArn, "firehose", firehoseStreamARN, "")
require.NoError(t, err)

_, err = snsBk.Publish(firehoseTopic.TopicArn, "hello-firehose", "", "", nil)
require.NoError(t, err)

firehoseBk.FlushAll(ctx)

require.Len(t, s3mock.calls, 1,
"SNS publish must have been delivered to the real Firehose backend via cli.go's wireSNSToLambdaFirehose")
assert.Contains(t, string(s3mock.calls[0].body), "hello-firehose")

// --- SNS -> Lambda with DLQ: publish must reach the real Lambda backend. There is
// no Docker runtime available in this test, so the invocation fails deterministically
// (a real failure mode, not a stub) — proving delivery requires the failed message to
// be redirected to the real SQS DLQ via the wired SQS sender. ---
dlqOut, err := sqsBk.CreateQueue(&sqsbackend.CreateQueueInput{QueueName: "lambda-dlq"})
require.NoError(t, err)

lambdaTopic, err := snsBk.CreateTopic("lambda-topic", nil)
require.NoError(t, err)

functionARN := arn.Build(
"lambda", config.DefaultRegion, config.DefaultAccountID, "function:wiring-test-fn",
)
sub, err := snsBk.Subscribe(lambdaTopic.TopicArn, "lambda", functionARN, "")
require.NoError(t, err)

dlqARN := arn.Build("sqs", config.DefaultRegion, config.DefaultAccountID, "lambda-dlq")
redrivePolicy, err := json.Marshal(map[string]string{"deadLetterTargetArn": dlqARN})
require.NoError(t, err)
require.NoError(t, snsBk.SetSubscriptionAttributes(sub.SubscriptionArn, "RedrivePolicy", string(redrivePolicy)))

_, err = snsBk.Publish(lambdaTopic.TopicArn, "hello-lambda", "", "", nil)
require.NoError(t, err)

recvOut, err := sqsBk.ReceiveMessage(&sqsbackend.ReceiveMessageInput{
QueueURL: dlqOut.QueueURL,
MaxNumberOfMessages: 1,
})
require.NoError(t, err)
require.Len(t, recvOut.Messages, 1,
"failed Lambda delivery must have been redirected to the real SQS DLQ via cli.go's wireSNSToLambdaFirehose "+
"(proves both SetLambdaBackend and SetSQSSender were wired)")
assert.Equal(t, "hello-lambda", recvOut.Messages[0].Body)
}
Loading
Loading