diff --git a/spinifex/handlers/rds/create.go b/spinifex/handlers/rds/create.go index 79b278433..b68e0bfb4 100644 --- a/spinifex/handlers/rds/create.go +++ b/spinifex/handlers/rds/create.go @@ -127,6 +127,8 @@ func (s *Service) CreateDBInstance(ctx context.Context, input *rds.CreateDBInsta // gates connectivity until then. s.publishDNS(ctx, accountID, stored, handlers_dns.ActionUpsert) + s.RecordEvent(ctx, accountID, EventSourceTypeDBInstance, req.Identifier, + "DB instance created.", EventCategoryCreation) slog.InfoContext(ctx, "rds: DB instance created", "dbInstance", req.Identifier, "accountId", accountID, "instanceId", launched.InstanceID, "endpoint", stored.EndpointAddress, "class", req.InstanceClass) diff --git a/spinifex/handlers/rds/create_test.go b/spinifex/handlers/rds/create_test.go index 48cf3a75e..9664b95f1 100644 --- a/spinifex/handlers/rds/create_test.go +++ b/spinifex/handlers/rds/create_test.go @@ -253,6 +253,14 @@ func TestCreateDBInstance_ProvisionsAndRecordsTheInstance(t *testing.T) { assert.Equal(t, "Sup3rSecret!", rec.Bootstrap.MasterUserPassword) assert.False(t, rec.Bootstrap.Consumed) + events := describeEvents(t, h.svc, &rds.DescribeEventsInput{ + SourceType: aws.String(EventSourceTypeDBInstance), + SourceIdentifier: aws.String(testDBInstanceID), + }) + require.Len(t, events, 1) + assert.Equal(t, "DB instance created.", aws.StringValue(events[0].Message)) + assert.Equal(t, []string{EventCategoryCreation}, aws.StringValueSlice(events[0].EventCategories)) + // The placement is the account's default VPC and its lowest-sorted subnet, // with the VPC's default security group when the request names none. assert.Equal(t, testDefaultVPC, rec.VpcID) diff --git a/spinifex/handlers/rds/reconciler.go b/spinifex/handlers/rds/reconciler.go index aecff6095..85b07196d 100644 --- a/spinifex/handlers/rds/reconciler.go +++ b/spinifex/handlers/rds/reconciler.go @@ -652,6 +652,10 @@ func (r *Reconciler) transition(ctx context.Context, kv jetstream.KeyValue, rev } slog.InfoContext(ctx, "rds reconciler: DB instance transitioned", "dbInstance", rec.DBInstanceIdentifier, "from", from, "to", to, "reason", reason) + if from == StatusCreating && to == StatusAvailable { + r.svc.RecordEvent(ctx, AccountIDFromBucketName(kv.Bucket()), EventSourceTypeDBInstance, + rec.DBInstanceIdentifier, "DB instance is available.", EventCategoryAvailability) + } return nil } diff --git a/spinifex/handlers/rds/reconciler_test.go b/spinifex/handlers/rds/reconciler_test.go index 6824b2aa1..b158344b5 100644 --- a/spinifex/handlers/rds/reconciler_test.go +++ b/spinifex/handlers/rds/reconciler_test.go @@ -6,6 +6,8 @@ import ( "testing" "time" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/rds" "github.com/mulgadc/spinifex/spinifex/testutil" "github.com/nats-io/nats.go" "github.com/stretchr/testify/assert" @@ -96,6 +98,14 @@ func TestReconciler_MarksAvailableOnHealthyHeartbeatFromARunningVM(t *testing.T) assert.Equal(t, StatusAvailable, status) assert.Empty(t, reason) assert.Equal(t, []string{testInstance}, h.state.calls) + + events := describeEvents(t, h.svc, &rds.DescribeEventsInput{ + SourceType: aws.String(EventSourceTypeDBInstance), + SourceIdentifier: aws.String(testDBID), + }) + require.Len(t, events, 1) + assert.Equal(t, "DB instance is available.", aws.StringValue(events[0].Message)) + assert.Equal(t, []string{EventCategoryAvailability}, aws.StringValueSlice(events[0].EventCategories)) } // Both halves must hold. A healthy beat from a VM that is not running means the diff --git a/tests/e2e/rds/create_describe_test.go b/tests/e2e/rds/create_describe_test.go index 534135331..5e9c0f39b 100644 --- a/tests/e2e/rds/create_describe_test.go +++ b/tests/e2e/rds/create_describe_test.go @@ -59,6 +59,28 @@ func TestCreateDescribe(t *testing.T) { assert.Equal(t, int64(5432), aws.Int64Value(instance.Endpoint.Port)) }) + t.Run("RecordsCreationAndAvailabilityEvents", func(t *testing.T) { + requireAvailable(t, instance) + out, err := f.AWS.RDS.DescribeEvents(&rds.DescribeEventsInput{ + SourceType: aws.String("db-instance"), + SourceIdentifier: aws.String(id), + Duration: aws.Int64(1440), + }) + require.NoError(t, err, "describe-events") + require.NotEmpty(t, out.Events, "a freshly created instance must have an event history") + + eventsByMessage := make(map[string]*rds.Event, len(out.Events)) + for _, event := range out.Events { + eventsByMessage[aws.StringValue(event.Message)] = event + } + createdEvent := eventsByMessage["DB instance created."] + require.NotNil(t, createdEvent) + assert.Contains(t, aws.StringValueSlice(createdEvent.EventCategories), "creation") + availableEvent := eventsByMessage["DB instance is available."] + require.NotNil(t, availableEvent) + assert.Contains(t, aws.StringValueSlice(availableEvent.EventCategories), "availability") + }) + t.Run("AppearsInTheFleetListing", func(t *testing.T) { requireAvailable(t, instance) list, err := f.AWS.RDS.DescribeDBInstances(&rds.DescribeDBInstancesInput{})