foobara-aws 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +28 -0
- data/README.md +42 -17
- data/lib/foobara/aws/cdk/service.rb +62 -5
- data/lib/foobara/aws/event_handler.rb +89 -0
- data/lib/foobara/aws/packager.rb +114 -2
- data/lib/foobara/aws/plan.rb +76 -20
- data/lib/foobara/aws/sqs_connector.rb +120 -0
- data/lib/foobara/aws/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3905338908c79d15be8eea5643596893e7604a35d91b78760de209f06822c230
|
|
4
|
+
data.tar.gz: 6a771a30dcc41281ee879b80c2536a6f63bf7a9755846be1685ab1d474034ba5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cfba5032f0d68511918ed2e2fca46bdf3842bca216c5361fcda23b1360cd6ea8708793ebf7d5063ae68d10d3e738d87337624a7a0c3d11fc8b598a65d3965107
|
|
7
|
+
data.tar.gz: 0fda844ba145042a870a580ef2c445fb8d5e5359866c0134c70a7fea0e67a58326cfbbf77ea6f837af7f2e1dcb64b30861278de84738ed75a64a5c7b85d478b8
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.4.0] - 2026-08-05
|
|
4
|
+
|
|
5
|
+
- `Foobara::AWS::SQSConnector` and `Foobara::AWS::EventHandler` — commands invoked by a queue rather than by HTTP.
|
|
6
|
+
Modelled on foobara-resque-connector, and using the same envelope, because a background job is not a special
|
|
7
|
+
kind of object: it is a command reached by a different transport. `connect` generates a `<Command>Async` that
|
|
8
|
+
enqueues, so a malformed job is refused where it is created rather than by a worker later.
|
|
9
|
+
- With no queue URL the connector runs the command inline, so local development and test suites need no SQS and
|
|
10
|
+
no fake. The queue is a deployment concern.
|
|
11
|
+
- `EventHandler` returns `batchItemFailures`, so one poison record does not fail its nine neighbours. `Service`
|
|
12
|
+
creates the event source mapping with `report_batch_item_failures: true`, without which that return value is
|
|
13
|
+
ignored.
|
|
14
|
+
- `Unit` gained a `trigger` (`:http` or `:sqs`), and `plan_from_connectors` plans several connectors at once. An
|
|
15
|
+
SQS unit gets a queue, a dead-letter queue and an event source mapping instead of a route, and its commands
|
|
16
|
+
stay out of the authorizer's anonymous list.
|
|
17
|
+
|
|
18
|
+
- `Packager` takes `provenance:` — a file recording which revision each unit last **changed** at, and `Service`
|
|
19
|
+
takes `provenance:` to stamp it as `FOOBARA_REVISION`. Deliberately not the deploying revision: stamping HEAD
|
|
20
|
+
everywhere would change every function's configuration on every commit, so CloudFormation would update all of
|
|
21
|
+
them and `cdk diff` would show all of them changing whether or not any code did.
|
|
22
|
+
- A unit's revision moves only when its artifact's content hash moves, so an untouched function keeps the honest
|
|
23
|
+
answer to "what is actually running here?" and its configuration does not churn.
|
|
24
|
+
- A revision recorded from a dirty tree is marked `-dirty` and treated as provisional: it is replaced by the first
|
|
25
|
+
clean revision seen to contain the same content, rather than sticking until the unit next changes.
|
|
26
|
+
- `Service` now has specs. It previously had one — that it raises without aws-cdk-lib — which let a real defect
|
|
27
|
+
through: a method defined and never called, so an environment variable silently vanished. There is now a fake
|
|
28
|
+
CDK (`spec/support/fake_cdk.rb`) exercising functions, sizing, routes, tracing and the authorizer, including
|
|
29
|
+
that the authorizer declares no identity source and no caching.
|
|
30
|
+
|
|
3
31
|
## [0.3.0] - 2026-08-04
|
|
4
32
|
|
|
5
33
|
- `Foobara::AWS.plan_from_connector(connector)` — build a plan by inspecting a live connector instead of a
|
data/README.md
CHANGED
|
@@ -15,6 +15,28 @@ service = Foobara::AWS::CDK::Service.new(self, "Api", plan: plan, code_root: "bu
|
|
|
15
15
|
posts_table.grant_read_write_data(service.function("posts"))
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
bundle add foobara-aws
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**No runtime dependencies.** `aws-cdk-lib` is your CDK app's, `foobara` is your
|
|
25
|
+
application's, and `jwt` belongs to the authorizer's own deployment unit — each
|
|
26
|
+
is referenced lazily, only where it is needed. That matters more than usual
|
|
27
|
+
here, because half this gem is loaded *inside a Lambda*, where every dependency
|
|
28
|
+
is paid for on each cold start.
|
|
29
|
+
|
|
30
|
+
Which half you need depends on where you are:
|
|
31
|
+
|
|
32
|
+
| you are | require | you need |
|
|
33
|
+
| --- | --- | --- |
|
|
34
|
+
| planning or packaging, in a build step | `foobara/aws/packager` | `foobara` |
|
|
35
|
+
| synthesising, in a CDK app | `foobara/aws/cdk/service` | `aws-cdk-lib` |
|
|
36
|
+
| serving a command, in a Lambda | `foobara/aws/handler` | neither |
|
|
37
|
+
| verifying a token, in the authorizer | `foobara/aws/authorizer` | `jwt` |
|
|
38
|
+
| checking a deployment | `foobara/aws/check` | neither |
|
|
39
|
+
|
|
18
40
|
## Where a plan comes from
|
|
19
41
|
|
|
20
42
|
```ruby
|
|
@@ -31,23 +53,14 @@ alone cannot say which commands are public.
|
|
|
31
53
|
|
|
32
54
|
All three produce the same `Plan`, and a spec asserts the first two agree.
|
|
33
55
|
|
|
34
|
-
##
|
|
35
|
-
|
|
36
|
-
```ruby
|
|
37
|
-
Foobara::AWS.plan(manifest) # what to deploy. Plain data. No CDK, no Foobara.
|
|
38
|
-
Foobara::AWS::CDK::Service.new # the AWS resources. No application.
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
They are separable because they usually run in different places. The plan is
|
|
42
|
-
produced where the app is — a running connector serves `/manifest` — and
|
|
43
|
-
consumed where the infrastructure is, which is often a CDK app that should not
|
|
44
|
-
have to load an application in order to deploy it.
|
|
56
|
+
## Carrying a plan between processes
|
|
45
57
|
|
|
46
|
-
A plan survives a JSON round trip, so it
|
|
58
|
+
A plan is plain data and survives a JSON round trip, so it can be produced in one
|
|
59
|
+
process and consumed in another:
|
|
47
60
|
|
|
48
61
|
```ruby
|
|
49
|
-
# in the build,
|
|
50
|
-
File.write("build/plan.json", JSON.pretty_generate(Foobara::AWS.
|
|
62
|
+
# in the build, where the app is loaded
|
|
63
|
+
File.write("build/plan.json", JSON.pretty_generate(Foobara::AWS.plan_from_connector(connector).to_h))
|
|
51
64
|
|
|
52
65
|
# in the CDK app — no Foobara, no ORM, no application gems
|
|
53
66
|
Foobara::AWS::CDK::Service.new(self, "Api",
|
|
@@ -55,9 +68,21 @@ Foobara::AWS::CDK::Service.new(self, "Api",
|
|
|
55
68
|
code_root: "build")
|
|
56
69
|
```
|
|
57
70
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
71
|
+
Worth understanding the trade rather than picking by default, because the two
|
|
72
|
+
options fail in opposite directions:
|
|
73
|
+
|
|
74
|
+
- **Read the connector at synth time** and the topology can never lag the code,
|
|
75
|
+
because it *is* the code. Synthesis then loads the application, so it is
|
|
76
|
+
slower and can fail for application reasons. If you do this, have the CDK
|
|
77
|
+
app's Gemfile inherit the application's rather than restating it — resolving a
|
|
78
|
+
second set of Foobara versions means the plan is computed by a different
|
|
79
|
+
Foobara than the one deployed.
|
|
80
|
+
- **Read a file** and synthesis depends on what was actually *built*, needs none
|
|
81
|
+
of the application's gems, and stays deterministic and offline. But the file is
|
|
82
|
+
only as current as the last build.
|
|
83
|
+
|
|
84
|
+
Either way `Service` raises when a unit has no artifact, so a command added but
|
|
85
|
+
not packaged fails at synth rather than 404ing after deploy.
|
|
61
86
|
|
|
62
87
|
## What the plan reads
|
|
63
88
|
|
|
@@ -26,12 +26,13 @@ module Foobara
|
|
|
26
26
|
class Service
|
|
27
27
|
DEFAULT_SIZING = { memory_size: 1024, timeout_seconds: 10 }.freeze
|
|
28
28
|
|
|
29
|
-
attr_reader :api, :functions, :plan
|
|
29
|
+
attr_reader :api, :functions, :plan, :queues
|
|
30
30
|
|
|
31
31
|
# rubocop:disable Metrics/ParameterLists
|
|
32
32
|
def initialize(scope, id, plan:, code_root:, sizing: {}, defaults: DEFAULT_SIZING,
|
|
33
33
|
environment: {}, runtime: nil, architecture: nil, authorizer: nil,
|
|
34
|
-
api_props: {}, tracing: nil, log_retention: nil
|
|
34
|
+
api_props: {}, tracing: nil, log_retention: nil, provenance: {},
|
|
35
|
+
batch_size: 10)
|
|
35
36
|
# Checked first: everything below needs it, and a bare LoadError from
|
|
36
37
|
# `constructs` would say nothing about which dependency is missing.
|
|
37
38
|
cdk
|
|
@@ -46,8 +47,16 @@ module Foobara
|
|
|
46
47
|
@architecture = architecture
|
|
47
48
|
@tracing = tracing
|
|
48
49
|
@log_retention = log_retention
|
|
50
|
+
# unit name => the revision its artifact last changed at, as the
|
|
51
|
+
# packager recorded it. Deliberately NOT the deploying revision: a
|
|
52
|
+
# function that has not changed keeps the answer to "what is actually
|
|
53
|
+
# running here?", and its configuration does not churn on every
|
|
54
|
+
# deploy.
|
|
55
|
+
@provenance = provenance.transform_keys(&:to_s)
|
|
56
|
+
@batch_size = batch_size
|
|
49
57
|
@functions = {}
|
|
50
58
|
|
|
59
|
+
@queues = {}
|
|
51
60
|
@api = cdk::APIGatewayv2::HttpAPI.new(@scope, "Api", api_props)
|
|
52
61
|
@authorizer = build_authorizer(authorizer)
|
|
53
62
|
plan.units.each { |unit| add_unit(unit) }
|
|
@@ -58,6 +67,10 @@ module Foobara
|
|
|
58
67
|
# table.grant_read_write_data(service.function("posts"))
|
|
59
68
|
def function(name) = @functions.fetch(name.to_s)
|
|
60
69
|
|
|
70
|
+
# The queue an SQS unit consumes, so the units that ENQUEUE onto it can
|
|
71
|
+
# be granted send access and told its URL.
|
|
72
|
+
def queue(name) = @queues.fetch(name.to_s)
|
|
73
|
+
|
|
61
74
|
def url = @api.url
|
|
62
75
|
|
|
63
76
|
private
|
|
@@ -66,6 +79,8 @@ module Foobara
|
|
|
66
79
|
fn = build_function(unit)
|
|
67
80
|
@functions[unit.name] = fn
|
|
68
81
|
|
|
82
|
+
return add_queue(unit, fn) unless unit.trigger == :http
|
|
83
|
+
|
|
69
84
|
props = {
|
|
70
85
|
path: unit.route,
|
|
71
86
|
methods: [cdk::APIGatewayv2::HttpMethod::ANY],
|
|
@@ -77,12 +92,44 @@ module Foobara
|
|
|
77
92
|
@api.add_routes(props)
|
|
78
93
|
end
|
|
79
94
|
|
|
95
|
+
# An SQS unit is reached by an event source mapping rather than a route,
|
|
96
|
+
# so it gets a queue, a dead-letter queue behind it, and no URL at all.
|
|
97
|
+
def add_queue(unit, function)
|
|
98
|
+
dead_letter = cdk::SQS::Queue.new(@scope, "#{logical(unit.name)}Dlq", {})
|
|
99
|
+
|
|
100
|
+
queue = cdk::SQS::Queue.new(@scope, "#{logical(unit.name)}Queue", {
|
|
101
|
+
# Longer than the function's own timeout, or SQS
|
|
102
|
+
# redelivers work that is still running — which looks
|
|
103
|
+
# like duplicate processing rather than a timeout.
|
|
104
|
+
visibility_timeout: cdk::Duration.seconds(
|
|
105
|
+
sizing_for(unit).fetch(:timeout_seconds) * 6
|
|
106
|
+
),
|
|
107
|
+
dead_letter_queue: { max_receive_count: 5, queue: dead_letter }
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
function.add_event_source(
|
|
111
|
+
cdk::LambdaEventSources::SQSEventSource.new(queue, {
|
|
112
|
+
batch_size: @batch_size,
|
|
113
|
+
# Without this, returning batchItemFailures
|
|
114
|
+
# is ignored and one bad record fails its
|
|
115
|
+
# nine neighbours.
|
|
116
|
+
report_batch_item_failures: true
|
|
117
|
+
})
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
@queues[unit.name] = queue
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def sizing_for(unit)
|
|
124
|
+
@defaults.merge(unit.sizing).merge(@sizing.fetch(unit.name, {}))
|
|
125
|
+
end
|
|
126
|
+
|
|
80
127
|
def build_function(unit)
|
|
81
128
|
# Precedence: an explicit sizing: override, then whatever the unit's
|
|
82
129
|
# commands declared via aws_lambda, then the defaults. The override
|
|
83
130
|
# exists for the case the declaration cannot know about — an
|
|
84
131
|
# environment where everything should be small, say.
|
|
85
|
-
sizing =
|
|
132
|
+
sizing = sizing_for(unit)
|
|
86
133
|
asset = File.join(@code_root, unit.name)
|
|
87
134
|
unless Dir.exist?(asset)
|
|
88
135
|
raise Error, "no artifact for unit #{unit.name.inspect} at #{asset} — build it before synthesising"
|
|
@@ -97,7 +144,7 @@ module Foobara
|
|
|
97
144
|
code: cdk::Lambda::Code.from_asset(asset),
|
|
98
145
|
memory_size: sizing.fetch(:memory_size),
|
|
99
146
|
timeout: cdk::Duration.seconds(sizing.fetch(:timeout_seconds)),
|
|
100
|
-
environment:
|
|
147
|
+
environment: unit_environment(unit)
|
|
101
148
|
}.merge(observability))
|
|
102
149
|
end
|
|
103
150
|
|
|
@@ -151,6 +198,13 @@ module Foobara
|
|
|
151
198
|
}.merge(observability))
|
|
152
199
|
end
|
|
153
200
|
|
|
201
|
+
def unit_environment(unit)
|
|
202
|
+
env = @environment.merge("FOOBARA_UNIT" => unit.name)
|
|
203
|
+
revision = @provenance[unit.name]
|
|
204
|
+
env["FOOBARA_REVISION"] = revision if revision
|
|
205
|
+
env
|
|
206
|
+
end
|
|
207
|
+
|
|
154
208
|
# Tracing and log retention, applied identically to every function
|
|
155
209
|
# including the authorizer — a trace that stops at the authorizer would
|
|
156
210
|
# hide the one invocation that happens on every single request.
|
|
@@ -185,7 +239,10 @@ module Foobara
|
|
|
185
239
|
# anything at stack level — a "comments" function and a "Comments" table
|
|
186
240
|
# both want the id "Comments" otherwise, and synthesis fails.
|
|
187
241
|
def construct(scope, id)
|
|
188
|
-
|
|
242
|
+
# Only if it is not already there: aws-cdk-lib loads it itself, and
|
|
243
|
+
# requiring it again is pointless work in the common case.
|
|
244
|
+
require "constructs" unless defined?(::Constructs)
|
|
245
|
+
|
|
189
246
|
Constructs::Construct.new(scope, id)
|
|
190
247
|
end
|
|
191
248
|
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Running Foobara commands from an AWS event source — SQS, and anything else
|
|
4
|
+
# that delivers records in batches.
|
|
5
|
+
#
|
|
6
|
+
# RUNTIME, not synth. Require this and nothing else from the gem.
|
|
7
|
+
#
|
|
8
|
+
# require "foobara/aws/event_handler"
|
|
9
|
+
# require_relative "config/connector"
|
|
10
|
+
#
|
|
11
|
+
# HANDLER = Foobara::AWS::EventHandler.new(QUEUE_CONNECTOR)
|
|
12
|
+
# def handle(event:, context:) = HANDLER.call(event, context)
|
|
13
|
+
#
|
|
14
|
+
# PARTIAL BATCH FAILURE is the whole reason this is not three lines. SQS hands a
|
|
15
|
+
# Lambda up to ten records at once. Raising on one of them fails the entire
|
|
16
|
+
# batch, so the nine that succeeded are delivered again — work is repeated, and a
|
|
17
|
+
# single poison message can replay its batch until the queue's retry limit.
|
|
18
|
+
#
|
|
19
|
+
# Returning `batchItemFailures` tells SQS which records to redeliver and lets the
|
|
20
|
+
# rest be deleted. It only works if the event source mapping was created with
|
|
21
|
+
# `report_batch_item_failures: true`, which Foobara::AWS::CDK::Service does.
|
|
22
|
+
|
|
23
|
+
require "json"
|
|
24
|
+
|
|
25
|
+
require_relative "error"
|
|
26
|
+
require_relative "version"
|
|
27
|
+
|
|
28
|
+
module Foobara
|
|
29
|
+
module AWS
|
|
30
|
+
class EventHandler
|
|
31
|
+
# What an enqueued message carries. Deliberately the same envelope the
|
|
32
|
+
# Resque connector uses — a command's name and its inputs are all that is
|
|
33
|
+
# needed, and keeping the shape means a queue is just another transport.
|
|
34
|
+
COMMAND_KEY = "command_name"
|
|
35
|
+
INPUTS_KEY = "inputs"
|
|
36
|
+
|
|
37
|
+
# +decode+ turns one record into [command_name, inputs] for event sources
|
|
38
|
+
# that carry no envelope — an S3 notification or a schedule knows nothing
|
|
39
|
+
# about commands, so the application has to say what it means.
|
|
40
|
+
def initialize(connector, decode: nil)
|
|
41
|
+
@connector = connector
|
|
42
|
+
@decode = decode
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def call(event, _context = nil)
|
|
46
|
+
failures = records(event).filter_map { |record| failure_for(record) }
|
|
47
|
+
|
|
48
|
+
{ "batchItemFailures" => failures }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def records(event) = Array(event["Records"] || event[:Records])
|
|
54
|
+
|
|
55
|
+
# Returns nil when the record succeeded, or its identifier when it did not.
|
|
56
|
+
# Nothing is re-raised: one bad record must not fail its nine neighbours,
|
|
57
|
+
# which is the entire point.
|
|
58
|
+
def failure_for(record)
|
|
59
|
+
run(record)
|
|
60
|
+
nil
|
|
61
|
+
rescue StandardError => e
|
|
62
|
+
# The message id, because that is what SQS asks for. Logged as well as
|
|
63
|
+
# reported, or a failure that keeps being redelivered is invisible.
|
|
64
|
+
warn("foobara-aws: record #{identifier(record)} failed: #{e.class}: #{e.message}")
|
|
65
|
+
{ "itemIdentifier" => identifier(record) }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def identifier(record) = record["messageId"] || record[:messageId]
|
|
69
|
+
|
|
70
|
+
def run(record)
|
|
71
|
+
name, inputs = command_and_inputs(record)
|
|
72
|
+
command_class = @connector.transformed_command_from_name(name)
|
|
73
|
+
raise Error, "no command #{name.inspect} is connected" unless command_class
|
|
74
|
+
|
|
75
|
+
# run!, not run: a command that returns an error outcome has failed, and
|
|
76
|
+
# this needs to raise so the record is reported and redelivered.
|
|
77
|
+
command_class.new(inputs || {}).run!
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def command_and_inputs(record)
|
|
81
|
+
return @decode.call(record) if @decode
|
|
82
|
+
|
|
83
|
+
body = record["body"] || record[:body]
|
|
84
|
+
envelope = body.is_a?(String) ? JSON.parse(body) : body
|
|
85
|
+
[envelope[COMMAND_KEY], envelope[INPUTS_KEY]]
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
data/lib/foobara/aws/packager.rb
CHANGED
|
@@ -37,7 +37,8 @@ module Foobara
|
|
|
37
37
|
# rubocop:disable Metrics/ParameterLists
|
|
38
38
|
def initialize(plan:, root: Dir.pwd, out: "build", sources: %w[app config],
|
|
39
39
|
gemfiles: "units", image: DEFAULT_IMAGE, env: {}, mounts: [],
|
|
40
|
-
handler_template: nil, authorizer: nil, docker: nil
|
|
40
|
+
handler_template: nil, authorizer: nil, docker: nil,
|
|
41
|
+
provenance: nil, revision: nil)
|
|
41
42
|
@plan = plan
|
|
42
43
|
@root = File.expand_path(root)
|
|
43
44
|
@out = File.expand_path(out, @root)
|
|
@@ -51,6 +52,11 @@ module Foobara
|
|
|
51
52
|
@mounts = mounts
|
|
52
53
|
@handler_template = handler_template
|
|
53
54
|
@authorizer = authorizer
|
|
55
|
+
# Where to keep the record of which revision each unit last CHANGED at.
|
|
56
|
+
# Outside `out` on purpose: it is only useful if it outlives a build and
|
|
57
|
+
# is committed, which a build directory is usually not.
|
|
58
|
+
@provenance_path = provenance
|
|
59
|
+
@revision = revision
|
|
54
60
|
# Injectable so the layout can be tested without Docker, and so an
|
|
55
61
|
# environment with a different container runtime can substitute one.
|
|
56
62
|
@docker = docker || method(:run_docker)
|
|
@@ -64,13 +70,92 @@ module Foobara
|
|
|
64
70
|
built << build_authorizer if @authorizer
|
|
65
71
|
|
|
66
72
|
File.write(plan_path, JSON.pretty_generate(@plan.to_h))
|
|
73
|
+
record_provenance(built) if @provenance_path
|
|
67
74
|
built
|
|
68
75
|
end
|
|
69
76
|
|
|
77
|
+
# unit name => the revision at which its artifact last changed. Empty
|
|
78
|
+
# unless `provenance:` was given.
|
|
79
|
+
attr_reader :provenance
|
|
80
|
+
|
|
70
81
|
def plan_path = File.join(@out, "plan.json")
|
|
71
82
|
|
|
72
83
|
private
|
|
73
84
|
|
|
85
|
+
# WHY THIS IS NOT SIMPLY THE CURRENT REVISION.
|
|
86
|
+
#
|
|
87
|
+
# Stamping every unit with HEAD would change every function's environment
|
|
88
|
+
# on every commit, so CloudFormation would update all of them and `cdk
|
|
89
|
+
# diff` would show all of them changing — every time, whether or not any
|
|
90
|
+
# of their code did. That is exactly the signal worth keeping.
|
|
91
|
+
#
|
|
92
|
+
# So a unit's revision only moves when its CONTENT moves. An untouched
|
|
93
|
+
# unit keeps the revision it last changed at, its configuration does not
|
|
94
|
+
# change, and CloudFormation leaves it alone. The file is also a readable
|
|
95
|
+
# history: `git log -p` over it says when each Lambda actually changed.
|
|
96
|
+
def record_provenance(built)
|
|
97
|
+
previous = read_provenance
|
|
98
|
+
current = built.to_h { |unit| [unit.fetch(:name), entry_for(unit, previous)] }
|
|
99
|
+
|
|
100
|
+
File.write(@provenance_path, "#{JSON.pretty_generate(current.sort.to_h)}\n")
|
|
101
|
+
@provenance = current.transform_values { |entry| entry["revision"] }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Unchanged content keeps its old revision. Only a real change adopts the
|
|
105
|
+
# current one.
|
|
106
|
+
#
|
|
107
|
+
# Except when the old revision is PROVISIONAL — recorded from a dirty tree,
|
|
108
|
+
# so it names a commit that did not contain this content. Those are
|
|
109
|
+
# replaced by the first clean revision seen to contain it, which is the
|
|
110
|
+
# best available answer and, unlike the alternative, self-healing: the
|
|
111
|
+
# marker would otherwise stick until the unit next changed, which could be
|
|
112
|
+
# never.
|
|
113
|
+
def entry_for(unit, previous)
|
|
114
|
+
content = content_hash(unit.fetch(:dir))
|
|
115
|
+
before = previous[unit.fetch(:name)]
|
|
116
|
+
keep = before && before["content"] == content && !provisional?(before["revision"])
|
|
117
|
+
|
|
118
|
+
{ "revision" => keep ? before["revision"] : revision_now, "content" => content }
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def provisional?(revision) = revision.to_s.end_with?("-dirty")
|
|
122
|
+
|
|
123
|
+
def read_provenance
|
|
124
|
+
return {} unless File.exist?(@provenance_path)
|
|
125
|
+
|
|
126
|
+
JSON.parse(File.read(@provenance_path))
|
|
127
|
+
rescue JSON::ParserError
|
|
128
|
+
{}
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# The artifact's contents, path-sensitively. Sorted so it does not depend
|
|
132
|
+
# on directory order, and paths are hashed as well as bytes so a rename is
|
|
133
|
+
# a change.
|
|
134
|
+
def content_hash(dir)
|
|
135
|
+
digest = Digest::SHA256.new
|
|
136
|
+
|
|
137
|
+
Dir.glob("**/*", File::FNM_DOTMATCH, base: dir).sort.each do |relative|
|
|
138
|
+
path = File.join(dir, relative)
|
|
139
|
+
next if File.directory?(path)
|
|
140
|
+
|
|
141
|
+
digest << relative << "\0" << Digest::SHA256.file(path).hexdigest
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
digest.hexdigest[0, 16]
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# `-dirty` because a revision recorded from an unclean tree does not
|
|
148
|
+
# identify the code that was built, and silently pretending otherwise is
|
|
149
|
+
# worse than an ugly string.
|
|
150
|
+
def revision_now
|
|
151
|
+
@revision_now ||= @revision || begin
|
|
152
|
+
sha = `git rev-parse --short HEAD 2>/dev/null`.strip
|
|
153
|
+
raise Error, "no revision available: pass revision: or run inside a git repository" if sha.empty?
|
|
154
|
+
|
|
155
|
+
`git status --porcelain 2>/dev/null`.strip.empty? ? sha : "#{sha}-dirty"
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
74
159
|
def build_unit(unit)
|
|
75
160
|
dir = prepare(unit.name)
|
|
76
161
|
@sources.each { |s| FileUtils.cp_r(File.join(@root, s), dir) }
|
|
@@ -101,7 +186,13 @@ module Foobara
|
|
|
101
186
|
end
|
|
102
187
|
|
|
103
188
|
def handler_source(unit)
|
|
104
|
-
template =
|
|
189
|
+
template =
|
|
190
|
+
if @handler_template
|
|
191
|
+
File.read(@handler_template)
|
|
192
|
+
else
|
|
193
|
+
unit.trigger == :http ? DEFAULT_HANDLER : DEFAULT_EVENT_HANDLER
|
|
194
|
+
end
|
|
195
|
+
|
|
105
196
|
render(template, unit)
|
|
106
197
|
end
|
|
107
198
|
|
|
@@ -252,6 +343,27 @@ module Foobara
|
|
|
252
343
|
end
|
|
253
344
|
ERB
|
|
254
345
|
|
|
346
|
+
# For a unit consumed from an event source rather than routed. It connects
|
|
347
|
+
# nothing itself: the application's own connector already knows which
|
|
348
|
+
# commands it consumes, and re-deriving that here could disagree with it.
|
|
349
|
+
DEFAULT_EVENT_HANDLER = <<~ERB
|
|
350
|
+
# Generated by foobara-aws for unit <%= unit.name.inspect %>. Do not edit.
|
|
351
|
+
#
|
|
352
|
+
# Consumes: <%= unit.commands.join(", ") %>
|
|
353
|
+
#
|
|
354
|
+
# Triggered by <%= unit.trigger %>, so there is no route and no
|
|
355
|
+
# authorizer — a message is already inside the trust boundary.
|
|
356
|
+
require_relative "vendor/bundle/bundler/setup"
|
|
357
|
+
require "foobara/aws/event_handler"
|
|
358
|
+
require_relative "config/connector"
|
|
359
|
+
|
|
360
|
+
HANDLER = Foobara::AWS::EventHandler.new(FOOBARA_EVENT_CONNECTOR)
|
|
361
|
+
|
|
362
|
+
def handle(event:, context:)
|
|
363
|
+
HANDLER.call(event, context)
|
|
364
|
+
end
|
|
365
|
+
ERB
|
|
366
|
+
|
|
255
367
|
DEFAULT_AUTHORIZER = <<~ERB
|
|
256
368
|
# Generated by foobara-aws. Do not edit.
|
|
257
369
|
#
|
data/lib/foobara/aws/plan.rb
CHANGED
|
@@ -12,7 +12,10 @@ module Foobara
|
|
|
12
12
|
# +sizing+ is whatever the unit's commands declared with
|
|
13
13
|
# {Foobara::AWS::Lambda}, reduced to one value per key — empty when none
|
|
14
14
|
# of them said anything.
|
|
15
|
-
|
|
15
|
+
# +trigger+ is how the unit is invoked: :http for a routed command, :sqs for
|
|
16
|
+
# one consumed from an SQS queue. An SQS unit has no route — it is reached by an
|
|
17
|
+
# event source mapping, not a URL — so `route` is nil for it.
|
|
18
|
+
Unit = Data.define(:name, :group, :commands, :route, :public_commands, :sizing, :trigger)
|
|
16
19
|
|
|
17
20
|
# The whole topology, read out of a Foobara manifest.
|
|
18
21
|
#
|
|
@@ -38,7 +41,7 @@ module Foobara
|
|
|
38
41
|
"units" => units.map do |u|
|
|
39
42
|
{ "name" => u.name, "group" => u.group, "commands" => u.commands,
|
|
40
43
|
"route" => u.route, "public_commands" => u.public_commands,
|
|
41
|
-
"sizing" => u.sizing }
|
|
44
|
+
"sizing" => u.sizing, "trigger" => u.trigger.to_s }
|
|
42
45
|
end
|
|
43
46
|
}
|
|
44
47
|
end
|
|
@@ -56,7 +59,8 @@ module Foobara
|
|
|
56
59
|
u = u.transform_keys(&:to_s)
|
|
57
60
|
Unit.new(name: u.fetch("name"), group: u["group"], commands: u.fetch("commands"),
|
|
58
61
|
route: u["route"], public_commands: u.fetch("public_commands", []),
|
|
59
|
-
sizing: (u["sizing"] || {}).to_h { |k, v| [k.to_sym, v] }
|
|
62
|
+
sizing: (u["sizing"] || {}).to_h { |k, v| [k.to_sym, v] },
|
|
63
|
+
trigger: u.fetch("trigger", "http").to_sym)
|
|
60
64
|
end
|
|
61
65
|
)
|
|
62
66
|
end
|
|
@@ -72,13 +76,13 @@ module Foobara
|
|
|
72
76
|
# A connector's manifest, not Foobara.manifest: `requires_authentication`
|
|
73
77
|
# only exists once commands are connected, and it is the field the public
|
|
74
78
|
# list comes from.
|
|
75
|
-
def plan(manifest, mount: "/run", granularity: :domain, exclude: DEFAULT_EXCLUDE)
|
|
79
|
+
def plan(manifest, mount: "/run", granularity: :domain, exclude: DEFAULT_EXCLUDE, trigger: :http)
|
|
76
80
|
commands = app_commands(manifest, exclude)
|
|
77
81
|
units =
|
|
78
82
|
case granularity
|
|
79
|
-
when :domain then group_units(commands, "domain", mount)
|
|
80
|
-
when :organization then group_units(commands, "organization", mount)
|
|
81
|
-
when :command then command_units(commands, mount)
|
|
83
|
+
when :domain then group_units(commands, "domain", mount, trigger)
|
|
84
|
+
when :organization then group_units(commands, "organization", mount, trigger)
|
|
85
|
+
when :command then command_units(commands, mount, trigger)
|
|
82
86
|
else raise ArgumentError, "unknown granularity #{granularity.inspect}"
|
|
83
87
|
end
|
|
84
88
|
|
|
@@ -99,9 +103,24 @@ module Foobara
|
|
|
99
103
|
#
|
|
100
104
|
# It adapts each command into the shape {plan} already reads, so there is
|
|
101
105
|
# one implementation and the two routes cannot drift.
|
|
106
|
+
# Several connectors at once, keyed by how each is triggered:
|
|
107
|
+
#
|
|
108
|
+
# Foobara::AWS.plan_from_connectors(http: rack_connector, sqs: sqs_connector)
|
|
109
|
+
#
|
|
110
|
+
# A domain can appear under both — Profiles serving HTTP commands and
|
|
111
|
+
# consuming a queue — so a non-http unit's name is suffixed. Otherwise two
|
|
112
|
+
# units would claim the same name and the same artifact directory.
|
|
113
|
+
def plan_from_connectors(connectors, **)
|
|
114
|
+
plans = connectors.map { |trigger, connector| plan_from_connector(connector, trigger:, **) }
|
|
115
|
+
first = plans.first or raise ArgumentError, "no connectors given"
|
|
116
|
+
|
|
117
|
+
Plan.new(mount: first.mount, granularity: first.granularity,
|
|
118
|
+
units: plans.flat_map(&:units).sort_by(&:name))
|
|
119
|
+
end
|
|
120
|
+
|
|
102
121
|
def plan_from_connector(connector, **)
|
|
103
122
|
commands = connector.command_registry.all_transformed_command_classes.to_h do |transformed|
|
|
104
|
-
[transformed.
|
|
123
|
+
[command_class_of(transformed).full_command_name, describe_command(transformed)]
|
|
105
124
|
end
|
|
106
125
|
|
|
107
126
|
plan({ "command" => commands }, **)
|
|
@@ -123,19 +142,44 @@ module Foobara
|
|
|
123
142
|
# planning reads — a manifest carries much more, and none of the rest
|
|
124
143
|
# matters here.
|
|
125
144
|
def describe_command(transformed)
|
|
126
|
-
command_class = transformed
|
|
145
|
+
command_class = command_class_of(transformed)
|
|
127
146
|
|
|
128
147
|
{
|
|
129
148
|
"domain" => domain_name(command_class),
|
|
130
149
|
"organization" => organization_name(command_class),
|
|
131
150
|
"scoped_full_path" => Array(command_class.scoped_full_path),
|
|
132
|
-
|
|
151
|
+
# Only a connector that gates commands answers this. One that does
|
|
152
|
+
# not — a queue has no anonymous callers — leaves it nil, which reads
|
|
153
|
+
# as "not gated" and is right: the question does not apply.
|
|
154
|
+
"requires_authentication" => gated?(transformed),
|
|
133
155
|
"depends_on" => Array(command_class.depends_on).map(&:to_s),
|
|
134
156
|
# The AWSLambda DSL, when the command extends it.
|
|
135
157
|
"aws_lambda" => (command_class.aws_lambda_manifest if command_class.respond_to?(:aws_lambda_manifest))
|
|
136
158
|
}.compact
|
|
137
159
|
end
|
|
138
160
|
|
|
161
|
+
# A connector wraps commands in a TransformedCommand only when it has
|
|
162
|
+
# something to transform. One with no transformers — a queue connector,
|
|
163
|
+
# say — registers the command class itself.
|
|
164
|
+
#
|
|
165
|
+
# Rescuing rather than asking respond_to?: Foobara namespaces answer
|
|
166
|
+
# respond_to? through method_missing, so the question comes back true and
|
|
167
|
+
# the call still raises.
|
|
168
|
+
def command_class_of(transformed)
|
|
169
|
+
transformed.command_class
|
|
170
|
+
rescue NoMethodError
|
|
171
|
+
transformed
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Only a connector that gates commands answers this. One that does not —
|
|
175
|
+
# a queue has no anonymous callers — leaves it nil, which reads as "not
|
|
176
|
+
# gated", and for a unit with no route the question does not arise.
|
|
177
|
+
def gated?(transformed)
|
|
178
|
+
transformed.requires_authentication
|
|
179
|
+
rescue NoMethodError
|
|
180
|
+
nil
|
|
181
|
+
end
|
|
182
|
+
|
|
139
183
|
def domain_name(command_class)
|
|
140
184
|
command_class.domain.scoped_full_name
|
|
141
185
|
rescue StandardError
|
|
@@ -154,11 +198,11 @@ module Foobara
|
|
|
154
198
|
end
|
|
155
199
|
end
|
|
156
200
|
|
|
157
|
-
def group_units(commands, key, mount)
|
|
201
|
+
def group_units(commands, key, mount, trigger)
|
|
158
202
|
commands.group_by { |_name, c| c[key].to_s }.map do |group, members|
|
|
159
203
|
names = members.map(&:first)
|
|
160
204
|
Unit.new(
|
|
161
|
-
name: slug(group),
|
|
205
|
+
name: unit_name(slug(group), trigger),
|
|
162
206
|
group: group,
|
|
163
207
|
commands: names.sort,
|
|
164
208
|
# Derived from the commands' own paths, not from the group's name.
|
|
@@ -171,18 +215,24 @@ module Foobara
|
|
|
171
215
|
# safe with a REQUEST authorizer, which sees the path and can decide
|
|
172
216
|
# per command; a JWT authorizer attaches per route, so a unit mixing
|
|
173
217
|
# public and authenticated commands would have to be split.
|
|
174
|
-
route:
|
|
175
|
-
|
|
176
|
-
|
|
218
|
+
# No route for anything but HTTP: an SQS unit is reached by an
|
|
219
|
+
# event source mapping.
|
|
220
|
+
route: (greedy_route(members, mount) if trigger == :http),
|
|
221
|
+
# Only meaningful for something with a route. A queue has no
|
|
222
|
+
# anonymous callers, and letting these through would put the command
|
|
223
|
+
# in the authorizer's anonymous list, where it does not belong.
|
|
224
|
+
public_commands: trigger == :http ? public_of(members) : [],
|
|
225
|
+
sizing: sizing_of(members),
|
|
226
|
+
trigger: trigger
|
|
177
227
|
)
|
|
178
228
|
end
|
|
179
229
|
end
|
|
180
230
|
|
|
181
|
-
def command_units(commands, mount)
|
|
231
|
+
def command_units(commands, mount, trigger)
|
|
182
232
|
commands.map do |name, command|
|
|
183
233
|
path = Array(command["scoped_full_path"])
|
|
184
234
|
Unit.new(
|
|
185
|
-
name: slug(name),
|
|
235
|
+
name: unit_name(slug(name), trigger),
|
|
186
236
|
group: command["domain"].to_s,
|
|
187
237
|
# Closed over `depends_on`, transitively. A command that invokes
|
|
188
238
|
# another must be able to reach it, so a per-command artifact has to
|
|
@@ -196,12 +246,13 @@ module Foobara
|
|
|
196
246
|
commands: closure(name, commands),
|
|
197
247
|
# Exact, not greedy: one command per Lambda needs no wildcard, and an
|
|
198
248
|
# exact route is what lets API Gateway 404 an unknown command itself.
|
|
199
|
-
route: "#{mount}/#{path.join("/")}",
|
|
249
|
+
route: ("#{mount}/#{path.join("/")}" if trigger == :http),
|
|
200
250
|
# Only the ROUTED command decides this. A dependency is invoked in
|
|
201
251
|
# process, never over HTTP, so whether it requires authentication
|
|
202
252
|
# says nothing about whether this route may be called anonymously.
|
|
203
|
-
public_commands: public_of([[name, command]]),
|
|
204
|
-
sizing: sizing_of([[name, command]])
|
|
253
|
+
public_commands: trigger == :http ? public_of([[name, command]]) : [],
|
|
254
|
+
sizing: sizing_of([[name, command]]),
|
|
255
|
+
trigger: trigger
|
|
205
256
|
)
|
|
206
257
|
end
|
|
207
258
|
end
|
|
@@ -263,6 +314,11 @@ module Foobara
|
|
|
263
314
|
end
|
|
264
315
|
end
|
|
265
316
|
|
|
317
|
+
# HTTP units keep their bare name, so nothing about existing deployments
|
|
318
|
+
# changes; anything else is suffixed to stay distinct from a same-named
|
|
319
|
+
# domain served over HTTP.
|
|
320
|
+
def unit_name(name, trigger) = trigger == :http ? name : "#{name}-#{trigger}"
|
|
321
|
+
|
|
266
322
|
def symbolize(hash) = hash.to_h { |k, v| [k.to_sym, v] }
|
|
267
323
|
|
|
268
324
|
# A CloudFormation-friendly, filesystem-friendly identifier: "Posts" and
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# A connector that runs commands from an SQS queue instead of over HTTP.
|
|
4
|
+
#
|
|
5
|
+
# Named for the service rather than for the idea: it speaks SQS specifically —
|
|
6
|
+
# its envelope, its client, its at-least-once delivery — and calling it a queue
|
|
7
|
+
# connector would suggest it abstracts over queues, which it does not.
|
|
8
|
+
#
|
|
9
|
+
# Modelled on foobara-resque-connector, and on purpose: the envelope is the same
|
|
10
|
+
# (`command_name` and `inputs`), so a background job is not a special kind of
|
|
11
|
+
# object — it is a command reached by a different transport.
|
|
12
|
+
#
|
|
13
|
+
# QUEUE = Foobara::AWS::SQSConnector.new(queue_url: ENV["RECONCILE_QUEUE_URL"])
|
|
14
|
+
# QUEUE.connect(Profiles::ReconcileAuthorSnapshot)
|
|
15
|
+
#
|
|
16
|
+
# Profiles::ReconcileAuthorSnapshotAsync.run!(user_sub: sub) # enqueues
|
|
17
|
+
#
|
|
18
|
+
# `connect` generates that `…Async` command, so enqueuing is itself a command:
|
|
19
|
+
# it validates its inputs against the real command's input type, so a malformed
|
|
20
|
+
# job is refused where it is enqueued rather than discovered by a worker later.
|
|
21
|
+
#
|
|
22
|
+
# WITH NO QUEUE URL IT RUNS INLINE. That is what makes local development and a
|
|
23
|
+
# test suite work without SQS or a fake, and it means the queue is a deployment
|
|
24
|
+
# concern rather than something the application has to know about.
|
|
25
|
+
#
|
|
26
|
+
# Requires Foobara. The consuming half — Foobara::AWS::EventHandler — does not.
|
|
27
|
+
|
|
28
|
+
require "json"
|
|
29
|
+
|
|
30
|
+
# Not merely "foobara": Foobara::CommandConnector exists after foobara/all but
|
|
31
|
+
# WITHOUT its methods, so subclassing it succeeds and fails later at `connect`
|
|
32
|
+
# with "no superclass method". Requiring the project explicitly is the
|
|
33
|
+
# difference between an error at load and a confusing one at runtime.
|
|
34
|
+
require "foobara/command_connectors"
|
|
35
|
+
|
|
36
|
+
require_relative "error"
|
|
37
|
+
|
|
38
|
+
module Foobara
|
|
39
|
+
module AWS
|
|
40
|
+
class SQSConnector < Foobara::CommandConnector
|
|
41
|
+
# Enqueues rather than executes. Generated per connected command by
|
|
42
|
+
# `connect`, the way ResqueConnector generates its own.
|
|
43
|
+
class EnqueueCommand < Foobara::Command
|
|
44
|
+
class << self
|
|
45
|
+
attr_accessor :target_command_class, :sqs_connector
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def execute
|
|
49
|
+
self.class.sqs_connector.enqueue(
|
|
50
|
+
self.class.target_command_class.full_command_name, raw_inputs
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
attr_reader :queue_url
|
|
56
|
+
|
|
57
|
+
def initialize(*, queue_url: nil, sqs: nil, **, &)
|
|
58
|
+
@queue_url = queue_url
|
|
59
|
+
@sqs = sqs
|
|
60
|
+
|
|
61
|
+
super(*, **, &)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def connect(connectable, *, **, &)
|
|
65
|
+
exposed = Util.array(super)
|
|
66
|
+
|
|
67
|
+
exposed.each { |exposed_command| build_async_command(exposed_command) }
|
|
68
|
+
|
|
69
|
+
exposed
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Puts the envelope on the queue, or runs the command inline when there is
|
|
73
|
+
# no queue configured.
|
|
74
|
+
def enqueue(command_name, inputs)
|
|
75
|
+
# Checked in both paths: a message naming a command nothing consumes is
|
|
76
|
+
# a job that can only fail, and it should fail here rather than in a
|
|
77
|
+
# worker after a delivery delay.
|
|
78
|
+
command_class = transformed_command_from_name(command_name)
|
|
79
|
+
raise Error, "command #{command_name.inspect} is not connected" unless command_class
|
|
80
|
+
|
|
81
|
+
envelope = { EventHandler::COMMAND_KEY => command_name }
|
|
82
|
+
envelope[EventHandler::INPUTS_KEY] = inputs unless inputs.nil? || inputs.empty?
|
|
83
|
+
|
|
84
|
+
return command_class.new(inputs || {}).run! unless queue_url
|
|
85
|
+
|
|
86
|
+
sqs.send_message(queue_url: queue_url, message_body: JSON.dump(envelope))
|
|
87
|
+
envelope
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
def sqs
|
|
93
|
+
@sqs ||= begin
|
|
94
|
+
require "aws-sdk-sqs"
|
|
95
|
+
Aws::SQS::Client.new
|
|
96
|
+
rescue LoadError
|
|
97
|
+
raise Error,
|
|
98
|
+
"SQSConnector needs the `aws-sdk-sqs` gem to enqueue, which foobara-aws does not depend on. " \
|
|
99
|
+
"Add it to the unit that enqueues, or pass sqs: your own client."
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def build_async_command(exposed_command)
|
|
104
|
+
command_class = exposed_command.command_class
|
|
105
|
+
async = Util.make_class("#{command_class.name}Async", EnqueueCommand)
|
|
106
|
+
|
|
107
|
+
# The real command's input type, so an invalid job is refused at the
|
|
108
|
+
# point of enqueuing rather than by a worker minutes later.
|
|
109
|
+
inputs_type = exposed_command.transformed_command_class.inputs_type
|
|
110
|
+
async.inputs(inputs_type) if inputs_type
|
|
111
|
+
|
|
112
|
+
async.sqs_connector = self
|
|
113
|
+
async.target_command_class = exposed_command
|
|
114
|
+
async
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
require_relative "event_handler"
|
data/lib/foobara/aws/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: foobara-aws
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Omar Qureshi
|
|
@@ -28,10 +28,12 @@ files:
|
|
|
28
28
|
- lib/foobara/aws/cdk/service.rb
|
|
29
29
|
- lib/foobara/aws/check.rb
|
|
30
30
|
- lib/foobara/aws/error.rb
|
|
31
|
+
- lib/foobara/aws/event_handler.rb
|
|
31
32
|
- lib/foobara/aws/handler.rb
|
|
32
33
|
- lib/foobara/aws/lambda.rb
|
|
33
34
|
- lib/foobara/aws/packager.rb
|
|
34
35
|
- lib/foobara/aws/plan.rb
|
|
36
|
+
- lib/foobara/aws/sqs_connector.rb
|
|
35
37
|
- lib/foobara/aws/version.rb
|
|
36
38
|
homepage: https://github.com/omarqureshi/foobara-aws
|
|
37
39
|
licenses:
|