jets 0.7.1 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
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