cloud-mu 3.2.0 → 3.3.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 (91) hide show
  1. checksums.yaml +4 -4
  2. data/Dockerfile +1 -1
  3. data/bin/mu-adopt +12 -1
  4. data/bin/mu-load-config.rb +2 -1
  5. data/bin/mu-run-tests +14 -2
  6. data/cloud-mu.gemspec +3 -3
  7. data/modules/mu.rb +2 -2
  8. data/modules/mu/adoption.rb +5 -5
  9. data/modules/mu/cleanup.rb +47 -25
  10. data/modules/mu/cloud.rb +29 -1
  11. data/modules/mu/cloud/dnszone.rb +0 -2
  12. data/modules/mu/cloud/resource_base.rb +9 -3
  13. data/modules/mu/cloud/wrappers.rb +4 -0
  14. data/modules/mu/config.rb +1 -1
  15. data/modules/mu/config/bucket.rb +31 -2
  16. data/modules/mu/config/cache_cluster.rb +1 -1
  17. data/modules/mu/config/cdn.rb +100 -0
  18. data/modules/mu/config/container_cluster.rb +1 -1
  19. data/modules/mu/config/database.rb +1 -1
  20. data/modules/mu/config/dnszone.rb +4 -3
  21. data/modules/mu/config/endpoint.rb +1 -0
  22. data/modules/mu/config/function.rb +16 -7
  23. data/modules/mu/config/job.rb +89 -0
  24. data/modules/mu/config/notifier.rb +7 -18
  25. data/modules/mu/config/ref.rb +53 -7
  26. data/modules/mu/config/server.rb +1 -1
  27. data/modules/mu/config/vpc.rb +1 -0
  28. data/modules/mu/defaults/AWS.yaml +26 -26
  29. data/modules/mu/deploy.rb +13 -0
  30. data/modules/mu/master.rb +21 -0
  31. data/modules/mu/mommacat.rb +1 -0
  32. data/modules/mu/mommacat/daemon.rb +13 -7
  33. data/modules/mu/providers/aws.rb +115 -16
  34. data/modules/mu/providers/aws/alarm.rb +2 -2
  35. data/modules/mu/providers/aws/bucket.rb +274 -40
  36. data/modules/mu/providers/aws/cache_cluster.rb +4 -4
  37. data/modules/mu/providers/aws/cdn.rb +782 -0
  38. data/modules/mu/providers/aws/collection.rb +2 -2
  39. data/modules/mu/providers/aws/container_cluster.rb +57 -37
  40. data/modules/mu/providers/aws/database.rb +11 -11
  41. data/modules/mu/providers/aws/dnszone.rb +24 -7
  42. data/modules/mu/providers/aws/endpoint.rb +535 -50
  43. data/modules/mu/providers/aws/firewall_rule.rb +6 -3
  44. data/modules/mu/providers/aws/folder.rb +1 -1
  45. data/modules/mu/providers/aws/function.rb +288 -125
  46. data/modules/mu/providers/aws/group.rb +9 -7
  47. data/modules/mu/providers/aws/habitat.rb +2 -2
  48. data/modules/mu/providers/aws/job.rb +466 -0
  49. data/modules/mu/providers/aws/loadbalancer.rb +9 -8
  50. data/modules/mu/providers/aws/log.rb +3 -3
  51. data/modules/mu/providers/aws/msg_queue.rb +12 -3
  52. data/modules/mu/providers/aws/nosqldb.rb +96 -5
  53. data/modules/mu/providers/aws/notifier.rb +135 -63
  54. data/modules/mu/providers/aws/role.rb +51 -37
  55. data/modules/mu/providers/aws/search_domain.rb +165 -29
  56. data/modules/mu/providers/aws/server.rb +12 -9
  57. data/modules/mu/providers/aws/server_pool.rb +26 -13
  58. data/modules/mu/providers/aws/storage_pool.rb +2 -2
  59. data/modules/mu/providers/aws/user.rb +4 -4
  60. data/modules/mu/providers/aws/userdata/linux.erb +5 -4
  61. data/modules/mu/providers/aws/vpc.rb +3 -3
  62. data/modules/mu/providers/azure/server.rb +2 -1
  63. data/modules/mu/providers/google.rb +1 -0
  64. data/modules/mu/providers/google/bucket.rb +1 -1
  65. data/modules/mu/providers/google/container_cluster.rb +1 -1
  66. data/modules/mu/providers/google/database.rb +1 -1
  67. data/modules/mu/providers/google/firewall_rule.rb +1 -1
  68. data/modules/mu/providers/google/folder.rb +1 -1
  69. data/modules/mu/providers/google/function.rb +1 -1
  70. data/modules/mu/providers/google/group.rb +1 -1
  71. data/modules/mu/providers/google/habitat.rb +1 -1
  72. data/modules/mu/providers/google/loadbalancer.rb +1 -1
  73. data/modules/mu/providers/google/role.rb +4 -2
  74. data/modules/mu/providers/google/server.rb +1 -1
  75. data/modules/mu/providers/google/server_pool.rb +1 -1
  76. data/modules/mu/providers/google/user.rb +1 -1
  77. data/modules/mu/providers/google/vpc.rb +1 -1
  78. data/modules/tests/aws-jobs-functions.yaml +46 -0
  79. data/modules/tests/centos6.yaml +4 -0
  80. data/modules/tests/centos7.yaml +4 -0
  81. data/modules/tests/ecs.yaml +2 -2
  82. data/modules/tests/eks.yaml +1 -1
  83. data/modules/tests/functions/node-function/lambda_function.js +10 -0
  84. data/modules/tests/functions/python-function/lambda_function.py +12 -0
  85. data/modules/tests/microservice_app.yaml +288 -0
  86. data/modules/tests/rds.yaml +5 -5
  87. data/modules/tests/regrooms/rds.yaml +5 -5
  88. data/modules/tests/server-with-scrub-muisms.yaml +1 -1
  89. data/modules/tests/super_complex_bok.yml +2 -2
  90. data/modules/tests/super_simple_bok.yml +2 -2
  91. metadata +12 -4
@@ -381,14 +381,14 @@ module MU
381
381
  # @param ignoremaster [Boolean]: If true, will remove resources not flagged as originating from this Mu server
382
382
  # @param region [String]: The cloud provider region
383
383
  # @return [void]
384
- def self.cleanup(noop: false, ignoremaster: false, region: MU.curRegion, credentials: nil, flags: {})
384
+ def self.cleanup(noop: false, deploy_id: MU.deploy_id, ignoremaster: false, region: MU.curRegion, credentials: nil, flags: {})
385
385
  filters = if flags and flags["vpc_id"]
386
386
  [
387
387
  {name: "vpc-id", values: [flags["vpc_id"]]}
388
388
  ]
389
389
  else
390
390
  filters = [
391
- {name: "tag:MU-ID", values: [MU.deploy_id]}
391
+ {name: "tag:MU-ID", values: [deploy_id]}
392
392
  ]
393
393
  if !ignoremaster
394
394
  filters << {name: "tag:MU-MASTER-IP", values: [MU.mu_public_ip]}
@@ -860,8 +860,11 @@ module MU
860
860
  p_start = rule['port'].to_i
861
861
  p_end = rule['port'].to_i
862
862
  elsif rule['proto'] != "icmp"
863
- raise MuError, "Can't create a TCP or UDP security group rule without specifying ports: #{rule}"
863
+ MU.log "Can't create a TCP or UDP security group rule without specifying ports, assuming 'all'", MU::WARN, details: rule
864
+ p_start = "0"
865
+ p_end = "65535"
864
866
  end
867
+
865
868
  if rule['proto'] != "icmp"
866
869
  if p_start.nil? or p_end.nil?
867
870
  raise MuError, "Got nil ports out of rule #{rule}"
@@ -59,7 +59,7 @@ module MU
59
59
  # @param ignoremaster [Boolean]: If true, will remove resources not flagged as originating from this Mu server
60
60
  # @param region [String]: The cloud provider region
61
61
  # @return [void]
62
- def self.cleanup(noop: false, ignoremaster: false, region: MU.curRegion, credentials: nil, flags: {})
62
+ def self.cleanup(noop: false, deploy_id: MU.deploy_id, ignoremaster: false, region: MU.curRegion, credentials: nil, flags: {})
63
63
  end
64
64
 
65
65
  # Locate an existing AWS organization. If no identifying parameters are specified, this will return a description of the Organization which owns the account for our credentials.
@@ -18,6 +18,18 @@ module MU
18
18
  # A function as configured in {MU::Config::BasketofKittens::functions}
19
19
  class Function < MU::Cloud::Function
20
20
 
21
+ # If we have sibling resources in our deployment, automatically inject
22
+ # interesting things about them into our function's environment
23
+ # variables.
24
+ SIBLING_VARS = {
25
+ "servers" => ["private_ip_address", "public_ip_address"],
26
+ "search_domains" => ["endpoint"],
27
+ "databases" => ["endpoint"],
28
+ "endpoints" => ["url"],
29
+ "notifiers" => ["TopicArn"],
30
+ "nosqldbs" => ["table_arn"]
31
+ }
32
+
21
33
  # Initialize this cloud resource object. Calling +super+ will invoke the initializer defined under {MU::Cloud}, which should set the attribtues listed in {MU::Cloud::PUBLIC_ATTRS} as well as applicable dependency shortcuts, like +@vpc+, for us.
22
34
  # @param args [Hash]: Hash of named arguments passed via Ruby's double-splat
23
35
  def initialize(**args)
@@ -42,92 +54,44 @@ module MU
42
54
 
43
55
  # Called automatically by {MU::Deploy#createResources}
44
56
  def create
45
- role_arn = get_role_arn(@config['iam_role'])
46
57
 
47
- lambda_properties = {
48
- code: {},
49
- function_name: @mu_name,
50
- handler: @config['handler'],
51
- publish: true,
52
- role: role_arn,
53
- runtime: @config['runtime'],
54
- }
58
+ lambda_properties = get_properties
55
59
 
56
- if @config['code']['zip_file']
57
- zip = File.read(@config['code']['zip_file'])
58
- MU.log "Uploading deployment package from #{@config['code']['zip_file']}"
59
- lambda_properties[:code][:zip_file] = zip
60
- else
61
- lambda_properties[:code][:s3_bucket] = @config['code']['s3_bucket']
62
- lambda_properties[:code][:s3_key] = @config['code']['s3_key']
63
- if @config['code']['s3_object_version']
64
- lambda_properties[:code][:s3_object_version] = @config['code']['s3_object_version']
65
- end
66
- end
67
-
68
- if @config.has_key?('timeout')
69
- lambda_properties[:timeout] = @config['timeout'].to_i ## secs
70
- end
71
-
72
- if @config.has_key?('memory')
73
- lambda_properties[:memory_size] = @config['memory'].to_i
74
- end
75
-
76
- if @config.has_key?('environment_variables')
77
- lambda_properties[:environment] = {
78
- variables: {@config['environment_variables'][0]['key'] => @config['environment_variables'][0]['value']}
79
- }
80
- end
81
-
82
- lambda_properties[:tags] = {}
83
- MU::MommaCat.listStandardTags.each_pair { |k, v|
84
- lambda_properties[:tags][k] = v
60
+ MU.retrier([Aws::Lambda::Errors::InvalidParameterValueException], max: 5, wait: 10) {
61
+ resp = MU::Cloud::AWS.lambda(region: @config['region'], credentials: @config['credentials']).create_function(lambda_properties)
62
+ @cloud_id = resp.function_name
85
63
  }
86
- if @config['tags']
87
- @config['tags'].each { |tag|
88
- lambda_properties[:tags][tag.key.first] = tag.values.first
89
- }
90
- end
91
64
 
92
- if @config.has_key?('vpc')
93
- sgs = []
94
- if @config['add_firewall_rules']
95
- @config['add_firewall_rules'].each { |sg|
96
- sg = @deploy.findLitterMate(type: "firewall_rule", name: sg['name'])
97
- sgs << sg.cloud_id if sg and sg.cloud_id
98
- }
99
- end
100
- if !@vpc
101
- raise MuError, "Function #{@config['name']} had a VPC configured, but none was loaded"
102
- end
103
- lambda_properties[:vpc_config] = {
104
- :subnet_ids => @vpc.subnets.map { |s| s.cloud_id },
105
- :security_group_ids => sgs
106
- }
107
- end
108
-
109
- retries = 0
110
- resp = begin
111
- MU::Cloud::AWS.lambda(region: @config['region'], credentials: @config['credentials']).create_function(lambda_properties)
112
- rescue Aws::Lambda::Errors::InvalidParameterValueException => e
113
- # Freshly-made IAM roles sometimes aren't really ready
114
- if retries < 5
115
- sleep 10
116
- retries += 1
117
- retry
118
- end
119
- raise e
120
- end
121
-
122
- @cloud_id = resp.function_name
65
+ # the console does this and docs expect it to be there, so mimic the
66
+ # behavior
67
+ MU::Cloud::AWS.cloudwatchlogs(region: @config["region"], credentials: @credentials).create_log_group(
68
+ log_group_name: "/aws/lambda/#{@cloud_id}",
69
+ tags: @tags
70
+ )
123
71
  end
124
72
 
125
73
  # Called automatically by {MU::Deploy#createResources}
126
74
  def groom
127
- desc = MU::Cloud::AWS.lambda(region: @config['region'], credentials: @config['credentials']).get_function(
128
- function_name: @mu_name
129
- )
130
- func_arn = desc.configuration.function_arn if !desc.empty?
75
+ old_props = MU.structToHash(cloud_desc)
76
+
77
+ new_props = get_properties
78
+ code_block = new_props[:code]
79
+ new_props.reject! { |k, _v| [:code, :publish, :tags].include?(k) }
80
+ changes = {}
81
+ new_props.each_pair { |k, v|
82
+ changes[k] = v if v != old_props[k]
83
+ }
84
+ if !changes.empty?
85
+ MU.log "Updating Lambda #{@mu_name}", MU::NOTICE, details: changes
86
+ MU::Cloud::AWS.lambda(region: @config['region'], credentials: @config['credentials']).update_function_configuration(new_props)
87
+ end
88
+
89
+ if @code_sha256 and @code_sha256 != cloud_desc.code_sha_256.chomp
90
+ MU.log "Updating code in Lambda #{@mu_name}", MU::NOTICE, details: { "old" => @code_sha256, "new" => cloud_desc.code_sha_256 }
91
+ code_block[:publish] = true
92
+ code_block[:function_name] = @cloud_id
93
+ MU::Cloud::AWS.lambda(region: @config['region'], credentials: @config['credentials']).update_function_code(code_block)
94
+ end
131
95
 
132
96
  # tag_function = assign_tag(lambda_func.function_arn, @config['tags'])
133
97
 
@@ -141,7 +105,7 @@ module MU
141
105
  ### triggers must exist prior
142
106
  if @config['triggers']
143
107
  @config['triggers'].each { |tr|
144
- trigger_arn = assume_trigger_arns(tr['service'], tr['name'])
108
+ trigger_arn = resolveARN(tr['service'], tr['name'])
145
109
 
146
110
  trigger_properties = {
147
111
  action: "lambda:InvokeFunction",
@@ -151,15 +115,33 @@ module MU
151
115
  statement_id: "#{@mu_name}-ID-1",
152
116
  }
153
117
 
154
- MU.log trigger_properties, MU::DEBUG
118
+ MU.log "Adding #{tr['service']} #{tr['name']} trigger to Lambda function #{@cloud_id}", details: trigger_properties
155
119
  begin
156
120
  MU::Cloud::AWS.lambda(region: @config['region'], credentials: @config['credentials']).add_permission(trigger_properties)
157
121
  rescue Aws::Lambda::Errors::ResourceConflictException
122
+ # just means the permission is already there
158
123
  end
159
- adjust_trigger(tr['service'], trigger_arn, func_arn, @mu_name)
124
+ adjust_trigger(tr['service'], trigger_arn, arn, @mu_name)
160
125
  }
161
126
 
162
127
  end
128
+
129
+ if @config['invoke_on_completion']
130
+ invoke_params = {
131
+ function_name: @cloud_id,
132
+ invocation_type: @config['invoke_on_completion']['invocation_type'],
133
+ log_type: "Tail"
134
+ }
135
+ if @config['invoke_on_completion']['payload']
136
+ invoke_params[:payload] = JSON.generate(@config['invoke_on_completion']['payload'])
137
+ end
138
+ resp = MU::Cloud::AWS.lambda(region: @config['region'], credentials: @config['credentials']).invoke(invoke_params)
139
+ if resp.status_code == 200
140
+ MU.log "Invoked #{@cloud_id}", MU::NOTICE, details: Base64.decode64(resp.log_result)
141
+ else
142
+ MU.log "Invoked #{@cloud_id} and got #{resp.status_code} (#{resp.function_error})", MU::WARN, details: Base64.decode64(resp.log_result)
143
+ end
144
+ end
163
145
  end
164
146
 
165
147
  # Intended to be called by other Mu resources, such as Endpoints (API
@@ -170,13 +152,16 @@ module MU
170
152
  function_name: @mu_name,
171
153
  principal: "#{calling_service}.amazonaws.com",
172
154
  source_arn: calling_arn,
173
- statement_id: "#{calling_service}-#{calling_name}",
155
+ statement_id: "#{calling_service}-#{calling_name.gsub(/[^a-z0-9\-_]/i, '_')}",
174
156
  }
175
157
 
176
158
  begin
177
159
  # XXX There doesn't seem to be an API call to list or view existing
178
160
  # permissions, wtaf. This means we can't intelligently guard this.
179
161
  MU::Cloud::AWS.lambda(region: @config['region'], credentials: @config['credentials']).add_permission(trigger)
162
+ rescue Aws::Lambda::Errors::ValidationException => e
163
+ MU.log e.message+" (calling_arn: #{calling_arn}, calling_service: #{calling_service}, calling_name: #{calling_name})", MU::ERR, details: trigger
164
+ raise e
180
165
  rescue Aws::Lambda::Errors::ResourceConflictException => e
181
166
  if e.message.match(/already exists/)
182
167
  MU::Cloud::AWS.lambda(region: @config['region'], credentials: @config['credentials']).remove_permission(
@@ -192,17 +177,23 @@ module MU
192
177
  end
193
178
 
194
179
  # Look up an ARN for a given trigger type and resource name
195
- def assume_trigger_arns(svc, name)
196
- supported_triggers = %w(apigateway sns events event cloudwatch_event)
180
+ def resolveARN(svc, name)
181
+ supported_triggers = %w(apigateway sns events event cloudwatch_event dynamodb)
197
182
  if supported_triggers.include?(svc.downcase)
198
183
  arn = nil
199
184
  case svc.downcase
200
185
  when 'sns'
201
- arn = "arn:aws:sns:#{@config['region']}:#{MU::Cloud::AWS.credToAcct(@config['credentials'])}:#{name}"
186
+ sib_sns = @deploy.findLitterMate(name: name, type: "notifiers")
187
+ arn = sib_sns ? sib_sns.arn : "arn:aws:sns:#{@config['region']}:#{MU::Cloud::AWS.credToAcct(@config['credentials'])}:#{name}"
202
188
  when 'alarm','events', 'event', 'cloudwatch_event'
203
- arn = "arn:aws:events:#{@config['region']}:#{MU::Cloud::AWS.credToAcct(@config['credentials'])}:rule/#{name}"
189
+ sib_event = @deploy.findLitterMate(name: name, type: "job")
190
+ arn = sib_event ? sib_event.arn : "arn:aws:events:#{@config['region']}:#{MU::Cloud::AWS.credToAcct(@config['credentials'])}:rule/#{name}"
191
+ when 'dynamodb'
192
+ sib_dynamo = @deploy.findLitterMate(name: name, type: "nosqldb")
193
+ arn = sib_dynamo ? sib_dynamo.arn : "arn:aws:dynamodb:#{@config['region']}:#{MU::Cloud::AWS.credToAcct(@config['credentials'])}:table/#{name}"
204
194
  when 'apigateway'
205
- arn = "arn:aws:apigateway:#{@config['region']}:#{MU::Cloud::AWS.credToAcct(@config['credentials'])}:#{name}"
195
+ sib_apig = @deploy.findLitterMate(name: name, type: "endpoints")
196
+ arn = sib_apig ? sib_apig.arn : "arn:aws:apigateway:#{@config['region']}:#{MU::Cloud::AWS.credToAcct(@config['credentials'])}:#{name}"
206
197
  when 's3'
207
198
  arn = ''
208
199
  end
@@ -219,13 +210,21 @@ module MU
219
210
  case trig_type
220
211
 
221
212
  when 'sns'
222
- # XXX don't do this, use MU::Cloud::AWS::Notification
223
- sns_client = MU::Cloud::AWS.sns(region: region, credentials: @config['credentials'])
224
- sns_client.subscribe({
225
- topic_arn: trig_arn,
226
- protocol: protocol,
227
- endpoint: func_arn
228
- })
213
+ MU::Cloud.resourceClass("AWS", "Notifier").subscribe(trig_arn, arn, "lambda", region: @config['region'], credentials: @credentials)
214
+ when 'dynamodb'
215
+ stream = MU::Cloud::AWS.dynamostream(region: @config['region'], credentials: @config['credentials']).list_streams(table_name: trig_arn.sub(/.*?:table\//, '')).streams.first
216
+ # XXX guard this
217
+ MU.log "Adding DynamoDB Stream from #{stream.stream_arn} as trigger for #{@cloud_id}"
218
+ begin
219
+ MU::Cloud::AWS.lambda(region: @config['region'], credentials: @config['credentials']).create_event_source_mapping(
220
+ event_source_arn: stream.stream_arn,
221
+ function_name: @cloud_id,
222
+ starting_position: "TRIM_HORIZON" # ...whatever that is
223
+ )
224
+ rescue ::Aws::Lambda::Errors::ResourceConflictException
225
+ end
226
+
227
+ # MU::Cloud.resourceClass("AWS", "NoSQLDB").subscribe(trig_arn, arn, "lambda", region: @config['region'], credentials: @credentials)
229
228
  when 'event','cloudwatch_event', 'events'
230
229
  # XXX don't do this, use MU::Cloud::AWS::Log
231
230
  MU::Cloud::AWS.cloudwatch_events(region: region, credentials: @config['credentials']).put_targets({
@@ -237,9 +236,8 @@ module MU
237
236
  }
238
237
  ]
239
238
  })
240
- # when 'apigateway'
241
- # XXX this is actually happening in ::Endpoint... maybe...
242
- # MU.log "Creation of API Gateway integrations not yet implemented, you'll have to do this manually", MU::WARN, details: "(because we'll basically have to implement all of APIG for this)"
239
+ when 'apigateway'
240
+ addTrigger(trig_arn, "lambda", trig_arn.sub(/.*?([a-z0-9\-_]+)$/i, '\1'))
243
241
  end
244
242
  end
245
243
 
@@ -247,9 +245,8 @@ module MU
247
245
  # Return the metadata for this Function rule
248
246
  # @return [Hash]
249
247
  def notify
250
- deploy_struct = MU.structToHash(MU::Cloud::AWS::Function.find(cloud_id: @cloud_id, credentials: @config['credentials'], region: @config['region']).values.first)
251
- deploy_struct['mu_name'] = @mu_name
252
- return deploy_struct
248
+ return nil if !cloud_desc
249
+ MU.structToHash(cloud_desc, stringify_keys: true)
253
250
  end
254
251
 
255
252
  # Does this resource type exist as a global (cloud-wide) artifact, or
@@ -270,14 +267,14 @@ module MU
270
267
  # @param ignoremaster [Boolean]: If true, will remove resources not flagged as originating from this Mu server
271
268
  # @param region [String]: The cloud provider region
272
269
  # @return [void]
273
- def self.cleanup(noop: false, ignoremaster: false, region: MU.curRegion, credentials: nil, flags: {})
270
+ def self.cleanup(noop: false, deploy_id: MU.deploy_id, ignoremaster: false, region: MU.curRegion, credentials: nil, flags: {})
274
271
  MU.log "AWS::Function.cleanup: need to support flags['known']", MU::DEBUG, details: flags
275
272
 
276
273
  MU::Cloud::AWS.lambda(credentials: credentials, region: region).list_functions.functions.each { |f|
277
274
  desc = MU::Cloud::AWS.lambda(credentials: credentials, region: region).get_function(
278
275
  function_name: f.function_name
279
276
  )
280
- if desc.tags and desc.tags["MU-ID"] == MU.deploy_id and (desc.tags["MU-MASTER-IP"] == MU.mu_public_ip or ignoremaster)
277
+ if desc.tags and desc.tags["MU-ID"] == deploy_id and (desc.tags["MU-MASTER-IP"] == MU.mu_public_ip or ignoremaster)
281
278
  MU.log "Deleting Lambda function #{f.function_name}"
282
279
  if !noop
283
280
  MU::Cloud::AWS.lambda(credentials: credentials, region: region).delete_function(
@@ -292,7 +289,7 @@ module MU
292
289
  # Canonical Amazon Resource Number for this resource
293
290
  # @return [String]
294
291
  def arn
295
- cloud_desc.function_arn
292
+ cloud_desc ? cloud_desc.function_arn : nil
296
293
  end
297
294
 
298
295
  # Locate an existing function.
@@ -334,6 +331,20 @@ module MU
334
331
  bok['timeout'] = cloud_desc.timeout
335
332
 
336
333
  function = MU::Cloud::AWS.lambda(region: @config['region'], credentials: @credentials).get_function(function_name: bok['name'])
334
+ # event_srcs = MU::Cloud::AWS.lambda(region: @config['region'], credentials: @credentials).list_event_source_mappings(function_name: @cloud_id)
335
+ # if event_srcs and !event_srcs.event_source_mappings.empty?
336
+ # MU.log "dem mappings tho #{@cloud_id}", MU::WARN, details: event_srcs
337
+ # end
338
+
339
+ # begin
340
+ # invoke_cfg = MU::Cloud::AWS.lambda(region: @config['region'], credentials: @credentials).get_function_event_invoke_config(function_name: @cloud_id)
341
+ # MU.log "invoke config #{@cloud_id}", MU::WARN, details: invoke_cfg
342
+ # rescue ::Aws::Lambda::Errors::ResourceNotFoundException
343
+ # end
344
+
345
+ # MU.log @cloud_id, MU::WARN, details: cloud_desc if @cloud_id == "Espier-Scheduled-Scanner"
346
+ # MU.log "configuration #{@cloud_id}", MU::WARN, details: MU::Cloud::AWS.lambda(region: @config['region'], credentials: @credentials).get_function_configuration(function_name: @cloud_id) if @cloud_id == "Espier-Scheduled-Scanner"
347
+
337
348
 
338
349
  if function.code.repository_type == "S3"
339
350
  bok['code'] = {}
@@ -393,16 +404,29 @@ module MU
393
404
 
394
405
  if function.configuration.role
395
406
  shortname = function.configuration.role.sub(/.*?role\/([^\/]+)$/, '\1')
396
- MU.log shortname, MU::NOTICE, details: function.configuration.role
397
407
  bok['role'] = MU::Config::Ref.get(
398
408
  id: shortname,
399
- name: shortname,
400
409
  cloud: "AWS",
401
410
  type: "roles"
402
411
  )
403
412
  end
413
+
414
+ begin
415
+ pol = MU::Cloud::AWS.lambda(region: @config['region'], credentials: @credentials).get_policy(function_name: @cloud_id).policy
416
+ MU.log @cloud_id, MU::WARN, details: JSON.parse(pol) if @cloud_id == "ESPIER-DEV-2020080900-LN-ON-DEMAND-SCANNER"
417
+ if pol
418
+ bok['triggers'] ||= []
419
+ JSON.parse(pol)["Statement"].each { |s|
420
+ bok['triggers'] << {
421
+ "service" => s["Principal"]["Service"].sub(/\..*/, ''),
422
+ "name" => s["Resource"].sub(/.*?[:\/]([^:\/]+)$/, '\1')
423
+ }
424
+ }
425
+ end
426
+ rescue ::Aws::Lambda::Errors::ResourceNotFoundException
427
+ end
404
428
  #MU.log @cloud_id, MU::NOTICE, details: function
405
- # XXX triggers, permissions
429
+ # XXX permissions
406
430
 
407
431
  bok
408
432
  end
@@ -414,6 +438,22 @@ MU.log shortname, MU::NOTICE, details: function.configuration.role
414
438
  def self.schema(_config)
415
439
  toplevel_required = ["runtime"]
416
440
  schema = {
441
+ "invoke_on_completion" => {
442
+ "type" => "object",
443
+ "description" => "Setting this will cause this Lambda function to be invoked when its groom phase is complete.",
444
+ "required" => ["invocation_type"],
445
+ "properties" => {
446
+ "invocation_type" => {
447
+ "type" => "string",
448
+ "enum" => ["RequestResponse", "Event", "Dryrun"],
449
+ "default" => "RequestReponse"
450
+ },
451
+ "payload" => {
452
+ "type" => "object",
453
+ "description" => "Optional input to the function, which will be formatted as JSON and sent for execution"
454
+ }
455
+ }
456
+ },
417
457
  "triggers" => {
418
458
  "type" => "array",
419
459
  "items" => {
@@ -423,7 +463,7 @@ MU.log shortname, MU::NOTICE, details: function.configuration.role
423
463
  "properties" => {
424
464
  "service" => {
425
465
  "type" => "string",
426
- "enum" => %w{apigateway events s3 sns sqs dynamodb kinesis ses cognito alexa iot},
466
+ "enum" => %w{apigateway events s3 sns sqs dynamodb kinesis ses cognito alexa iot lex},
427
467
  "description" => "The name of the AWS service that will trigger this function"
428
468
  },
429
469
  "name" => {
@@ -482,6 +522,28 @@ MU.log shortname, MU::NOTICE, details: function.configuration.role
482
522
  def self.validateConfig(function, configurator)
483
523
  ok = true
484
524
 
525
+ if function['triggers']
526
+ function['triggers'].each { |t|
527
+ mu_type = if t["service"] == "sns"
528
+ "notifiers"
529
+ elsif t["service"] == "apigateway"
530
+ "endpoints"
531
+ elsif t["service"] == "s3"
532
+ "buckets"
533
+ elsif t["service"] == "dynamodb"
534
+ "nosqldbs"
535
+ elsif t["service"] == "events"
536
+ "jobs"
537
+ elsif t["service"] == "sqs"
538
+ "msg_queues"
539
+ end
540
+
541
+ if mu_type
542
+ MU::Config.addDependency(function, t['name'], mu_type, no_create_wait: true)
543
+ end
544
+ }
545
+ end
546
+
485
547
  if function['vpc']
486
548
  fwname = "lambda-#{function['name']}"
487
549
  # default to allowing pings, if no ingress_rules were specified
@@ -508,7 +570,10 @@ MU.log shortname, MU::NOTICE, details: function.configuration.role
508
570
  MU::Config.addDependency(function, fwname, "firewall_rule")
509
571
  end
510
572
 
511
- if !function['iam_role']
573
+ function['role'] ||= function['iam_role']
574
+ function.delete("iam_role")
575
+
576
+ if !function['role']
512
577
  policy_map = {
513
578
  "basic" => "AWSLambdaBasicExecutionRole",
514
579
  "kinesis" => "AWSLambdaKinesisExecutionRole",
@@ -537,9 +602,21 @@ MU.log shortname, MU::NOTICE, details: function.configuration.role
537
602
  }
538
603
  configurator.insertKitten(roledesc, "roles")
539
604
 
540
- function['iam_role'] = function['name']+"execrole"
605
+ function['role'] = function['name']+"execrole"
541
606
 
542
- MU::Config.addDependency(function, function['name']+"execrole", "role")
607
+ end
608
+
609
+ if function['role'].is_a?(String)
610
+ function['role'] = MU::Config::Ref.get(
611
+ name: function['role'],
612
+ type: "roles",
613
+ cloud: "AWS",
614
+ credentials: function['credentials']
615
+ )
616
+ end
617
+
618
+ if function['role']['name']
619
+ MU::Config.addDependency(function, function['role']['name'], "role")
543
620
  end
544
621
 
545
622
  ok
@@ -547,23 +624,109 @@ MU.log shortname, MU::NOTICE, details: function.configuration.role
547
624
 
548
625
  private
549
626
 
550
- # Given an IAM role name, resolve to ARN. Will attempt to identify any
551
- # sibling Mu role resources by this name first, and failing that, will
552
- # do a plain get_role() to the IAM API for the provided name.
553
- # @param name [String]
554
- def get_role_arn(name)
555
- sib_role = @deploy.findLitterMate(name: name, type: "roles")
556
- return sib_role.cloudobj.arn if sib_role
627
+ def get_properties
628
+ role_obj = MU::Config::Ref.get(@config['role']).kitten(@deploy, cloud: "AWS")
629
+ raise MuError.new "Failed to fetch object from role reference", details: @config['role'].to_h if !role_obj
557
630
 
558
- begin
559
- role = MU::Cloud::AWS.iam(credentials: @config['credentials']).get_role({
560
- role_name: name.to_s
561
- })
562
- return role['role']['arn']
563
- rescue StandardError => e
564
- MU.log "#{e}", MU::ERR
631
+ lambda_properties = {
632
+ code: {},
633
+ function_name: @mu_name,
634
+ handler: @config['handler'],
635
+ publish: true,
636
+ role: role_obj.arn,
637
+ runtime: @config['runtime'],
638
+ }
639
+
640
+ if @config['code']['zip_file'] or @config['code']['path']
641
+ tempfile = nil
642
+ if @config['code']['path']
643
+ tempfile = Tempfile.new
644
+ MU.log "#{@mu_name} using code at #{@config['code']['path']}"
645
+ MU::Master.zipDir(@config['code']['path'], tempfile.path)
646
+ @config['code']['zip_file'] = tempfile.path
647
+ else
648
+ MU.log "#{@mu_name} using code packaged at #{@config['code']['zip_file']}"
649
+ end
650
+ zip = File.read(@config['code']['zip_file'])
651
+ @code_sha256 = Base64.encode64(Digest::SHA256.digest(zip)).chomp
652
+ lambda_properties[:code][:zip_file] = zip
653
+ if tempfile
654
+ tempfile.close
655
+ tempfile.unlink
656
+ end
657
+ else
658
+ lambda_properties[:code][:s3_bucket] = @config['code']['s3_bucket']
659
+ lambda_properties[:code][:s3_key] = @config['code']['s3_key']
660
+ if @config['code']['s3_object_version']
661
+ lambda_properties[:code][:s3_object_version] = @config['code']['s3_object_version']
662
+ end
663
+ # XXX need to download to a temporarily file, read it in, and calculate the digest in order to trigger updates in groom
664
+ end
665
+
666
+ if @config.has_key?('timeout')
667
+ lambda_properties[:timeout] = @config['timeout'].to_i ## secs
668
+ end
669
+
670
+ if @config.has_key?('memory')
671
+ lambda_properties[:memory_size] = @config['memory'].to_i
672
+ end
673
+
674
+ SIBLING_VARS.each_key { |sib_type|
675
+ siblings = @deploy.findLitterMate(return_all: true, type: sib_type, cloud: "AWS")
676
+ if siblings
677
+ siblings.each_value { |sibling|
678
+ metadata = sibling.notify
679
+ if !metadata
680
+ MU.log "Failed to extract metadata from sibling #{sibling}", MU::WARN
681
+ next
682
+ end
683
+ SIBLING_VARS[sib_type].each { |var|
684
+ if metadata[var]
685
+ @config['environment_variables'] ||= []
686
+ @config['environment_variables'] << {
687
+ "key" => (sibling.config['name']+"_"+var).gsub(/[^a-z0-9_]/i, '_'),
688
+ "value" => metadata[var]
689
+ }
690
+ end
691
+ }
692
+ }
693
+ end
694
+ }
695
+
696
+ if @config.has_key?('environment_variables')
697
+ lambda_properties[:environment] = {
698
+ variables: Hash[@config['environment_variables'].map { |v| [v['key'], v['value']] }]
699
+ }
700
+ end
701
+
702
+ lambda_properties[:tags] = {}
703
+ MU::MommaCat.listStandardTags.each_pair { |k, v|
704
+ lambda_properties[:tags][k] = v
705
+ }
706
+ if @config['tags']
707
+ @config['tags'].each { |tag|
708
+ lambda_properties[:tags][tag.key.first] = tag.values.first
709
+ }
565
710
  end
566
- nil
711
+
712
+ if @config.has_key?('vpc')
713
+ sgs = []
714
+ if @config['add_firewall_rules']
715
+ @config['add_firewall_rules'].each { |sg|
716
+ sg = @deploy.findLitterMate(type: "firewall_rule", name: sg['name'])
717
+ sgs << sg.cloud_id if sg and sg.cloud_id
718
+ }
719
+ end
720
+ if !@vpc
721
+ raise MuError, "Function #{@config['name']} had a VPC configured, but none was loaded"
722
+ end
723
+ lambda_properties[:vpc_config] = {
724
+ :subnet_ids => @vpc.subnets.map { |s| s.cloud_id },
725
+ :security_group_ids => sgs
726
+ }
727
+ end
728
+
729
+ lambda_properties
567
730
  end
568
731
 
569
732
  end