Module: PlanMyStuff::AwsSnsSimulator

Defined in:
lib/plan_my_stuff/aws_sns_simulator.rb

Overview

Dev helper: build a fake SNS notification wrapping an ECS Deployment State Change event and POST it to a local /webhooks/aws endpoint. Used by plan_my_stuff:webhooks:simulate_aws so deployment flows can be exercised without waiting for a real AWS deployment.

The consuming app must be configured with a no-op SNS verifier (see PlanMyStuff::NullSnsVerifier) or signature verification will reject the simulated envelope. Never enable that verifier in production.

Constant Summary collapse

DEFAULT_EVENT =
'SERVICE_DEPLOYMENT_COMPLETED'

Class Method Summary collapse

Class Method Details

.build_ecs_message(event_name:, service_arn:, commit_sha:) ⇒ Hash

Returns:

  • (Hash)


71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/plan_my_stuff/aws_sns_simulator.rb', line 71

def build_ecs_message(event_name:, service_arn:, commit_sha:)
  detail = {
    'eventType' => 'INFO',
    'eventName' => event_name,
    'deploymentId' => "ecs-svc/#{SecureRandom.hex(8)}",
    'updatedAt' => Time.now.utc.iso8601,
  }
  detail['commitSha'] = commit_sha if commit_sha

  {
    'version' => '0',
    'id' => SecureRandom.uuid,
    'detail-type' => 'ECS Deployment State Change',
    'source' => 'aws.ecs',
    'account' => '000000000000',
    'time' => Time.now.utc.iso8601,
    'region' => 'us-east-1',
    'resources' => [service_arn],
    'detail' => detail,
  }
end

.build_sns_envelope(message:, topic_arn:) ⇒ Hash

Returns:

  • (Hash)


94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/plan_my_stuff/aws_sns_simulator.rb', line 94

def build_sns_envelope(message:, topic_arn:)
  now = Time.now.utc.iso8601
  {
    'Type' => 'Notification',
    'MessageId' => SecureRandom.uuid,
    'TopicArn' => topic_arn,
    'Subject' => 'ECS Deployment State Change',
    'Message' => JSON.generate(message),
    'Timestamp' => now,
    'SignatureVersion' => '1',
    'Signature' => 'simulated',
    'SigningCertURL' => 'https://example.com/cert.pem',
    'UnsubscribeURL' => 'https://example.com/unsubscribe',
  }
end

.post(endpoint_url:, event_name: DEFAULT_EVENT) ⇒ Net::HTTPResponse

POSTs a simulated SNS envelope to the endpoint. Pulls sns_topic_arn, aws_service_identifier, and production_commit_sha from PlanMyStuff.configuration (this task is dev-only, so there’s no reason to parameterize them).

Parameters:

  • endpoint_url (String)

    full URL including path

  • event_name (String) (defaults to: DEFAULT_EVENT)

    ECS eventName (default completed)

Returns:

  • (Net::HTTPResponse)


35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/plan_my_stuff/aws_sns_simulator.rb', line 35

def post(endpoint_url:, event_name: DEFAULT_EVENT)
  config = PlanMyStuff.configuration
  raise('PlanMyStuff.configuration.sns_topic_arn is blank') if config.sns_topic_arn.blank?
  raise('PlanMyStuff.configuration.aws_service_identifier is blank') if config.aws_service_identifier.blank?

  topic_arn = config.sns_topic_arn
  service_arn = "arn:aws:ecs:us-east-1:000000000000:service/simulated-cluster/#{config.aws_service_identifier}"
  commit_sha = config.production_commit_sha

  message = build_ecs_message(event_name: event_name, service_arn: service_arn, commit_sha: commit_sha)
  envelope = build_sns_envelope(message: message, topic_arn: topic_arn)
  raw_body = JSON.generate(envelope)

  uri = URI(endpoint_url)
  http = Net::HTTP.new(uri.host, uri.port)
  http.use_ssl = uri.scheme == 'https'

  request = Net::HTTP::Post.new(uri.request_uri)
  request['Content-Type'] = 'application/json'
  request['x-amz-sns-message-type'] = 'Notification'
  request.body = raw_body

  $stdout.puts("POST #{endpoint_url}")
  $stdout.puts("  event:     #{event_name}")
  $stdout.puts("  topic:     #{topic_arn}")
  $stdout.puts("  resources: #{service_arn}")
  $stdout.puts("  commit:    #{commit_sha || '(none)'}")
  $stdout.puts('---')

  response = http.request(request)
  $stdout.puts("HTTP #{response.code} #{response.message}")
  $stdout.puts(response.body) if response.body.present?
  response
end