jets 0.7.1 → 0.8.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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +7 -0
  3. data/Gemfile.lock +1 -1
  4. data/lib/jets.rb +2 -0
  5. data/lib/jets/cfn/ship.rb +2 -0
  6. data/lib/jets/cfn/template_builders/base_child_builder.rb +14 -0
  7. data/lib/jets/cfn/template_builders/controller_builder.rb +4 -74
  8. data/lib/jets/cfn/template_builders/interface.rb +5 -0
  9. data/lib/jets/cfn/template_builders/job_builder.rb +1 -55
  10. data/lib/jets/cfn/template_builders/rule_builder.rb +7 -58
  11. data/lib/jets/cfn/template_mappers.rb +1 -4
  12. data/lib/jets/cfn/template_mappers/child_mapper.rb +1 -1
  13. data/lib/jets/cfn/template_mappers/gateway_resource_mapper.rb +5 -4
  14. data/lib/jets/commands/build.rb +2 -2
  15. data/lib/jets/commands/templates/skeleton/config/application.rb.tt +4 -1
  16. data/lib/jets/internal/app/jobs/jets/preheat_job.rb +2 -2
  17. data/lib/jets/job.rb +0 -1
  18. data/lib/jets/job/dsl.rb +52 -34
  19. data/lib/jets/lambda/dsl.rb +43 -1
  20. data/lib/jets/lambda/task.rb +2 -1
  21. data/lib/jets/pascalize.rb +26 -8
  22. data/lib/jets/resource.rb +7 -0
  23. data/lib/jets/resource/attributes.rb +46 -0
  24. data/lib/jets/resource/creator.rb +17 -0
  25. data/lib/jets/resource/permission.rb +43 -0
  26. data/lib/jets/resource/replacer.rb +40 -0
  27. data/lib/jets/resource/replacer/base.rb +98 -0
  28. data/lib/jets/resource/replacer/config_rule.rb +18 -0
  29. data/lib/jets/resource/route.rb +67 -0
  30. data/lib/jets/resource/route/attributes.rb +8 -0
  31. data/lib/jets/resource/route/cors.rb +60 -0
  32. data/lib/jets/rule.rb +0 -2
  33. data/lib/jets/rule/dsl.rb +71 -31
  34. data/lib/jets/version.rb +1 -1
  35. metadata +12 -8
  36. data/lib/jets/cfn/template_mappers/config_rule_mapper.rb +0 -34
  37. data/lib/jets/cfn/template_mappers/events_rule_mapper.rb +0 -40
  38. data/lib/jets/cfn/template_mappers/gateway_method_mapper.rb +0 -56
  39. data/lib/jets/job/task.rb +0 -17
  40. data/lib/jets/rule/aws_managed_rule.rb +0 -12
  41. data/lib/jets/rule/task.rb +0 -44
data/lib/jets/job.rb CHANGED
@@ -1,5 +1,4 @@
1
1
  class Jets::Job
2
2
  autoload :Dsl, "jets/job/dsl"
3
3
  autoload :Base, "jets/job/base"
4
- autoload :Task, "jets/job/task"
5
4
  end
data/lib/jets/job/dsl.rb CHANGED
@@ -1,57 +1,75 @@
1
1
  # Jets::Job::Base < Jets::Lambda::Functions
2
2
  # Both Jets::Job::Base and Jets::Lambda::Functions have Dsl modules included.
3
3
  # So the Jets::Job::Dsl overrides some of the Jets::Lambda::Functions behavior.
4
+ #
5
+ # Implements:
6
+ # default_associated_resource: must return @resources
4
7
  module Jets::Job::Dsl
5
8
  extend ActiveSupport::Concern
6
9
 
7
10
  included do
8
11
  class << self
9
12
  def rate(expression)
10
- @rate = expression
13
+ update_properties(schedule_expression: "rate(#{expression})")
11
14
  end
12
15
 
13
16
  def cron(expression)
14
- @cron = expression
17
+ update_properties(schedule_expression: "cron(#{expression}")
15
18
  end
16
19
 
17
- # Explicitly disable scheduling for the function
18
- def disable(value)
19
- @disable = value
20
+ def event_pattern(details={})
21
+ event_rule(event_pattern: details)
22
+ add_descriptions # useful: generic description in the Event Rule console
20
23
  end
21
24
 
22
- # This is a property of the AWS::Events::Rule not the Lambda function
23
- def state(value)
24
- @state = value
25
+ def add_descriptions
26
+ numbered_resources = []
27
+ n = 1
28
+ @resources.map do |definition|
29
+ logical_id = definition.keys.first
30
+ attributes = definition.values.first
31
+ attributes[:properties][:description] = "#{self.name} Event Rule #{n}"
32
+ numbered_resources << { "#{logical_id}" => attributes }
33
+ n += 1
34
+ end
35
+ @resources = numbered_resources
25
36
  end
26
37
 
27
- # Override register_task.
28
- # A Job::Task is a Lambda::Task with some added DSL methods like
29
- # rate and cron.
30
- def register_task(meth, lang=:ruby)
31
- if @rate || @cron || @disable
32
- # Job lambda function.
33
- all_tasks[meth] = Jets::Job::Task.new(self.name, meth,
34
- rate: @rate,
35
- cron: @cron,
36
- state: @state,
37
- properties: @properties,
38
- lang: lang)
39
- true
40
- else
41
- task_name = "#{name}##{meth}" # IE: HardJob#dig
42
- puts "[WARNING] #{task_name} created without a rate or cron expression. " \
43
- "Add a rate or cron expression above the method definition if you want this method to be scheduled. " \
44
- "If #{task_name} is not meant to be a scheduled lambda function, you can put the method under after a private keyword to get rid of this warning. " \
45
- "#{task_name} defined at #{caller[1].inspect}."
46
- false
47
- end
48
- # Done storing options, clear out for the next added method.
49
- clear_properties
38
+ def default_associated_resource
39
+ event_rule
40
+ @resources # must return @resoures for update_properties
50
41
  end
51
42
 
52
- def clear_properties
53
- super
54
- @rate, @cron, @state, @disable = nil, nil, nil, nil
43
+ def event_rule(props={})
44
+ default_props = {
45
+ state: "ENABLED",
46
+ targets: [{
47
+ arn: "!GetAtt {namespace}LambdaFunction.Arn",
48
+ id: "{namespace}RuleTarget"
49
+ }]
50
+ }
51
+ properties = default_props.deep_merge(props)
52
+
53
+ resource("{namespace}EventsRule" => {
54
+ type: "AWS::Events::Rule",
55
+ properties: properties
56
+ })
57
+
58
+ add_logical_id_counter if @resources.size > 1
59
+ end
60
+
61
+ # Loop back through the resources and add a counter to the end of the id
62
+ # to handle multiple events.
63
+ # Then replace @resources entirely
64
+ def add_logical_id_counter
65
+ numbered_resources = []
66
+ n = 1
67
+ @resources.map do |definition|
68
+ logical_id = definition.keys.first
69
+ numbered_resources << { "#{logical_id}#{n}" => definition.values.first }
70
+ n += 1
71
+ end
72
+ @resources = numbered_resources
55
73
  end
56
74
  end
57
75
  end
@@ -1,3 +1,5 @@
1
+ # Other dsl that rely on this must implement
2
+ # default_associated_resource: must return @resources
1
3
  module Jets::Lambda::Dsl
2
4
  extend ActiveSupport::Concern
3
5
 
@@ -120,6 +122,44 @@ module Jets::Lambda::Dsl
120
122
  !!(class_iam_policy || class_managed_iam_policy)
121
123
  end
122
124
 
125
+ #############################
126
+ # Generic method that registers a resource to be associated with the Lambda function.
127
+ # In the future all DSL methods can lead here.
128
+ def resources(*definitions)
129
+ if definitions == [nil] # when resources called with no arguments
130
+ @resources || []
131
+ else
132
+ @resources ||= []
133
+ @resources += definitions
134
+ @resources.flatten!
135
+ end
136
+ end
137
+ alias_method :resource, :resources
138
+
139
+ # Main method that the convenience methods call for to create resources associated
140
+ # with the Lambda function. References the first resource and updates it inplace.
141
+ # Useful for associated resources that are meant to be declare and associated
142
+ # with only one Lambda function. Example:
143
+ #
144
+ # Config Rule <=> Lambda function is 1-to-1
145
+ #
146
+ # Note: This methods calls default_associated_resource. The inheriting DSL class
147
+ # must implement default_associated_resource. The default_associated_resource should
148
+ # wrap another method that is nicely name so that the nicely name method is
149
+ # available in the DSL. Example:
150
+ #
151
+ # def default_associated_resource
152
+ # config_rule
153
+ # end
154
+ #
155
+ def update_properties(values={})
156
+ @resources ||= default_associated_resource
157
+ definition = @resources.first # singleton
158
+ attributes = definition.values.first
159
+ attributes[:properties].merge!(values)
160
+ @resources
161
+ end
162
+
123
163
  # meth is a Symbol
124
164
  def method_added(meth)
125
165
  return if %w[initialize method_missing].include?(meth.to_s)
@@ -133,7 +173,8 @@ module Jets::Lambda::Dsl
133
173
  # We adjust the class name when we build the functions later in
134
174
  # FunctionContstructor#adjust_tasks.
135
175
  all_tasks[meth] = Jets::Lambda::Task.new(self.name, meth,
136
- properties: @properties,
176
+ resources: @resources, # associated resources
177
+ properties: @properties, # lambda function properties
137
178
  iam_policy: @iam_policy,
138
179
  managed_iam_policy: @managed_iam_policy,
139
180
  lang: lang)
@@ -152,6 +193,7 @@ module Jets::Lambda::Dsl
152
193
  end
153
194
 
154
195
  def clear_properties
196
+ @resources = nil
155
197
  @properties = nil
156
198
  @iam_policy = nil
157
199
  @managed_iam_policy = nil
@@ -1,11 +1,12 @@
1
1
  class Jets::Lambda::Task
2
2
  attr_accessor :class_name, :type
3
- attr_reader :meth, :properties, :iam_policy, :managed_iam_policy, :lang
3
+ attr_reader :meth, :resources, :properties, :iam_policy, :managed_iam_policy, :lang
4
4
  def initialize(class_name, meth, options={})
5
5
  @class_name = class_name.to_s # use at EventsRuleMapper#full_task_name
6
6
  @meth = meth
7
7
  @options = options
8
8
  @type = options[:type] || get_type # controller, job, or function
9
+ @resources = options[:resources] || {}
9
10
  @properties = options[:properties] || {}
10
11
  @iam_policy = options[:iam_policy]
11
12
  @managed_iam_policy = options[:managed_iam_policy]
@@ -4,14 +4,14 @@ module Jets
4
4
  # Specialized pascalize that will not pascalize keys under the
5
5
  # Variables part of the hash structure.
6
6
  # Based on: https://stackoverflow.com/questions/8706930/converting-nested-hash-keys-from-camelcase-to-snake-case-in-ruby
7
- def pascalize(value, parent_key=nil)
7
+ def pascalize(value, parent_keys=[])
8
8
  case value
9
9
  when Array
10
10
  value.map { |v| pascalize(v) }
11
11
  when Hash
12
12
  initializer = value.map do |k, v|
13
- new_key = pascal_key(k, parent_key)
14
- [new_key, pascalize(v, new_key)]
13
+ new_key = pascal_key(k, parent_keys)
14
+ [new_key, pascalize(v, parent_keys+[new_key])]
15
15
  end
16
16
  Hash[initializer]
17
17
  else
@@ -19,14 +19,32 @@ module Jets
19
19
  end
20
20
  end
21
21
 
22
- def pascal_key(k, parent_key=nil)
23
- if parent_key == "Variables" # do not pascalize keys anything under Variables
24
- k
22
+ def pascal_key(k, parent_keys=[])
23
+ k = k.to_s
24
+ if parent_keys.include?("Variables") # do not pascalize keys anything under Variables
25
+ k # pass through untouch
26
+ elsif parent_keys.include?("ResponseParameters")
27
+ k # pass through untouch
28
+ elsif k.include?('-') || k.include?('/')
29
+ k # pass through untouch
30
+ elsif parent_keys.last == "EventPattern" # top-level
31
+ k.dasherize
32
+ elsif parent_keys.include?("EventPattern")
33
+ # any keys at 2nd level under EventPattern will be camelize
34
+ new_k = k.camelize # an earlier pascalize has made the first char upcase
35
+ # so we need to downcase it again
36
+ first_char = new_k[0..0].downcase
37
+ new_k[0] = first_char
38
+ new_k
25
39
  else
26
- k = k.to_s.camelize
27
- k.slice(0,1).capitalize + k.slice(1..-1) # capitalize first letter only
40
+ pascalize_string(k)
28
41
  end
29
42
  end
43
+
44
+ def pascalize_string(s)
45
+ s = s.to_s.camelize
46
+ s.slice(0,1).capitalize + s.slice(1..-1) # capitalize first letter only
47
+ end
30
48
  end
31
49
  end
32
50
  end
@@ -0,0 +1,7 @@
1
+ module Jets::Resource
2
+ autoload :Attributes, 'jets/resource/attributes'
3
+ autoload :Creator, 'jets/resource/creator'
4
+ autoload :Permission, 'jets/resource/permission'
5
+ autoload :Replacer, 'jets/resource/replacer'
6
+ autoload :Route, 'jets/resource/route'
7
+ end
@@ -0,0 +1,46 @@
1
+ module Jets::Resource
2
+ class Attributes
3
+ extend Memoist
4
+
5
+ def initialize(data, task, replacements={})
6
+ @data = data
7
+ @task = task
8
+ @replacements = replacements
9
+ end
10
+
11
+ def logical_id
12
+ id = @data.keys.first
13
+ # replace possible {namespace} in the logical id
14
+ id = replacer.replace_value(id)
15
+ Jets::Pascalize.pascalize_string(id)
16
+ end
17
+
18
+ def type
19
+ attributes['Type']
20
+ end
21
+
22
+ def properties
23
+ attributes['Properties']
24
+ end
25
+
26
+ def attributes
27
+ attributes = @data.values.first
28
+ attributes = replacer.replace_placeholders(attributes, @replacements)
29
+ Jets::Pascalize.pascalize(attributes)
30
+ end
31
+
32
+ def replacer
33
+ # Use raw @data to avoid infinite loop from using attributes
34
+ attributes = Jets::Pascalize.pascalize(@data.values.first)
35
+ type = attributes['Type']
36
+ replacer_class = Replacer.lookup(type)
37
+ replacer_class.new(@task)
38
+ end
39
+ memoize :replacer
40
+
41
+ def permission
42
+ Permission.new(@task, self)
43
+ end
44
+ memoize :permission
45
+ end
46
+ end
@@ -0,0 +1,17 @@
1
+ module Jets::Resource
2
+ class Creator
3
+ extend Memoist
4
+
5
+ def initialize(definition, task)
6
+ @definition = definition
7
+ @task = task # task that the definition belongs to
8
+ end
9
+
10
+ # Template snippet that gets injected into the CloudFormation template.
11
+ def attributes
12
+ Attributes.new(@definition, @task)
13
+ end
14
+ alias_method :resource, :attributes
15
+ memoize :attributes
16
+ end
17
+ end
@@ -0,0 +1,43 @@
1
+ module Jets::Resource
2
+ class Permission
3
+ extend Memoist
4
+
5
+ def initialize(task, resource)
6
+ @task = task
7
+ @resource_attributes = resource
8
+ end
9
+
10
+ def attributes
11
+ logical_id = "{namespace}Permission"
12
+ md = @resource_attributes.logical_id.match(/(\d+)/)
13
+ if md
14
+ counter = md[1]
15
+ end
16
+ logical_id = [logical_id, counter].compact.join('')
17
+
18
+ attributes = {
19
+ logical_id => {
20
+ type: "AWS::Lambda::Permission",
21
+ properties: {
22
+ function_name: "!GetAtt {namespace}LambdaFunction.Arn",
23
+ action: "lambda:InvokeFunction",
24
+ principal: principal,
25
+ source_arn: source_arn,
26
+ }
27
+ }
28
+ }
29
+ Attributes.new(attributes, @task)
30
+ end
31
+ memoize :attributes
32
+
33
+ # Auto-detect principal from the associated resources.
34
+ def principal
35
+ Replacer.principal_map(@resource_attributes.type)
36
+ end
37
+
38
+ def source_arn
39
+ default_arn = "!GetAtt #{@resource_attributes.logical_id}.Arn"
40
+ Replacer.source_arn_map(@resource_attributes.type) || default_arn
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,40 @@
1
+ module Jets::Resource
2
+ module Replacer
3
+ autoload :Base, 'jets/resource/replacer/base'
4
+ autoload :ConfigRule, 'jets/resource/replacer/config_rule'
5
+ # TODO: handle autoloading for plugins
6
+
7
+ class << self
8
+ def lookup(type)
9
+ klass = replacer_map[type] || "Jets::Resource::Replacer::Base"
10
+ klass.constantize
11
+ end
12
+
13
+ # Maps
14
+ # TODO: get rid of this map, and use a convention
15
+ # * connect a plugin to figure out interface.
16
+ # * add ability to explicitly override principal and source_arn.
17
+ def replacer_map
18
+ {
19
+ "AWS::Config::ConfigRule" => "Jets::Resource::Replacer::ConfigRule"
20
+ }
21
+ end
22
+
23
+ # Examples:
24
+ # "AWS::Events::Rule" => "events.amazonaws.com",
25
+ # "AWS::Config::ConfigRule" => "config.amazonaws.com",
26
+ # "AWS::ApiGateway::Method" => "apigateway.amazonaws.com"
27
+ def principal_map(type)
28
+ service = type.split('::')[1].downcase
29
+ "#{service}.amazonaws.com"
30
+ end
31
+
32
+ def source_arn_map(type)
33
+ map = {
34
+ "AWS::ApiGateway::Method" => "!Sub arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${RestApi}/*/*",
35
+ }
36
+ map[type]
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,98 @@
1
+ module Jets::Resource::Replacer
2
+ class Base
3
+ extend Memoist
4
+
5
+ def initialize(task)
6
+ @task = task
7
+ @app_class = task.class_name.to_s
8
+ end
9
+
10
+ # Replace placeholder hash values with replacements. This does a deep replacement
11
+ # to the hash values. The replacement "key" is the string value within the value.
12
+ #
13
+ # Example:
14
+ #
15
+ # attributes = {whatever: "foo REPLACE_KEY bar" }
16
+ # replace_placeholders(attributes, REPLACE_KEY: "blah:arn")
17
+ # => {whatever: "foo blah:arn bar" }
18
+ #
19
+ # Also, we always replace the special {namespace} value in the hash values. Example:
20
+ #
21
+ # attributes = {whatever: "{namespace}LambdaFunction" }
22
+ # replace_placeholders(attributes, {})
23
+ # => {whatever: "foo PostsControllerIndexLambdaFunction bar" }
24
+ #
25
+ def replace_placeholders(attributes, replacements={})
26
+ update_values(attributes, replacements)
27
+ end
28
+
29
+ def update_values(original, replacements={})
30
+ case original
31
+ when Array
32
+ original.map { |v| update_values(v, replacements) }
33
+ when Hash
34
+ initializer = original.map do |k, v|
35
+ [k, update_values(v, replacements)]
36
+ end
37
+ Hash[initializer]
38
+ else
39
+ replace_value(original, replacements)
40
+ end
41
+ end
42
+
43
+ def replace_value(text, replacements={})
44
+ text = text.to_s # normalize to String
45
+ # custom replacements
46
+ replacements.each do |k,v|
47
+ text = text.gsub(k.to_s, v)
48
+ end
49
+ # Values to always replace
50
+ text = replace_core_values(text)
51
+ text
52
+ end
53
+
54
+ # Values to always replace.
55
+ def replace_core_values(text)
56
+ text = text.gsub('{namespace}', namespace) # always replace namespace
57
+ core_replacements.each do |k,v|
58
+ text = text.gsub("{#{k}}", v)
59
+ end
60
+ text
61
+ end
62
+
63
+ # Meant to beoverriden by different resource types in the child class.
64
+ # These values replace the variables in the resource template.
65
+ #
66
+ # Example:
67
+ #
68
+ # In child class:
69
+ #
70
+ # def core_replacements
71
+ # { config_rule_name: "my-config-rule" }
72
+ # end
73
+ #
74
+ # And we declare these properties in the resource:
75
+ #
76
+ # properties: {
77
+ # config_rule_name: "{config_rule_name}",
78
+ # ...
79
+ #
80
+ # The replacements result in:
81
+ #
82
+ # properties: {
83
+ # config_rule_name: "my-config-rule",
84
+ # ...
85
+ #
86
+ def core_replacements
87
+ {}
88
+ end
89
+
90
+ # Full camelized namespace
91
+ # Example: HardJobDig, PostsControllerIndex, SleepJobPerform
92
+ def namespace
93
+ class_name = @task.class_name.gsub('::','')
94
+ function_name = @task.meth.to_s.camelize
95
+ "#{class_name}#{function_name}"
96
+ end
97
+ end
98
+ end