cloud-mu 3.1.3 → 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 (212) hide show
  1. checksums.yaml +4 -4
  2. data/Dockerfile +15 -3
  3. data/ansible/roles/mu-windows/README.md +33 -0
  4. data/ansible/roles/mu-windows/defaults/main.yml +2 -0
  5. data/ansible/roles/mu-windows/files/LaunchConfig.json +9 -0
  6. data/ansible/roles/mu-windows/files/config.xml +76 -0
  7. data/ansible/roles/mu-windows/handlers/main.yml +2 -0
  8. data/ansible/roles/mu-windows/meta/main.yml +53 -0
  9. data/ansible/roles/mu-windows/tasks/main.yml +36 -0
  10. data/ansible/roles/mu-windows/tests/inventory +2 -0
  11. data/ansible/roles/mu-windows/tests/test.yml +5 -0
  12. data/ansible/roles/mu-windows/vars/main.yml +2 -0
  13. data/bin/mu-adopt +21 -13
  14. data/bin/mu-azure-tests +57 -0
  15. data/bin/mu-cleanup +2 -4
  16. data/bin/mu-configure +52 -0
  17. data/bin/mu-deploy +3 -3
  18. data/bin/mu-findstray-tests +25 -0
  19. data/bin/mu-gen-docs +2 -4
  20. data/bin/mu-load-config.rb +4 -4
  21. data/bin/mu-node-manage +15 -16
  22. data/bin/mu-run-tests +147 -37
  23. data/cloud-mu.gemspec +22 -20
  24. data/cookbooks/mu-activedirectory/resources/domain.rb +4 -4
  25. data/cookbooks/mu-activedirectory/resources/domain_controller.rb +4 -4
  26. data/cookbooks/mu-tools/libraries/helper.rb +3 -2
  27. data/cookbooks/mu-tools/libraries/monkey.rb +35 -0
  28. data/cookbooks/mu-tools/recipes/apply_security.rb +14 -14
  29. data/cookbooks/mu-tools/recipes/aws_api.rb +9 -0
  30. data/cookbooks/mu-tools/recipes/eks.rb +2 -2
  31. data/cookbooks/mu-tools/recipes/google_api.rb +2 -2
  32. data/cookbooks/mu-tools/recipes/selinux.rb +2 -1
  33. data/cookbooks/mu-tools/recipes/windows-client.rb +163 -164
  34. data/cookbooks/mu-tools/resources/disk.rb +1 -1
  35. data/cookbooks/mu-tools/resources/windows_users.rb +44 -43
  36. data/extras/clean-stock-amis +25 -19
  37. data/extras/generate-stock-images +1 -0
  38. data/extras/image-generators/AWS/win2k12.yaml +18 -13
  39. data/extras/image-generators/AWS/win2k16.yaml +18 -13
  40. data/extras/image-generators/AWS/win2k19.yaml +21 -0
  41. data/extras/image-generators/Google/centos6.yaml +1 -0
  42. data/extras/image-generators/Google/centos7.yaml +1 -1
  43. data/modules/mommacat.ru +6 -16
  44. data/modules/mu.rb +158 -111
  45. data/modules/mu/adoption.rb +404 -71
  46. data/modules/mu/cleanup.rb +221 -306
  47. data/modules/mu/cloud.rb +129 -1633
  48. data/modules/mu/cloud/database.rb +49 -0
  49. data/modules/mu/cloud/dnszone.rb +44 -0
  50. data/modules/mu/cloud/machine_images.rb +212 -0
  51. data/modules/mu/cloud/providers.rb +81 -0
  52. data/modules/mu/cloud/resource_base.rb +926 -0
  53. data/modules/mu/cloud/server.rb +40 -0
  54. data/modules/mu/cloud/server_pool.rb +1 -0
  55. data/modules/mu/cloud/ssh_sessions.rb +228 -0
  56. data/modules/mu/cloud/winrm_sessions.rb +237 -0
  57. data/modules/mu/cloud/wrappers.rb +169 -0
  58. data/modules/mu/config.rb +171 -1767
  59. data/modules/mu/config/alarm.rb +2 -6
  60. data/modules/mu/config/bucket.rb +32 -3
  61. data/modules/mu/config/cache_cluster.rb +2 -2
  62. data/modules/mu/config/cdn.rb +100 -0
  63. data/modules/mu/config/collection.rb +4 -4
  64. data/modules/mu/config/container_cluster.rb +9 -4
  65. data/modules/mu/config/database.rb +84 -105
  66. data/modules/mu/config/database.yml +1 -2
  67. data/modules/mu/config/dnszone.rb +10 -9
  68. data/modules/mu/config/doc_helpers.rb +516 -0
  69. data/modules/mu/config/endpoint.rb +5 -4
  70. data/modules/mu/config/firewall_rule.rb +103 -4
  71. data/modules/mu/config/folder.rb +4 -4
  72. data/modules/mu/config/function.rb +19 -10
  73. data/modules/mu/config/group.rb +4 -4
  74. data/modules/mu/config/habitat.rb +4 -4
  75. data/modules/mu/config/job.rb +89 -0
  76. data/modules/mu/config/loadbalancer.rb +60 -14
  77. data/modules/mu/config/log.rb +4 -4
  78. data/modules/mu/config/msg_queue.rb +4 -4
  79. data/modules/mu/config/nosqldb.rb +4 -4
  80. data/modules/mu/config/notifier.rb +10 -21
  81. data/modules/mu/config/ref.rb +411 -0
  82. data/modules/mu/config/role.rb +4 -4
  83. data/modules/mu/config/schema_helpers.rb +509 -0
  84. data/modules/mu/config/search_domain.rb +4 -4
  85. data/modules/mu/config/server.rb +98 -71
  86. data/modules/mu/config/server.yml +1 -0
  87. data/modules/mu/config/server_pool.rb +5 -9
  88. data/modules/mu/config/storage_pool.rb +1 -1
  89. data/modules/mu/config/tail.rb +200 -0
  90. data/modules/mu/config/user.rb +4 -4
  91. data/modules/mu/config/vpc.rb +71 -27
  92. data/modules/mu/config/vpc.yml +0 -1
  93. data/modules/mu/defaults/AWS.yaml +91 -68
  94. data/modules/mu/defaults/Azure.yaml +1 -0
  95. data/modules/mu/defaults/Google.yaml +3 -2
  96. data/modules/mu/deploy.rb +43 -26
  97. data/modules/mu/groomer.rb +17 -2
  98. data/modules/mu/groomers/ansible.rb +188 -41
  99. data/modules/mu/groomers/chef.rb +116 -55
  100. data/modules/mu/logger.rb +127 -148
  101. data/modules/mu/master.rb +410 -2
  102. data/modules/mu/master/chef.rb +3 -4
  103. data/modules/mu/master/ldap.rb +3 -3
  104. data/modules/mu/master/ssl.rb +12 -3
  105. data/modules/mu/mommacat.rb +218 -2612
  106. data/modules/mu/mommacat/daemon.rb +403 -0
  107. data/modules/mu/mommacat/naming.rb +473 -0
  108. data/modules/mu/mommacat/search.rb +495 -0
  109. data/modules/mu/mommacat/storage.rb +722 -0
  110. data/modules/mu/{clouds → providers}/README.md +1 -1
  111. data/modules/mu/{clouds → providers}/aws.rb +380 -122
  112. data/modules/mu/{clouds → providers}/aws/alarm.rb +7 -5
  113. data/modules/mu/{clouds → providers}/aws/bucket.rb +297 -59
  114. data/modules/mu/{clouds → providers}/aws/cache_cluster.rb +37 -71
  115. data/modules/mu/providers/aws/cdn.rb +782 -0
  116. data/modules/mu/{clouds → providers}/aws/collection.rb +26 -25
  117. data/modules/mu/{clouds → providers}/aws/container_cluster.rb +724 -744
  118. data/modules/mu/providers/aws/database.rb +1744 -0
  119. data/modules/mu/{clouds → providers}/aws/dnszone.rb +88 -70
  120. data/modules/mu/providers/aws/endpoint.rb +1072 -0
  121. data/modules/mu/{clouds → providers}/aws/firewall_rule.rb +220 -247
  122. data/modules/mu/{clouds → providers}/aws/folder.rb +8 -8
  123. data/modules/mu/{clouds → providers}/aws/function.rb +300 -142
  124. data/modules/mu/{clouds → providers}/aws/group.rb +31 -29
  125. data/modules/mu/{clouds → providers}/aws/habitat.rb +18 -15
  126. data/modules/mu/providers/aws/job.rb +466 -0
  127. data/modules/mu/{clouds → providers}/aws/loadbalancer.rb +66 -56
  128. data/modules/mu/{clouds → providers}/aws/log.rb +17 -14
  129. data/modules/mu/{clouds → providers}/aws/msg_queue.rb +29 -19
  130. data/modules/mu/{clouds → providers}/aws/nosqldb.rb +114 -16
  131. data/modules/mu/{clouds → providers}/aws/notifier.rb +142 -65
  132. data/modules/mu/{clouds → providers}/aws/role.rb +158 -118
  133. data/modules/mu/{clouds → providers}/aws/search_domain.rb +201 -59
  134. data/modules/mu/{clouds → providers}/aws/server.rb +844 -1139
  135. data/modules/mu/{clouds → providers}/aws/server_pool.rb +74 -65
  136. data/modules/mu/{clouds → providers}/aws/storage_pool.rb +26 -44
  137. data/modules/mu/{clouds → providers}/aws/user.rb +24 -25
  138. data/modules/mu/{clouds → providers}/aws/userdata/README.md +0 -0
  139. data/modules/mu/{clouds → providers}/aws/userdata/linux.erb +5 -4
  140. data/modules/mu/{clouds → providers}/aws/userdata/windows.erb +2 -1
  141. data/modules/mu/{clouds → providers}/aws/vpc.rb +525 -931
  142. data/modules/mu/providers/aws/vpc_subnet.rb +286 -0
  143. data/modules/mu/{clouds → providers}/azure.rb +29 -9
  144. data/modules/mu/{clouds → providers}/azure/container_cluster.rb +3 -8
  145. data/modules/mu/{clouds → providers}/azure/firewall_rule.rb +18 -11
  146. data/modules/mu/{clouds → providers}/azure/habitat.rb +8 -6
  147. data/modules/mu/{clouds → providers}/azure/loadbalancer.rb +5 -5
  148. data/modules/mu/{clouds → providers}/azure/role.rb +8 -10
  149. data/modules/mu/{clouds → providers}/azure/server.rb +97 -49
  150. data/modules/mu/{clouds → providers}/azure/user.rb +6 -8
  151. data/modules/mu/{clouds → providers}/azure/userdata/README.md +0 -0
  152. data/modules/mu/{clouds → providers}/azure/userdata/linux.erb +0 -0
  153. data/modules/mu/{clouds → providers}/azure/userdata/windows.erb +0 -0
  154. data/modules/mu/{clouds → providers}/azure/vpc.rb +16 -21
  155. data/modules/mu/{clouds → providers}/cloudformation.rb +18 -7
  156. data/modules/mu/{clouds → providers}/cloudformation/alarm.rb +3 -3
  157. data/modules/mu/{clouds → providers}/cloudformation/cache_cluster.rb +3 -3
  158. data/modules/mu/{clouds → providers}/cloudformation/collection.rb +3 -3
  159. data/modules/mu/{clouds → providers}/cloudformation/database.rb +6 -17
  160. data/modules/mu/{clouds → providers}/cloudformation/dnszone.rb +3 -3
  161. data/modules/mu/{clouds → providers}/cloudformation/firewall_rule.rb +3 -3
  162. data/modules/mu/{clouds → providers}/cloudformation/loadbalancer.rb +3 -3
  163. data/modules/mu/{clouds → providers}/cloudformation/log.rb +3 -3
  164. data/modules/mu/{clouds → providers}/cloudformation/server.rb +7 -7
  165. data/modules/mu/{clouds → providers}/cloudformation/server_pool.rb +5 -5
  166. data/modules/mu/{clouds → providers}/cloudformation/vpc.rb +5 -7
  167. data/modules/mu/{clouds → providers}/docker.rb +0 -0
  168. data/modules/mu/{clouds → providers}/google.rb +68 -30
  169. data/modules/mu/{clouds → providers}/google/bucket.rb +13 -15
  170. data/modules/mu/{clouds → providers}/google/container_cluster.rb +85 -78
  171. data/modules/mu/{clouds → providers}/google/database.rb +11 -21
  172. data/modules/mu/{clouds → providers}/google/firewall_rule.rb +15 -14
  173. data/modules/mu/{clouds → providers}/google/folder.rb +20 -17
  174. data/modules/mu/{clouds → providers}/google/function.rb +140 -168
  175. data/modules/mu/{clouds → providers}/google/group.rb +29 -34
  176. data/modules/mu/{clouds → providers}/google/habitat.rb +21 -22
  177. data/modules/mu/{clouds → providers}/google/loadbalancer.rb +19 -21
  178. data/modules/mu/{clouds → providers}/google/role.rb +94 -58
  179. data/modules/mu/{clouds → providers}/google/server.rb +243 -156
  180. data/modules/mu/{clouds → providers}/google/server_pool.rb +26 -45
  181. data/modules/mu/{clouds → providers}/google/user.rb +95 -31
  182. data/modules/mu/{clouds → providers}/google/userdata/README.md +0 -0
  183. data/modules/mu/{clouds → providers}/google/userdata/linux.erb +0 -0
  184. data/modules/mu/{clouds → providers}/google/userdata/windows.erb +0 -0
  185. data/modules/mu/{clouds → providers}/google/vpc.rb +103 -79
  186. data/modules/tests/aws-jobs-functions.yaml +46 -0
  187. data/modules/tests/bucket.yml +4 -0
  188. data/modules/tests/centos6.yaml +15 -0
  189. data/modules/tests/centos7.yaml +15 -0
  190. data/modules/tests/centos8.yaml +12 -0
  191. data/modules/tests/ecs.yaml +23 -0
  192. data/modules/tests/eks.yaml +1 -1
  193. data/modules/tests/functions/node-function/lambda_function.js +10 -0
  194. data/modules/tests/functions/python-function/lambda_function.py +12 -0
  195. data/modules/tests/includes-and-params.yaml +2 -1
  196. data/modules/tests/microservice_app.yaml +288 -0
  197. data/modules/tests/rds.yaml +108 -0
  198. data/modules/tests/regrooms/aws-iam.yaml +201 -0
  199. data/modules/tests/regrooms/bucket.yml +19 -0
  200. data/modules/tests/regrooms/rds.yaml +123 -0
  201. data/modules/tests/server-with-scrub-muisms.yaml +2 -1
  202. data/modules/tests/super_complex_bok.yml +2 -2
  203. data/modules/tests/super_simple_bok.yml +3 -5
  204. data/modules/tests/win2k12.yaml +17 -5
  205. data/modules/tests/win2k16.yaml +25 -0
  206. data/modules/tests/win2k19.yaml +25 -0
  207. data/requirements.txt +1 -0
  208. data/spec/mu/clouds/azure_spec.rb +2 -2
  209. metadata +240 -154
  210. data/extras/image-generators/AWS/windows.yaml +0 -18
  211. data/modules/mu/clouds/aws/database.rb +0 -1985
  212. data/modules/mu/clouds/aws/endpoint.rb +0 -592
@@ -61,7 +61,7 @@ module MU
61
61
  ]
62
62
  }
63
63
 
64
- keypairname, ssh_private_key, ssh_public_key = @deploy.SSHKey
64
+ keypairname, _ssh_private_key, _ssh_public_key = @deploy.SSHKey
65
65
 
66
66
  parameters = Array.new
67
67
  if !@config["parameters"].nil?
@@ -108,7 +108,7 @@ module MU
108
108
  end
109
109
 
110
110
  MU.log "Creating CloudFormation stack '#{@config['name']}'", details: stack_descriptor
111
- res = MU::Cloud::AWS.cloudformation(region: region, credentials: @config['credentials']).create_stack(stack_descriptor);
111
+ MU::Cloud::AWS.cloudformation(region: region, credentials: @config['credentials']).create_stack(stack_descriptor);
112
112
 
113
113
  sleep(10);
114
114
  stack_response = MU::Cloud::AWS.cloudformation(region: region, credentials: @config['credentials']).describe_stacks({:stack_name => stack_name}).stacks.first
@@ -135,7 +135,7 @@ module MU
135
135
  end
136
136
 
137
137
  if flag == "FAIL" then
138
- stack_response = MU::Cloud::AWS.cloudformation(region: region, credentials: @config['credentials']).delete_stack({:stack_name => stack_name})
138
+ MU::Cloud::AWS.cloudformation(region: region, credentials: @config['credentials']).delete_stack({:stack_name => stack_name})
139
139
  exit 1
140
140
  end
141
141
 
@@ -150,14 +150,14 @@ module MU
150
150
  when "AWS::EC2::Instance"
151
151
  MU::Cloud::AWS.createStandardTags(resource.physical_resource_id)
152
152
  instance_name = MU.deploy_id+"-"+@config['name']+"-"+resource.logical_resource_id
153
- MU::MommaCat.createTag(resource.physical_resource_id, "Name", instance_name, credentials: @config['credentials'])
153
+ MU::Cloud::AWS.createTag(resource.physical_resource_id, "Name", instance_name, credentials: @config['credentials'])
154
154
 
155
- instance = MU::Cloud::AWS::Server.notifyDeploy(
155
+ instance = MU::Cloud.resourceClass("AWS", "Server").notifyDeploy(
156
156
  @config['name']+"-"+resource.logical_resource_id,
157
157
  resource.physical_resource_id
158
158
  )
159
159
 
160
- MU::MommaCat.addHostToSSHConfig(
160
+ MU::Master.addHostToSSHConfig(
161
161
  instance_name,
162
162
  instance["private_ip_address"],
163
163
  instance["private_dns_name"],
@@ -168,23 +168,23 @@ module MU
168
168
  key_name: instance["key_name"]
169
169
  )
170
170
 
171
- mu_zone, junk = MU::Cloud::DNSZone.find(name: "mu")
171
+ mu_zone, _junk = MU::Cloud::DNSZone.find(name: "mu")
172
172
  if !mu_zone.nil?
173
- MU::Cloud::AWS::DNSZone.genericMuDNSEntry(instance_name, instance["private_ip_address"], MU::Cloud::Server)
173
+ MU::Cloud.resourceClass("AWS", "DNSZone").genericMuDNSEntry(instance_name, instance["private_ip_address"], MU::Cloud::Server)
174
174
  else
175
- MU::MommaCat.addInstanceToEtcHosts(instance["public_ip_address"], instance_name)
175
+ MU::Master.addInstanceToEtcHosts(instance["public_ip_address"], instance_name)
176
176
  end
177
177
 
178
178
  when "AWS::EC2::SecurityGroup"
179
179
  MU::Cloud::AWS.createStandardTags(resource.physical_resource_id)
180
- MU::MommaCat.createTag(resource.physical_resource_id, "Name", MU.deploy_id+"-"+@config['name']+'-'+resource.logical_resource_id, credentials: @config['credentials'])
181
- MU::Cloud::AWS::FirewallRule.notifyDeploy(
180
+ MU::Cloud::AWS.createTag(resource.physical_resource_id, "Name", MU.deploy_id+"-"+@config['name']+'-'+resource.logical_resource_id, credentials: @config['credentials'])
181
+ MU::Cloud.resourceClass("AWS", "FirewallRule").notifyDeploy(
182
182
  @config['name']+"-"+resource.logical_resource_id,
183
183
  resource.physical_resource_id
184
184
  )
185
185
  when "AWS::EC2::Subnet"
186
186
  MU::Cloud::AWS.createStandardTags(resource.physical_resource_id)
187
- MU::MommaCat.createTag(resource.physical_resource_id, "Name", MU.deploy_id+"-"+@config['name']+'-'+resource.logical_resource_id, credentials: @config['credentials'])
187
+ MU::Cloud::AWS.createTag(resource.physical_resource_id, "Name", MU.deploy_id+"-"+@config['name']+'-'+resource.logical_resource_id, credentials: @config['credentials'])
188
188
  data = {
189
189
  "collection" => @config["name"],
190
190
  "subnet_id" => resource.physical_resource_id,
@@ -192,7 +192,7 @@ module MU
192
192
  @deploy.notify("subnets", @config['name']+"-"+resource.logical_resource_id, data)
193
193
  when "AWS::EC2::VPC"
194
194
  MU::Cloud::AWS.createStandardTags(resource.physical_resource_id)
195
- MU::MommaCat.createTag(resource.physical_resource_id, "Name", MU.deploy_id+"-"+@config['name']+'-'+resource.logical_resource_id, credentials: @config['credentials'])
195
+ MU::Cloud::AWS.createTag(resource.physical_resource_id, "Name", MU.deploy_id+"-"+@config['name']+'-'+resource.logical_resource_id, credentials: @config['credentials'])
196
196
  data = {
197
197
  "collection" => @config["name"],
198
198
  "vpc_id" => resource.physical_resource_id,
@@ -200,10 +200,10 @@ module MU
200
200
  @deploy.notify("vpcs", @config['name']+"-"+resource.logical_resource_id, data)
201
201
  when "AWS::EC2::InternetGateway"
202
202
  MU::Cloud::AWS.createStandardTags(resource.physical_resource_id)
203
- MU::MommaCat.createTag(resource.physical_resource_id, "Name", MU.deploy_id+"-"+@config['name']+'-'+resource.logical_resource_id, credentials: @config['credentials'])
203
+ MU::Cloud::AWS.createTag(resource.physical_resource_id, "Name", MU.deploy_id+"-"+@config['name']+'-'+resource.logical_resource_id, credentials: @config['credentials'])
204
204
  when "AWS::EC2::RouteTable"
205
205
  MU::Cloud::AWS.createStandardTags(resource.physical_resource_id)
206
- MU::MommaCat.createTag(resource.physical_resource_id, "Name", MU.deploy_id+"-"+@config['name']+'-'+resource.logical_resource_id, credentials: @config['credentials'])
206
+ MU::Cloud::AWS.createTag(resource.physical_resource_id, "Name", MU.deploy_id+"-"+@config['name']+'-'+resource.logical_resource_id, credentials: @config['credentials'])
207
207
 
208
208
  # The rest of these aren't anything we act on
209
209
  when "AWS::EC2::Route"
@@ -242,13 +242,16 @@ module MU
242
242
  # @param region [String]: The cloud provider region
243
243
  # @param wait [Boolean]: Block on the removal of this stack; AWS deletion will continue in the background otherwise if false.
244
244
  # @return [void]
245
- def self.cleanup(noop: false, ignoremaster: false, region: MU.curRegion, wait: false, credentials: nil, flags: {})
245
+ def self.cleanup(noop: false, deploy_id: MU.deploy_id, ignoremaster: false, region: MU.curRegion, wait: false, credentials: nil, flags: {})
246
+ MU.log "AWS::Collection.cleanup: need to support flags['known']", MU::DEBUG, details: flags
247
+ MU.log "Placeholder: AWS Collection artifacts do not support tags, so ignoremaster cleanup flag has no effect", MU::DEBUG, details: ignoremaster
248
+
246
249
  # XXX needs to check tags instead of name- possible?
247
250
  resp = MU::Cloud::AWS.cloudformation(credentials: credentials, region: region).describe_stacks
248
251
  resp.stacks.each { |stack|
249
252
  ok = false
250
253
  stack.tags.each { |tag|
251
- ok = true if (tag.key == "MU-ID") and tag.value == MU.deploy_id
254
+ ok = true if (tag.key == "MU-ID") and tag.value == deploy_id
252
255
  }
253
256
  if ok
254
257
  MU.log "Deleting CloudFormation stack #{stack.stack_name})"
@@ -257,7 +260,6 @@ module MU
257
260
  MU::Cloud::AWS.cloudformation(credentials: credentials, region: region).delete_stack(stack_name: stack.stack_name)
258
261
  end
259
262
  if wait
260
- last_status = ""
261
263
  max_retries = 10
262
264
  retries = 0
263
265
  mystack = nil
@@ -272,10 +274,9 @@ module MU
272
274
  MU.log "Couldn't delete CloudFormation stack #{stack.stack_name}", MU::ERR, details: mystack.stack_status_reason
273
275
  return
274
276
  end
275
- last_status = mystack.stack_status_reason
276
277
  MU.log "Waiting for CloudFormation stack #{stack.stack_name} to delete (#{stack.stack_status})...", MU::NOTICE
277
278
  end
278
- rescue Aws::CloudFormation::Errors::ValidationError => e
279
+ rescue Aws::CloudFormation::Errors::ValidationError
279
280
  # this is ok, it means deletion finally succeeded
280
281
 
281
282
  end while !desc.nil? and desc.size > 0 and retries < max_retries
@@ -317,19 +318,19 @@ module MU
317
318
  end
318
319
 
319
320
  # Cloud-specific configuration properties.
320
- # @param config [MU::Config]: The calling MU::Config object
321
+ # @param _config [MU::Config]: The calling MU::Config object
321
322
  # @return [Array<Array,Hash>]: List of required fields, and json-schema Hash of cloud-specific configuration parameters for this resource
322
- def self.schema(config)
323
+ def self.schema(_config)
323
324
  toplevel_required = []
324
325
  schema = {}
325
326
  [toplevel_required, schema]
326
327
  end
327
328
 
328
329
  # Cloud-specific pre-processing of {MU::Config::BasketofKittens::collections}, bare and unvalidated.
329
- # @param stack [Hash]: The resource to process and validate
330
- # @param configurator [MU::Config]: The overall deployment configurator of which this resource is a member
330
+ # @param _stack [Hash]: The resource to process and validate
331
+ # @param _configurator [MU::Config]: The overall deployment configurator of which this resource is a member
331
332
  # @return [Boolean]: True if validation succeeded, False otherwise
332
- def self.validateConfig(stack, configurator)
333
+ def self.validateConfig(_stack, _configurator)
333
334
  true
334
335
  end
335
336
 
@@ -39,123 +39,81 @@ module MU
39
39
  def create
40
40
  if @config['flavor'] == "EKS" or
41
41
  (@config['flavor'] == "Fargate" and !@config['containers'])
42
- subnet_ids = []
43
- @config["vpc"]["subnets"].each { |subnet|
44
- subnet_obj = @vpc.getSubnet(cloud_id: subnet["subnet_id"].to_s, name: subnet["subnet_name"].to_s)
45
- raise MuError, "Couldn't find a live subnet matching #{subnet} in #{@vpc} (#{@vpc.subnets})" if subnet_obj.nil?
46
- subnet_ids << subnet_obj.cloud_id
47
- }
48
42
 
49
- role_arn = @deploy.findLitterMate(name: @config['name']+"controlplane", type: "roles").arn
43
+ subnet_ids = mySubnets.map { |s| s.cloud_id }
50
44
 
51
- security_groups = []
52
- if @dependencies.has_key?("firewall_rule")
53
- @dependencies['firewall_rule'].values.each { |sg|
54
- security_groups << sg.cloud_id
45
+ params = {
46
+ :name => @mu_name,
47
+ :version => @config['kubernetes']['version'],
48
+ :role_arn => @deploy.findLitterMate(name: @config['name']+"controlplane", type: "roles").arn,
49
+ :resources_vpc_config => {
50
+ :security_group_ids => myFirewallRules.map { |fw| fw.cloud_id },
51
+ :subnet_ids => subnet_ids
52
+ }
53
+ }
54
+ if @config['logging'] and @config['logging'].size > 0
55
+ params[:logging] = {
56
+ :cluster_logging => [
57
+ {
58
+ :types => @config['logging'],
59
+ :enabled => true
60
+ }
61
+ ]
55
62
  }
56
63
  end
64
+ params.delete(:version) if params[:version] == "latest"
57
65
 
58
- resp = nil
59
- begin
60
- params = {
61
- :name => @mu_name,
62
- :version => @config['kubernetes']['version'],
63
- :role_arn => role_arn,
64
- :resources_vpc_config => {
65
- :security_group_ids => security_groups,
66
- :subnet_ids => subnet_ids
66
+ on_retry = Proc.new { |e|
67
+ # soul-crushing, yet effective
68
+ if e.message.match(/because (#{Regexp.quote(@config['region'])}[a-z]), the targeted availability zone, does not currently have sufficient capacity/)
69
+ bad_az = Regexp.last_match(1)
70
+ deletia = []
71
+ mySubnets.each { |subnet|
72
+ deletia << subnet.cloud_id if subnet.az == bad_az
67
73
  }
68
- }
69
- if @config['logging'] and @config['logging'].size > 0
70
- params[:logging] = {
71
- :cluster_logging => [
72
- {
73
- :types => @config['logging'],
74
- :enabled => true
75
- }
76
- ]
74
+ raise e if deletia.empty?
75
+ MU.log "#{bad_az} does not have EKS capacity. Dropping unsupported subnets from ContainerCluster '#{@config['name']}' and retrying.", MU::NOTICE, details: deletia
76
+ deletia.each { |subnet|
77
+ params[:resources_vpc_config][:subnet_ids].delete(subnet)
77
78
  }
78
79
  end
79
- params.delete(:version) if params[:version] == "latest"
80
+ }
80
81
 
82
+ MU.retrier([Aws::EKS::Errors::UnsupportedAvailabilityZoneException, Aws::EKS::Errors::InvalidParameterException], on_retry: on_retry, max: subnet_ids.size) {
81
83
  MU.log "Creating EKS cluster #{@mu_name}", details: params
82
- resp = MU::Cloud::AWS.eks(region: @config['region'], credentials: @config['credentials']).create_cluster(params)
83
- rescue Aws::EKS::Errors::UnsupportedAvailabilityZoneException => e
84
- # this isn't the dumbest thing we've ever done, but it's up there
85
- if e.message.match(/because (#{Regexp.quote(@config['region'])}[a-z]), the targeted availability zone, does not currently have sufficient capacity/)
86
- bad_az = Regexp.last_match(1)
87
- deletia = nil
88
- subnet_ids.each { |subnet|
89
- subnet_obj = @vpc.getSubnet(cloud_id: subnet)
90
- if subnet_obj.az == bad_az
91
- deletia = subnet
92
- break
93
- end
94
- }
95
- raise e if deletia.nil?
96
- MU.log "#{bad_az} does not have EKS capacity. Dropping #{deletia} from ContainerCluster '#{@config['name']}' and retrying.", MU::NOTICE
97
- subnet_ids.delete(deletia)
98
- retry
99
- end
100
- rescue Aws::EKS::Errors::InvalidParameterException => e
101
- if e.message.match(/role with arn: #{Regexp.quote(role_arn)}.*?(could not be assumed|does not exist)/i)
102
- sleep 5
103
- retry
104
- else
105
- MU.log e.message, MU::WARN, details: params
106
- sleep 5
107
- retry
108
- end
109
- end
84
+ MU::Cloud::AWS.eks(region: @config['region'], credentials: @config['credentials']).create_cluster(params)
85
+ }
86
+ @cloud_id = @mu_name
110
87
 
111
- status = nil
112
- retries = 0
113
- begin
114
- resp = MU::Cloud::AWS.eks(region: @config['region'], credentials: @config['credentials']).describe_cluster(
115
- name: @mu_name
116
- )
117
- status = resp.cluster.status
118
- if status == "FAILED"
88
+ loop_if = Proc.new {
89
+ cloud_desc(use_cache: false).status != "ACTIVE"
90
+ }
91
+
92
+ MU.retrier(ignoreme: [Aws::EKS::Errors::ResourceNotFoundException], wait: 30, max: 60, loop_if: loop_if) { |retries, _wait|
93
+ if cloud_desc.status == "FAILED"
119
94
  raise MuError, "EKS cluster #{@mu_name} had FAILED status"
120
95
  end
121
- if retries > 0 and (retries % 3) == 0 and status != "ACTIVE"
122
- MU.log "Waiting for EKS cluster #{@mu_name} to become active (currently #{status})", MU::NOTICE
123
- end
124
- sleep 30
125
- retries += 1
126
- rescue Aws::EKS::Errors::ResourceNotFoundException => e
127
- if retries < 30
128
- if retries > 0 and (retries % 3) == 0
129
- MU.log "Got #{e.message} trying to describe EKS cluster #{@mu_name}, waiting and retrying", MU::WARN, details: resp
130
- end
131
- sleep 30
132
- retries += 1
133
- retry
134
- else
135
- raise e
96
+ if retries > 0 and (retries % 3) == 0 and cloud_desc.status != "ACTIVE"
97
+ MU.log "Waiting for EKS cluster #{@mu_name} to become active (currently #{cloud_desc.status})", MU::NOTICE
136
98
  end
137
- end while status != "ACTIVE"
99
+ }
138
100
 
139
101
  MU.log "Creation of EKS cluster #{@mu_name} complete"
140
102
  else
141
103
  MU::Cloud::AWS.ecs(region: @config['region'], credentials: @config['credentials']).create_cluster(
142
104
  cluster_name: @mu_name
143
105
  )
144
-
106
+ @cloud_id = @mu_name
145
107
  end
146
- @cloud_id = @mu_name
147
108
  end
148
109
 
149
110
  # Called automatically by {MU::Deploy#createResources}
150
111
  def groom
151
112
 
152
- serverpool = if ['EKS', 'ECS'].include?(@config['flavor'])
153
- @deploy.findLitterMate(type: "server_pools", name: @config["name"]+"workers")
154
- end
155
- resource_lookup = MU::Cloud::AWS.listInstanceTypes(@config['region'])[@config['region']]
156
-
113
+ # EKS or Fargate-EKS: do Kubernetes things
157
114
  if @config['flavor'] == "EKS" or
158
115
  (@config['flavor'] == "Fargate" and !@config['containers'])
116
+
159
117
  # This will be needed if a loadbalancer has never been created in
160
118
  # this account; EKS applications might want one, but will fail in
161
119
  # confusing ways if this hasn't been done.
@@ -166,228 +124,20 @@ module MU
166
124
  rescue ::Aws::IAM::Errors::InvalidInput
167
125
  end
168
126
 
169
- kube = ERB.new(File.read(MU.myRoot+"/cookbooks/mu-tools/templates/default/kubeconfig-eks.erb"))
170
- configmap = ERB.new(File.read(MU.myRoot+"/extras/aws-auth-cm.yaml.erb"))
171
- tagme = [@vpc.cloud_id]
172
- tagme_elb = []
173
- @vpc.subnets.each { |s|
174
- tagme << s.cloud_id
175
- tagme_elb << s.cloud_id if !s.private?
176
- }
177
- rtbs = MU::Cloud::AWS.ec2(region: @config['region'], credentials: @config['credentials']).describe_route_tables(
178
- filters: [ { name: "vpc-id", values: [@vpc.cloud_id] } ]
179
- ).route_tables
180
- tagme.concat(rtbs.map { |r| r.route_table_id } )
181
- main_sg = @deploy.findLitterMate(type: "firewall_rules", name: "server_pool#{@config['name']}workers")
182
- tagme << main_sg.cloud_id if main_sg
183
- MU.log "Applying kubernetes.io tags to VPC resources", details: tagme
184
- MU::Cloud::AWS.createTag("kubernetes.io/cluster/#{@mu_name}", "shared", tagme, credentials: @config['credentials'])
185
- MU::Cloud::AWS.createTag("kubernetes.io/cluster/elb", @mu_name, tagme_elb, credentials: @config['credentials'])
186
-
187
- if @config['flavor'] == "Fargate"
188
- fargate_subnets = []
189
- @config["vpc"]["subnets"].each { |subnet|
190
- subnet_obj = @vpc.getSubnet(cloud_id: subnet["subnet_id"].to_s, name: subnet["subnet_name"].to_s)
191
- raise MuError, "Couldn't find a live subnet matching #{subnet} in #{@vpc} (#{@vpc.subnets})" if subnet_obj.nil?
192
- next if !subnet_obj.private?
193
- fargate_subnets << subnet_obj.cloud_id
194
- }
195
- podrole_arn = @deploy.findLitterMate(name: @config['name']+"pods", type: "roles").arn
196
- poolnum = 0
197
- poolthreads =[]
198
- @config['kubernetes_pools'].each { |selectors|
199
- profname = @mu_name+"-"+poolnum.to_s
200
- poolnum += 1
201
- desc = {
202
- :fargate_profile_name => profname,
203
- :cluster_name => @mu_name,
204
- :pod_execution_role_arn => podrole_arn,
205
- :selectors => selectors,
206
- :subnets => fargate_subnets.sort,
207
- :tags => @tags
208
- }
209
- begin
210
- resp = MU::Cloud::AWS.eks(region: @config['region'], credentials: @config['credentials']).describe_fargate_profile(
211
- cluster_name: @mu_name,
212
- fargate_profile_name: profname
213
- )
214
- if resp and resp.fargate_profile
215
- old_desc = MU.structToHash(resp.fargate_profile, stringify_keys: true)
216
- new_desc = MU.structToHash(desc, stringify_keys: true)
217
- ["created_at", "status", "fargate_profile_arn"].each { |k|
218
- old_desc.delete(k)
219
- }
220
- old_desc["subnets"].sort!
221
- if !old_desc.eql?(new_desc)
222
- MU.log "Deleting Fargate profile #{profname} in order to apply changes", MU::WARN, details: desc
223
- MU::Cloud::AWS::ContainerCluster.purge_fargate_profile(profname, @mu_name, @config['region'], @credentials)
224
- else
225
- next
226
- end
227
- end
228
- rescue Aws::EKS::Errors::ResourceNotFoundException
229
- # This is just fine!
230
- end
231
- MU.log "Creating EKS Fargate profile #{profname}", details: desc
232
- resp = MU::Cloud::AWS.eks(region: @config['region'], credentials: @config['credentials']).create_fargate_profile(desc)
233
- begin
234
- resp = MU::Cloud::AWS.eks(region: @config['region'], credentials: @config['credentials']).describe_fargate_profile(
235
- cluster_name: @mu_name,
236
- fargate_profile_name: profname
237
- )
238
- sleep 1 if resp.fargate_profile.status == "CREATING"
239
- end while resp.fargate_profile.status == "CREATING"
240
- MU.log "Creation of EKS Fargate profile #{profname} complete"
241
- }
242
- end
243
-
244
- me = cloud_desc
245
- @endpoint = me.endpoint
246
- @cacert = me.certificate_authority.data
247
- @cluster = @mu_name
248
- if @config['flavor'] != "Fargate"
249
- resp = MU::Cloud::AWS.iam(credentials: @config['credentials']).get_role(role_name: @mu_name+"WORKERS")
250
- @worker_role_arn = resp.role.arn
251
- end
252
- kube_conf = @deploy.deploy_dir+"/kubeconfig-#{@config['name']}"
253
- gitlab_helper = @deploy.deploy_dir+"/gitlab-eks-helper-#{@config['name']}.sh"
254
-
255
- File.open(kube_conf, "w"){ |k|
256
- k.puts kube.result(binding)
257
- }
258
- gitlab = ERB.new(File.read(MU.myRoot+"/extras/gitlab-eks-helper.sh.erb"))
259
- File.open(gitlab_helper, "w"){ |k|
260
- k.puts gitlab.result(binding)
261
- }
127
+ apply_kubernetes_tags
128
+ create_fargate_kubernetes_profile if @config['flavor'] == "Fargate"
129
+ apply_kubernetes_resources
262
130
 
263
- if @config['flavor'] != "Fargate"
264
- eks_auth = @deploy.deploy_dir+"/eks-auth-cm-#{@config['name']}.yaml"
265
- File.open(eks_auth, "w"){ |k|
266
- k.puts configmap.result(binding)
267
- }
268
- authmap_cmd = %Q{#{MU::Master.kubectl} --kubeconfig "#{kube_conf}" apply -f "#{eks_auth}"}
269
- MU.log "Configuring Kubernetes <=> IAM mapping for worker nodes", MU::NOTICE, details: authmap_cmd
270
- # maybe guard this mess
271
- %x{#{authmap_cmd}}
272
- end
273
-
274
- # and this one
275
- admin_user_cmd = %Q{#{MU::Master.kubectl} --kubeconfig "#{kube_conf}" apply -f "#{MU.myRoot}/extras/admin-user.yaml"}
276
- admin_role_cmd = %Q{#{MU::Master.kubectl} --kubeconfig "#{kube_conf}" apply -f "#{MU.myRoot}/extras/admin-role-binding.yaml"}
277
- MU.log "Configuring Kubernetes admin-user and role", MU::NOTICE, details: admin_user_cmd+"\n"+admin_role_cmd
278
- %x{#{admin_user_cmd}}
279
- %x{#{admin_role_cmd}}
280
-
281
- if @config['kubernetes_resources']
282
- MU::Master.applyKubernetesResources(
283
- @config['name'],
284
- @config['kubernetes_resources'],
285
- kubeconfig: kube_conf,
286
- outputdir: @deploy.deploy_dir
287
- )
288
- end
289
-
290
- MU.log %Q{How to interact with your EKS cluster\nkubectl --kubeconfig "#{kube_conf}" get all\nkubectl --kubeconfig "#{kube_conf}" create -f some_k8s_deploy.yml\nkubectl --kubeconfig "#{kube_conf}" get nodes}, MU::SUMMARY
291
131
  elsif @config['flavor'] != "Fargate"
292
- resp = MU::Cloud::AWS.ecs(region: @config['region'], credentials: @config['credentials']).list_container_instances({
293
- cluster: @mu_name
294
- })
295
- existing = {}
296
- if resp
297
- uuids = []
298
- resp.container_instance_arns.each { |arn|
299
- uuids << arn.sub(/^.*?:container-instance\//, "")
300
- }
301
- if uuids.size > 0
302
- resp = MU::Cloud::AWS.ecs(region: @config['region'], credentials: @config['credentials']).describe_container_instances({
303
- cluster: @mu_name,
304
- container_instances: uuids
305
- })
306
- resp.container_instances.each { |i|
307
- existing[i.ec2_instance_id] = i
308
- }
309
- end
310
- end
311
-
312
- serverpool.listNodes.each { |node|
313
- resources = resource_lookup[node.cloud_desc.instance_type]
314
- t = Thread.new {
315
- ident_doc = nil
316
- ident_doc_sig = nil
317
- if !node.windows?
318
- session = node.getSSHSession(10, 30)
319
- ident_doc = session.exec!("curl -s http://169.254.169.254/latest/dynamic/instance-identity/document/")
320
- ident_doc_sig = session.exec!("curl -s http://169.254.169.254/latest/dynamic/instance-identity/signature/")
321
- else
322
- begin
323
- session = node.getWinRMSession(1, 60)
324
- rescue Exception # XXX
325
- session = node.getSSHSession(1, 60)
326
- end
327
- end
328
- MU.log "Identity document for #{node}", MU::DEBUG, details: ident_doc
329
- MU.log "Identity document signature for #{node}", MU::DEBUG, details: ident_doc_sig
330
- params = {
331
- :cluster => @mu_name,
332
- :instance_identity_document => ident_doc,
333
- :instance_identity_document_signature => ident_doc_sig,
334
- :total_resources => [
335
- {
336
- :name => "CPU",
337
- :type => "INTEGER",
338
- :integer_value => resources["vcpu"].to_i
339
- },
340
- {
341
- :name => "MEMORY",
342
- :type => "INTEGER",
343
- :integer_value => (resources["memory"]*1024*1024).to_i
344
- }
345
- ]
346
- }
347
- if !existing.has_key?(node.cloud_id)
348
- MU.log "Registering ECS instance #{node} in cluster #{@mu_name}", details: params
349
- else
350
- params[:container_instance_arn] = existing[node.cloud_id].container_instance_arn
351
- MU.log "Updating ECS instance #{node} in cluster #{@mu_name}", MU::NOTICE, details: params
352
- end
353
- MU::Cloud::AWS.ecs(region: @config['region'], credentials: @config['credentials']).register_container_instance(params)
354
-
355
- }
356
- }
132
+ manage_ecs_workers
357
133
  end
358
134
 
135
+ # ECS: manage containers/services/tasks
359
136
  if @config['flavor'] != "EKS" and @config['containers']
360
137
 
361
- security_groups = []
362
- if @dependencies.has_key?("firewall_rule")
363
- @dependencies['firewall_rule'].values.each { |sg|
364
- security_groups << sg.cloud_id
365
- }
366
- end
367
-
368
- tasks_registered = 0
369
- retries = 0
370
- svc_resp = begin
371
- MU::Cloud::AWS.ecs(region: @config['region'], credentials: @config['credentials']).list_services(
372
- cluster: arn
373
- )
374
- rescue Aws::ECS::Errors::ClusterNotFoundException => e
375
- if retries < 10
376
- sleep 5
377
- retries += 1
378
- retry
379
- else
380
- raise e
381
- end
382
- end
383
- existing_svcs = svc_resp.service_arns.map { |s|
384
- s.gsub(/.*?:service\/(.*)/, '\1')
385
- }
386
-
387
138
  # Reorganize things so that we have services and task definitions
388
139
  # mapped to the set of containers they must contain
389
140
  tasks = {}
390
- created_generic_loggroup = false
391
141
 
392
142
  @config['containers'].each { |c|
393
143
  service_name = c['service'] ? @mu_name+"-"+c['service'].upcase : @mu_name
@@ -395,238 +145,35 @@ module MU
395
145
  tasks[service_name] << c
396
146
  }
397
147
 
148
+ existing_svcs = list_ecs_services
149
+
398
150
  tasks.each_pair { |service_name, containers|
399
- launch_type = @config['flavor'] == "ECS" ? "EC2" : "FARGATE"
400
- cpu_total = 0
401
- mem_total = 0
402
151
  role_arn = nil
403
- lbs = []
404
152
 
405
- container_definitions = containers.map { |c|
406
- container_name = @mu_name+"-"+c['name'].upcase
153
+ container_definitions, role, lbs = get_ecs_container_definitions(containers)
154
+ role_arn ||= role
155
+
156
+ cpu_total = mem_total = 0
157
+ containers.each { |c|
407
158
  cpu_total += c['cpu']
408
159
  mem_total += c['memory']
409
-
410
- if c["role"] and !role_arn
411
- found = MU::MommaCat.findStray(
412
- @config['cloud'],
413
- "role",
414
- cloud_id: c["role"]["id"],
415
- name: c["role"]["name"],
416
- deploy_id: c["role"]["deploy_id"] || @deploy.deploy_id,
417
- dummy_ok: false
418
- )
419
- if found
420
- found = found.first
421
- if found and found.cloudobj
422
- role_arn = found.cloudobj.arn
423
- end
424
- else
425
- raise MuError, "Unable to find execution role from #{c["role"]}"
426
- end
427
- end
428
-
429
- if c['loadbalancers'] != []
430
- c['loadbalancers'].each {|lb|
431
- found = @deploy.findLitterMate(name: lb['name'], type: "loadbalancer")
432
- if found
433
- MU.log "Mapping LB #{found.mu_name} to service #{c['name']}", MU::INFO
434
- if found.cloud_desc.type != "classic"
435
- elb_groups = MU::Cloud::AWS.elb2(region: @config['region'], credentials: @config['credentials']).describe_target_groups({
436
- load_balancer_arn: found.cloud_desc.load_balancer_arn
437
- })
438
- matching_target_groups = []
439
- elb_groups.target_groups.each { |tg|
440
- if tg.port.to_i == lb['container_port'].to_i
441
- matching_target_groups << {
442
- arn: tg['target_group_arn'],
443
- name: tg['target_group_name']
444
- }
445
- end
446
- }
447
- if matching_target_groups.length >= 1
448
- MU.log "#{matching_target_groups.length} matching target groups found. Mapping #{container_name} to target group #{matching_target_groups.first['name']}", MU::INFO
449
- lbs << {
450
- container_name: container_name,
451
- container_port: lb['container_port'],
452
- target_group_arn: matching_target_groups.first[:arn]
453
- }
454
- else
455
- raise MuError, "No matching target groups found"
456
- end
457
- elsif @config['flavor'] == "Fargate" && found.cloud_desc.type == "classic"
458
- raise MuError, "Classic Load Balancers are not supported with Fargate."
459
- else
460
- MU.log "Mapping Classic LB #{found.mu_name} to service #{container_name}", MU::INFO
461
- lbs << {
462
- container_name: container_name,
463
- container_port: lb['container_port'],
464
- load_balancer_name: found.mu_name
465
- }
466
- end
467
- else
468
- raise MuError, "Unable to find loadbalancers from #{c["loadbalancers"].first['name']}"
469
- end
470
- }
471
- end
472
-
473
- params = {
474
- name: @mu_name+"-"+c['name'].upcase,
475
- image: c['image'],
476
- memory: c['memory'],
477
- cpu: c['cpu']
478
- }
479
- if !@config['vpc']
480
- c['hostname'] ||= @mu_name+"-"+c['name'].upcase
481
- end
482
- [:essential, :hostname, :start_timeout, :stop_timeout, :user, :working_directory, :disable_networking, :privileged, :readonly_root_filesystem, :interactive, :pseudo_terminal, :links, :entry_point, :command, :dns_servers, :dns_search_domains, :docker_security_options, :port_mappings, :repository_credentials, :mount_points, :environment, :volumes_from, :secrets, :depends_on, :extra_hosts, :docker_labels, :ulimits, :system_controls, :health_check, :resource_requirements].each { |param|
483
- if c.has_key?(param.to_s)
484
- params[param] = if !c[param.to_s].nil? and (c[param.to_s].is_a?(Hash) or c[param.to_s].is_a?(Array))
485
- MU.strToSym(c[param.to_s])
486
- else
487
- c[param.to_s]
488
- end
489
- end
490
- }
491
- if @config['vpc']
492
- [:hostname, :dns_servers, :dns_search_domains, :links].each { |param|
493
- if params[param]
494
- MU.log "Container parameter #{param.to_s} not supported in VPC clusters, ignoring", MU::WARN
495
- params.delete(param)
496
- end
497
- }
498
- end
499
- if @config['flavor'] == "Fargate"
500
- [:privileged, :docker_security_options].each { |param|
501
- if params[param]
502
- MU.log "Container parameter #{param.to_s} not supported in Fargate clusters, ignoring", MU::WARN
503
- params.delete(param)
504
- end
505
- }
506
- end
507
- if c['log_configuration']
508
- log_obj = @deploy.findLitterMate(name: c['log_configuration']['options']['awslogs-group'], type: "logs")
509
- if log_obj
510
- c['log_configuration']['options']['awslogs-group'] = log_obj.mu_name
511
- end
512
- params[:log_configuration] = MU.strToSym(c['log_configuration'])
513
- end
514
- params
515
160
  }
516
-
517
161
  cpu_total = 2 if cpu_total == 0
518
162
  mem_total = 2 if mem_total == 0
519
163
 
520
- task_params = {
521
- family: @deploy.deploy_id,
522
- container_definitions: container_definitions,
523
- requires_compatibilities: [launch_type]
524
- }
525
-
526
- if @config['volumes']
527
- task_params[:volumes] = []
528
- @config['volumes'].each { |v|
529
- vol = { :name => v['name'] }
530
- if v['type'] == "host"
531
- vol[:host] = {}
532
- if v['host_volume_source_path']
533
- vol[:host][:source_path] = v['host_volume_source_path']
534
- end
535
- elsif v['type'] == "docker"
536
- vol[:docker_volume_configuration] = MU.strToSym(v['docker_volume_configuration'])
537
- else
538
- raise MuError, "Invalid volume type '#{v['type']}' specified in ContainerCluster '#{@mu_name}'"
539
- end
540
- task_params[:volumes] << vol
541
- }
542
- end
543
-
544
- if role_arn
545
- task_params[:execution_role_arn] = role_arn
546
- task_params[:task_role_arn] = role_arn
547
- end
548
- if @config['flavor'] == "Fargate"
549
- task_params[:network_mode] = "awsvpc"
550
- task_params[:cpu] = cpu_total.to_i.to_s
551
- task_params[:memory] = mem_total.to_i.to_s
552
- end
164
+ task_def = register_ecs_task(container_definitions, service_name, cpu_total, mem_total, role_arn: role_arn)
553
165
 
554
- tasks_registered += 1
555
- MU.log "Registering task definition #{service_name} with #{container_definitions.size.to_s} containers"
556
-
557
- # XXX this helpfully keeps revisions, but let's compare anyway and avoid cluttering with identical ones
558
- resp = MU::Cloud::AWS.ecs(region: @config['region'], credentials: @config['credentials']).register_task_definition(task_params)
166
+ create_update_ecs_service(task_def, service_name, lbs, existing_svcs)
167
+ existing_svcs << service_name
168
+ }
559
169
 
560
- task_def = resp.task_definition.task_definition_arn
561
- service_params = {
562
- :cluster => @mu_name,
563
- :desired_count => @config['instance_count'], # XXX this makes no sense
564
- :service_name => service_name,
565
- :launch_type => launch_type,
566
- :task_definition => task_def,
567
- :load_balancers => lbs
170
+ if tasks.size > 0
171
+ tasks_failing = false
172
+ MU.retrier(wait: 15, max: 10, loop_if: Proc.new { tasks_failing }){ |retries, _wait|
173
+ tasks_failing = !MU::Cloud::AWS::ContainerCluster.tasksRunning?(@mu_name, log: (retries > 0), region: @config['region'], credentials: @config['credentials'])
568
174
  }
569
- if @config['vpc']
570
- subnet_ids = []
571
- all_public = true
572
-
573
- subnets =
574
- if @config["vpc"]["subnets"].empty?
575
- @vpc.subnets
576
- else
577
- subnet_objects= []
578
- @config["vpc"]["subnets"].each { |subnet|
579
- sobj = @vpc.getSubnet(cloud_id: subnet["subnet_id"], name: subnet["subnet_name"])
580
- if sobj.nil?
581
- MU.log "Got nil result from @vpc.getSubnet(cloud_id: #{subnet["subnet_id"]}, name: #{subnet["subnet_name"]})", MU::WARN
582
- else
583
- subnet_objects << sobj
584
- end
585
- }
586
- subnet_objects
587
- end
588
-
589
- subnets.each { |subnet_obj|
590
- subnet_ids << subnet_obj.cloud_id
591
- all_public = false if subnet_obj.private?
592
- }
593
-
594
- service_params[:network_configuration] = {
595
- :awsvpc_configuration => {
596
- :subnets => subnet_ids,
597
- :security_groups => security_groups,
598
- :assign_public_ip => all_public ? "ENABLED" : "DISABLED"
599
- }
600
- }
601
- end
602
175
 
603
- if !existing_svcs.include?(service_name)
604
- MU.log "Creating Service #{service_name}"
605
-
606
- resp = MU::Cloud::AWS.ecs(region: @config['region'], credentials: @config['credentials']).create_service(service_params)
607
- else
608
- service_params[:service] = service_params[:service_name].dup
609
- service_params.delete(:service_name)
610
- service_params.delete(:launch_type)
611
- MU.log "Updating Service #{service_name}", MU::NOTICE, details: service_params
612
-
613
- resp = MU::Cloud::AWS.ecs(region: @config['region'], credentials: @config['credentials']).update_service(service_params)
614
- end
615
- existing_svcs << service_name
616
- }
617
-
618
- max_retries = 10
619
- retries = 0
620
- if tasks_registered > 0
621
- retry_me = false
622
- begin
623
- retry_me = !MU::Cloud::AWS::ContainerCluster.tasksRunning?(@mu_name, log: (retries > 0), region: @config['region'], credentials: @config['credentials'])
624
- retries += 1
625
- sleep 15 if retry_me
626
- end while retry_me and retries < max_retries
627
- tasks = nil
628
-
629
- if retry_me
176
+ if tasks_failing
630
177
  MU.log "Not all tasks successfully launched in cluster #{@mu_name}", MU::WARN
631
178
  end
632
179
  end
@@ -653,7 +200,7 @@ module MU
653
200
  listme = services.slice!(0, (services.length >= 10 ? 10 : services.length))
654
201
  if services.size > 0
655
202
  tasks_defined.concat(
656
- tasks = MU::Cloud::AWS.ecs(region: region, credentials: credentials).describe_services(
203
+ MU::Cloud::AWS.ecs(region: region, credentials: credentials).describe_services(
657
204
  cluster: cluster,
658
205
  services: listme
659
206
  ).services.map { |s| s.task_definition }
@@ -693,7 +240,6 @@ module MU
693
240
  cluster: cluster,
694
241
  tasks: task_ids
695
242
  ).tasks.each { |t|
696
- task_name = t.task_definition_arn.sub(/^.*?:task-definition\/([^\/:]+)$/, '\1')
697
243
  t.containers.each { |c|
698
244
  containers[c.name] ||= {}
699
245
  containers[c.name][t.desired_status] ||= {
@@ -736,10 +282,13 @@ MU.log c.name, MU::NOTICE, details: t
736
282
  to_return
737
283
  end
738
284
 
285
+ @cloud_desc_cache = nil
739
286
  # Return the cloud layer descriptor for this EKS/ECS/Fargate cluster
740
287
  # @return [OpenStruct]
741
- def cloud_desc
742
- if @config['flavor'] == "EKS" or
288
+ def cloud_desc(use_cache: true)
289
+ return @cloud_desc_cache if @cloud_desc_cache and use_cache
290
+ return nil if !@cloud_id
291
+ @cloud_desc_cache = if @config['flavor'] == "EKS" or
743
292
  (@config['flavor'] == "Fargate" and !@config['containers'])
744
293
  resp = MU::Cloud::AWS.eks(region: @config['region'], credentials: @config['credentials']).describe_cluster(
745
294
  name: @cloud_id
@@ -751,6 +300,7 @@ MU.log c.name, MU::NOTICE, details: t
751
300
  )
752
301
  resp.clusters.first
753
302
  end
303
+ @cloud_desc_cache
754
304
  end
755
305
 
756
306
  # Canonical Amazon Resource Number for this resource
@@ -777,7 +327,7 @@ MU.log c.name, MU::NOTICE, details: t
777
327
  end
778
328
 
779
329
  @@eks_versions = {}
780
- @@eks_version_semaphore = Mutex.new
330
+ @@eks_version_semaphores = {}
781
331
  # Use the AWS SSM API to fetch the current version of the Amazon Linux
782
332
  # ECS-optimized AMI, so we can use it as a default AMI for ECS deploys.
783
333
  # @param flavor [String]: ECS or EKS
@@ -790,24 +340,22 @@ MU.log c.name, MU::NOTICE, details: t
790
340
  names: ["/aws/service/#{flavor.downcase}/optimized-ami/amazon-linux/recommended"]
791
341
  )
792
342
  else
793
- @@eks_version_semaphore.synchronize {
343
+ @@eks_version_semaphores[region] ||= Mutex.new
344
+
345
+ @@eks_version_semaphores[region].synchronize {
794
346
  if !@@eks_versions[region]
795
347
  @@eks_versions[region] ||= []
796
348
  versions = {}
797
- resp = nil
798
- next_token = nil
799
- begin
800
- resp = MU::Cloud::AWS.ssm(region: region).get_parameters_by_path(
801
- path: "/aws/service/#{flavor.downcase}",
802
- recursive: true,
803
- next_token: next_token
804
- )
805
- resp.parameters.each { |p|
806
- p.name.match(/\/aws\/service\/eks\/optimized-ami\/([^\/]+?)\//)
807
- versions[Regexp.last_match[1]] = true
808
- }
809
- next_token = resp.next_token
810
- end while !next_token.nil?
349
+ resp = MU::Cloud::AWS.ssm(region: region).get_parameters_by_path(
350
+ path: "/aws/service/#{flavor.downcase}/optimized-ami",
351
+ recursive: true,
352
+ max_results: 10 # as high as it goes, ugh
353
+ )
354
+
355
+ resp.parameters.each { |p|
356
+ p.name.match(/\/aws\/service\/eks\/optimized-ami\/([^\/]+?)\//)
357
+ versions[Regexp.last_match[1]] = true
358
+ }
811
359
  @@eks_versions[region] = versions.keys.sort { |a, b| MU.version_sort(a, b) }
812
360
  end
813
361
  }
@@ -827,15 +375,31 @@ MU.log c.name, MU::NOTICE, details: t
827
375
  nil
828
376
  end
829
377
 
378
+ @@supported_eks_region_cache = []
379
+ @@eks_region_semaphore = Mutex.new
380
+
830
381
  # Return the list of regions where we know EKS is supported.
831
382
  def self.EKSRegions(credentials = nil)
832
- eks_regions = []
833
- MU::Cloud::AWS.listRegions(credentials: credentials).each { |r|
834
- ami = getStandardImage("EKS", r)
835
- eks_regions << r if ami
836
- }
383
+ @@eks_region_semaphore.synchronize {
384
+ if @@supported_eks_region_cache and !@@supported_eks_region_cache.empty?
385
+ return @@supported_eks_region_cache
386
+ end
387
+ start = Time.now
388
+ # the SSM API is painfully slow for large result sets, so thread
389
+ # these and do them in parallel
390
+ @@supported_eks_region_cache = []
391
+ region_threads = []
392
+ MU::Cloud::AWS.listRegions(credentials: credentials).each { |region|
393
+ region_threads << Thread.new(region) { |r|
394
+ r_start = Time.now
395
+ ami = getStandardImage("EKS", r)
396
+ @@supported_eks_region_cache << r if ami
397
+ }
398
+ }
399
+ region_threads.each { |t| t.join }
837
400
 
838
- eks_regions
401
+ @@supported_eks_region_cache
402
+ }
839
403
  end
840
404
 
841
405
  # Does this resource type exist as a global (cloud-wide) artifact, or
@@ -856,156 +420,143 @@ MU.log c.name, MU::NOTICE, details: t
856
420
  # @param ignoremaster [Boolean]: If true, will remove resources not flagged as originating from this Mu server
857
421
  # @param region [String]: The cloud provider region
858
422
  # @return [void]
859
- def self.cleanup(noop: false, ignoremaster: false, region: MU.curRegion, credentials: nil, flags: {})
860
- resp = MU::Cloud::AWS.ecs(credentials: credentials, region: region).list_clusters
861
-
862
-
863
- if resp and resp.cluster_arns and resp.cluster_arns.size > 0
864
- resp.cluster_arns.each { |arn|
865
- if arn.match(/:cluster\/(#{MU.deploy_id}[^:]+)$/)
866
- cluster = Regexp.last_match[1]
867
-
868
- svc_resp = MU::Cloud::AWS.ecs(region: region, credentials: credentials).list_services(
869
- cluster: arn
870
- )
871
- if svc_resp and svc_resp.service_arns
872
- svc_resp.service_arns.each { |svc_arn|
873
- svc_name = svc_arn.gsub(/.*?:service\/(.*)/, '\1')
874
- MU.log "Deleting Service #{svc_name} from ECS Cluster #{cluster}"
875
- if !noop
876
- MU::Cloud::AWS.ecs(region: region, credentials: credentials).delete_service(
877
- cluster: arn,
878
- service: svc_name,
879
- force: true # man forget scaling up and down if we're just deleting the cluster
880
- )
881
- end
882
- }
883
- end
423
+ def self.cleanup(noop: false, deploy_id: MU.deploy_id, ignoremaster: false, region: MU.curRegion, credentials: nil, flags: {})
424
+ MU.log "AWS::ContainerCluster.cleanup: need to support flags['known']", MU::DEBUG, details: flags
425
+ MU.log "Placeholder: AWS ContainerCluster artifacts do not support tags, so ignoremaster cleanup flag has no effect", MU::DEBUG, details: ignoremaster
884
426
 
885
- instances = MU::Cloud::AWS.ecs(credentials: credentials, region: region).list_container_instances({
886
- cluster: cluster
887
- })
888
- if instances
889
- instances.container_instance_arns.each { |instance_arn|
890
- uuid = instance_arn.sub(/^.*?:container-instance\//, "")
891
- MU.log "Deregistering instance #{uuid} from ECS Cluster #{cluster}"
892
- if !noop
893
- resp = MU::Cloud::AWS.ecs(credentials: credentials, region: region).deregister_container_instance({
894
- cluster: cluster,
895
- container_instance: uuid,
896
- force: true,
897
- })
898
- end
899
- }
900
- end
901
- MU.log "Deleting ECS Cluster #{cluster}"
902
- if !noop
903
- # TODO de-register container instances
904
- begin
905
- deletion = MU::Cloud::AWS.ecs(credentials: credentials, region: region).delete_cluster(
906
- cluster: cluster
907
- )
908
- rescue Aws::ECS::Errors::ClusterContainsTasksException => e
909
- sleep 5
910
- retry
911
- end
912
- end
913
- end
914
- }
915
- end
427
+ purge_ecs_clusters(noop: noop, region: region, credentials: credentials, deploy_id: deploy_id)
916
428
 
917
- tasks = MU::Cloud::AWS.ecs(region: region, credentials: credentials).list_task_definitions(
918
- family_prefix: MU.deploy_id
919
- )
429
+ purge_eks_clusters(noop: noop, region: region, credentials: credentials, deploy_id: deploy_id)
920
430
 
921
- if tasks and tasks.task_definition_arns
922
- tasks.task_definition_arns.each { |arn|
923
- MU.log "Deregistering Fargate task definition #{arn}"
924
- if !noop
925
- MU::Cloud::AWS.ecs(region: region, credentials: credentials).deregister_task_definition(
926
- task_definition: arn
927
- )
928
- end
929
- }
930
- end
931
-
932
- return if !MU::Cloud::AWS::ContainerCluster.EKSRegions.include?(region)
431
+ end
933
432
 
433
+ def self.purge_eks_clusters(noop: false, region: MU.curRegion, credentials: nil, deploy_id: MU.deploy_id)
934
434
  resp = begin
935
435
  MU::Cloud::AWS.eks(credentials: credentials, region: region).list_clusters
936
436
  rescue Aws::EKS::Errors::AccessDeniedException
937
437
  # EKS isn't actually live in this region, even though SSM lists
938
438
  # base images for it
439
+ if @@supported_eks_region_cache
440
+ @@supported_eks_region_cache.delete(region)
441
+ end
939
442
  return
940
443
  end
941
444
 
445
+ return if !resp or !resp.clusters
942
446
 
943
- if resp and resp.clusters
944
- resp.clusters.each { |cluster|
945
- if cluster.match(/^#{MU.deploy_id}-/)
447
+ resp.clusters.each { |cluster|
448
+ if cluster.match(/^#{deploy_id}-/)
946
449
 
947
- desc = MU::Cloud::AWS.eks(credentials: credentials, region: region).describe_cluster(
948
- name: cluster
949
- ).cluster
450
+ desc = MU::Cloud::AWS.eks(credentials: credentials, region: region).describe_cluster(
451
+ name: cluster
452
+ ).cluster
950
453
 
951
- profiles = MU::Cloud::AWS.eks(region: region, credentials: credentials).list_fargate_profiles(
952
- cluster_name: cluster
953
- )
954
- if profiles and profiles.fargate_profile_names
955
- profiles.fargate_profile_names.each { |profile|
956
- MU.log "Deleting Fargate EKS profile #{profile}"
957
- next if noop
958
- MU::Cloud::AWS::ContainerCluster.purge_fargate_profile(profile, cluster, region, credentials)
959
- }
960
- end
454
+ profiles = MU::Cloud::AWS.eks(region: region, credentials: credentials).list_fargate_profiles(
455
+ cluster_name: cluster
456
+ )
457
+ if profiles and profiles.fargate_profile_names
458
+ profiles.fargate_profile_names.each { |profile|
459
+ MU.log "Deleting Fargate EKS profile #{profile}"
460
+ next if noop
461
+ MU::Cloud::AWS::ContainerCluster.purge_fargate_profile(profile, cluster, region, credentials)
462
+ }
463
+ end
464
+
465
+ remove_kubernetes_tags(cluster, desc, region: region, credentials: credentials, noop: noop)
961
466
 
962
- untag = []
963
- untag << desc.resources_vpc_config.vpc_id
964
- subnets = MU::Cloud::AWS.ec2(credentials: credentials, region: region).describe_subnets(
965
- filters: [ { name: "vpc-id", values: [desc.resources_vpc_config.vpc_id] } ]
966
- ).subnets
967
-
968
- # subnets
969
- untag.concat(subnets.map { |s| s.subnet_id } )
970
- rtbs = MU::Cloud::AWS.ec2(credentials: credentials, region: region).describe_route_tables(
971
- filters: [ { name: "vpc-id", values: [desc.resources_vpc_config.vpc_id] } ]
972
- ).route_tables
973
- untag.concat(rtbs.map { |r| r.route_table_id } )
974
- untag.concat(desc.resources_vpc_config.subnet_ids)
975
- untag.concat(desc.resources_vpc_config.security_group_ids)
976
- MU.log "Removing Kubernetes tags from VPC resources for #{cluster}", details: untag
977
- if !noop
978
- MU::Cloud::AWS.removeTag("kubernetes.io/cluster/#{cluster}", "shared", untag)
979
- MU::Cloud::AWS.removeTag("kubernetes.io/cluster/elb", cluster, untag)
467
+ MU.log "Deleting EKS Cluster #{cluster}"
468
+ next if noop
469
+ MU::Cloud::AWS.eks(credentials: credentials, region: region).delete_cluster(
470
+ name: cluster
471
+ )
472
+
473
+ status = nil
474
+ loop_if = Proc.new {
475
+ status != "FAILED"
476
+ }
477
+
478
+ MU.retrier(ignoreme: [Aws::EKS::Errors::ResourceNotFoundException], wait: 60){ |retries, _wait|
479
+ status = MU::Cloud::AWS.eks(credentials: credentials, region: region).describe_cluster(
480
+ name: cluster
481
+ ).cluster.status
482
+ if retries > 0 and (retries % 3) == 0
483
+ MU.log "Waiting for EKS cluster #{cluster} to finish deleting (status #{status})", MU::NOTICE
980
484
  end
981
- MU.log "Deleting EKS Cluster #{cluster}"
982
- if !noop
983
- MU::Cloud::AWS.eks(credentials: credentials, region: region).delete_cluster(
984
- name: cluster
485
+ }
486
+ # MU::Cloud.resourceClass("AWS", "Server").removeIAMProfile(cluster)
487
+ end
488
+ }
489
+ end
490
+ private_class_method :purge_eks_clusters
491
+
492
+ def self.purge_ecs_clusters(noop: false, region: MU.curRegion, credentials: nil, deploy_id: MU.deploy_id)
493
+ start = Time.now
494
+ resp = MU::Cloud::AWS.ecs(credentials: credentials, region: region).list_clusters
495
+
496
+ return if !resp or !resp.cluster_arns or resp.cluster_arns.empty?
497
+
498
+ resp.cluster_arns.each { |arn|
499
+ if arn.match(/:cluster\/(#{deploy_id}[^:]+)$/)
500
+ cluster = Regexp.last_match[1]
501
+
502
+ svc_resp = MU::Cloud::AWS.ecs(region: region, credentials: credentials).list_services(
503
+ cluster: arn
504
+ )
505
+ if svc_resp and svc_resp.service_arns
506
+ svc_resp.service_arns.each { |svc_arn|
507
+ svc_name = svc_arn.gsub(/.*?:service\/(.*)/, '\1')
508
+ MU.log "Deleting Service #{svc_name} from ECS Cluster #{cluster}"
509
+ next if noop
510
+ MU::Cloud::AWS.ecs(region: region, credentials: credentials).delete_service(
511
+ cluster: arn,
512
+ service: svc_name,
513
+ force: true # man forget scaling up and down if we're just deleting the cluster
985
514
  )
986
- begin
987
- status = nil
988
- retries = 0
989
- begin
990
- deletion = MU::Cloud::AWS.eks(credentials: credentials, region: region).describe_cluster(
991
- name: cluster
992
- )
993
- status = deletion.cluster.status
994
- if retries > 0 and (retries % 3) == 0
995
- MU.log "Waiting for EKS cluster #{cluster} to finish deleting (status #{status})", MU::NOTICE
996
- end
997
- retries += 1
998
- sleep 30
999
- end while status
1000
- rescue Aws::EKS::Errors::ResourceNotFoundException
1001
- # this is what we want
1002
- end
1003
- # MU::Cloud::AWS::Server.removeIAMProfile(cluster)
1004
- end
515
+ }
516
+ end
517
+
518
+ instances = MU::Cloud::AWS.ecs(credentials: credentials, region: region).list_container_instances({
519
+ cluster: cluster
520
+ })
521
+ if instances
522
+ instances.container_instance_arns.each { |instance_arn|
523
+ uuid = instance_arn.sub(/^.*?:container-instance\//, "")
524
+ MU.log "Deregistering instance #{uuid} from ECS Cluster #{cluster}"
525
+ next if noop
526
+ resp = MU::Cloud::AWS.ecs(credentials: credentials, region: region).deregister_container_instance({
527
+ cluster: cluster,
528
+ container_instance: uuid,
529
+ force: true,
530
+ })
531
+ }
532
+ end
533
+ MU.log "Deleting ECS Cluster #{cluster}"
534
+ next if noop
535
+ MU.retrier([Aws::ECS::Errors::ClusterContainsTasksException], wait: 5){
536
+ # TODO de-register container instances
537
+ MU::Cloud::AWS.ecs(credentials: credentials, region: region).delete_cluster(
538
+ cluster: cluster
539
+ )
540
+ }
541
+ end
542
+ }
543
+
544
+ tasks = MU::Cloud::AWS.ecs(region: region, credentials: credentials).list_task_definitions(
545
+ family_prefix: deploy_id
546
+ )
547
+
548
+ if tasks and tasks.task_definition_arns
549
+ tasks.task_definition_arns.each { |arn|
550
+ MU.log "Deregistering Fargate task definition #{arn}"
551
+ if !noop
552
+ MU::Cloud::AWS.ecs(region: region, credentials: credentials).deregister_task_definition(
553
+ task_definition: arn
554
+ )
1005
555
  end
1006
556
  }
1007
557
  end
1008
558
  end
559
+ private_class_method :purge_ecs_clusters
1009
560
 
1010
561
  # Locate an existing container_cluster.
1011
562
  # @return [Hash<String,OpenStruct>]: The cloud provider's complete descriptions of matching container_clusters.
@@ -1016,10 +567,8 @@ MU.log c.name, MU::NOTICE, details: t
1016
567
  resp = MU::Cloud::AWS.ecs(region: args[:region], credentials: args[:credentials]).describe_clusters(clusters: [args[:cloud_id]])
1017
568
  if resp.clusters and resp.clusters.size > 0
1018
569
  found[args[:cloud_id]] = resp.clusters.first
1019
- end
1020
-
1021
- # XXX name collision is possible here
1022
- if found.size == 0
570
+ else
571
+ # XXX misses due to name collision are possible here
1023
572
  desc = MU::Cloud::AWS.eks(region: args[:region], credentials: args[:credentials]).describe_cluster(name: args[:cloud_id])
1024
573
  found[args[:cloud_id]] = desc.cluster if desc and desc.cluster
1025
574
  end
@@ -1027,14 +576,14 @@ MU.log c.name, MU::NOTICE, details: t
1027
576
  next_token = nil
1028
577
  begin
1029
578
  resp = MU::Cloud::AWS.ecs(region: args[:region], credentials: args[:credentials]).list_clusters(next_token: next_token)
1030
- if resp and resp.cluster_arns and resp.cluster_arns.size > 0
1031
- names = resp.cluster_arns.map { |a| a.sub(/.*?:cluster\//, '') }
1032
- descs = MU::Cloud::AWS.ecs(region: args[:region], credentials: args[:credentials]).describe_clusters(clusters: names)
1033
- if descs and descs.clusters
1034
- descs.clusters.each { |c|
1035
- found[c.cluster_name] = c
1036
- }
1037
- end
579
+ break if !resp or !resp.cluster_arns
580
+ next_token = resp.next_token
581
+ names = resp.cluster_arns.map { |a| a.sub(/.*?:cluster\//, '') }
582
+ descs = MU::Cloud::AWS.ecs(region: args[:region], credentials: args[:credentials]).describe_clusters(clusters: names)
583
+ if descs and descs.clusters
584
+ descs.clusters.each { |c|
585
+ found[c.cluster_name] = c
586
+ }
1038
587
  end
1039
588
  end while next_token
1040
589
 
@@ -1042,14 +591,12 @@ MU.log c.name, MU::NOTICE, details: t
1042
591
  next_token = nil
1043
592
  begin
1044
593
  resp = MU::Cloud::AWS.eks(region: args[:region], credentials: args[:credentials]).list_clusters(next_token: next_token)
1045
- if resp and resp.clusters
1046
- resp.clusters.each { |c|
1047
- puts c
1048
- desc = MU::Cloud::AWS.eks(region: args[:region], credentials: args[:credentials]).describe_cluster(name: c)
1049
- found[c] = desc.cluster if desc and desc.cluster
1050
- }
1051
- next_token = resp.next_token
1052
- end
594
+ break if !resp or !resp.clusters
595
+ resp.clusters.each { |c|
596
+ desc = MU::Cloud::AWS.eks(region: args[:region], credentials: args[:credentials]).describe_cluster(name: c)
597
+ found[c] = desc.cluster if desc and desc.cluster
598
+ }
599
+ next_token = resp.next_token
1053
600
  rescue Aws::EKS::Errors::AccessDeniedException
1054
601
  # not all regions support EKS
1055
602
  end while next_token
@@ -1059,9 +606,9 @@ MU.log c.name, MU::NOTICE, details: t
1059
606
  end
1060
607
 
1061
608
  # Cloud-specific configuration properties.
1062
- # @param config [MU::Config]: The calling MU::Config object
609
+ # @param _config [MU::Config]: The calling MU::Config object
1063
610
  # @return [Array<Array,Hash>]: List of required fields, and json-schema Hash of cloud-specific configuration parameters for this resource
1064
- def self.schema(config)
611
+ def self.schema(_config)
1065
612
  toplevel_required = []
1066
613
 
1067
614
  schema = {
@@ -1685,18 +1232,18 @@ MU.log c.name, MU::NOTICE, details: t
1685
1232
  # @return [Boolean]: True if validation succeeded, False otherwise
1686
1233
  def self.validateConfig(cluster, configurator)
1687
1234
  ok = true
1688
-
1689
- cluster['size'] = MU::Cloud::AWS::Server.validateInstanceType(cluster["instance_type"], cluster["region"])
1235
+ start = Time.now
1236
+ cluster['size'] = MU::Cloud.resourceClass("AWS", "Server").validateInstanceType(cluster["instance_type"], cluster["region"])
1690
1237
  ok = false if cluster['size'].nil?
1691
1238
 
1692
1239
  cluster["flavor"] = "EKS" if cluster["flavor"].match(/^Kubernetes$/i)
1693
1240
 
1694
- if cluster["flavor"] == "ECS" and cluster["kubernetes"] and !MU::Cloud::AWS.isGovCloud?(cluster["region"])
1241
+ if cluster["flavor"] == "ECS" and cluster["kubernetes"] and !MU::Cloud::AWS.isGovCloud?(cluster["region"]) and !cluster["containers"] and MU::Cloud::AWS::ContainerCluster.EKSRegions(cluster['credentials']).include?(cluster['region'])
1695
1242
  cluster["flavor"] = "EKS"
1696
1243
  MU.log "Setting flavor of ContainerCluster '#{cluster['name']}' to EKS ('kubernetes' stanza was specified)", MU::NOTICE
1697
1244
  end
1698
1245
 
1699
- if cluster["flavor"] == "EKS" and !MU::Cloud::AWS::ContainerCluster.EKSRegions.include?(cluster['region'])
1246
+ if cluster["flavor"] == "EKS" and !MU::Cloud::AWS::ContainerCluster.EKSRegions(cluster['credentials']).include?(cluster['region'])
1700
1247
  MU.log "EKS is only available in some regions", MU::ERR, details: MU::Cloud::AWS::ContainerCluster.EKSRegions
1701
1248
  ok = false
1702
1249
  end
@@ -1766,7 +1313,7 @@ MU.log c.name, MU::NOTICE, details: t
1766
1313
  end
1767
1314
 
1768
1315
  if !created_generic_loggroup
1769
- cluster["dependencies"] << { "type" => "log", "name" => logname }
1316
+ MU::Config.addDependency(cluster, logname, "log")
1770
1317
  logdesc = {
1771
1318
  "name" => logname,
1772
1319
  "region" => cluster["region"],
@@ -1805,10 +1352,7 @@ MU.log c.name, MU::NOTICE, details: t
1805
1352
  }
1806
1353
  configurator.insertKitten(roledesc, "roles")
1807
1354
 
1808
- cluster["dependencies"] << {
1809
- "type" => "role",
1810
- "name" => rolename
1811
- }
1355
+ MU::Config.addDependency(cluster, rolename, "role")
1812
1356
  end
1813
1357
 
1814
1358
  created_generic_loggroup = true
@@ -1837,11 +1381,10 @@ MU.log c.name, MU::NOTICE, details: t
1837
1381
  role["tags"] = cluster["tags"] if !cluster["tags"].nil?
1838
1382
  role["optional_tags"] = cluster["optional_tags"] if !cluster["optional_tags"].nil?
1839
1383
  configurator.insertKitten(role, "roles")
1840
- cluster['dependencies'] << {
1841
- "type" => "role",
1842
- "name" => cluster["name"]+"pods",
1843
- "phase" => "groom"
1844
- }
1384
+ MU::Config.addDependency(cluster, cluster["name"]+"pods", "role", phase: "groom")
1385
+ if !MU::Master.kubectl
1386
+ MU.log "Since I can't find a kubectl executable, you will have to handle all service account, user, and role bindings manually!", MU::WARN
1387
+ end
1845
1388
  end
1846
1389
 
1847
1390
  if MU::Cloud::AWS.isGovCloud?(cluster["region"]) and cluster["flavor"] == "EKS"
@@ -1851,7 +1394,8 @@ MU.log c.name, MU::NOTICE, details: t
1851
1394
 
1852
1395
 
1853
1396
  if ["ECS", "EKS"].include?(cluster["flavor"])
1854
- std_ami = getStandardImage(cluster["flavor"], cluster['region'], version: cluster['kubernetes']['version'], gpu: cluster['gpu'])
1397
+ version = cluster["kubernetes"] ? cluster['kubernetes']['version'] : nil
1398
+ std_ami = getStandardImage(cluster["flavor"], cluster['region'], version: version, gpu: cluster['gpu'])
1855
1399
  cluster["host_image"] ||= std_ami
1856
1400
  if cluster["host_image"] != std_ami
1857
1401
  if cluster["flavor"] == "ECS"
@@ -1939,34 +1483,32 @@ MU.log c.name, MU::NOTICE, details: t
1939
1483
  end
1940
1484
 
1941
1485
  if cluster["flavor"] == "EKS"
1486
+
1487
+ if !MU::Master.kubectl
1488
+ MU.log "Without a kubectl executable, I cannot bind IAM roles to EKS worker nodes", MU::ERR
1489
+ ok = false
1490
+ end
1942
1491
  worker_pool["canned_iam_policies"] = [
1943
1492
  "AmazonEKSWorkerNodePolicy",
1944
1493
  "AmazonEKS_CNI_Policy",
1945
1494
  "AmazonEC2ContainerRegistryReadOnly"
1946
1495
  ]
1947
- worker_pool["dependencies"] = [
1948
- {
1949
- "type" => "container_cluster",
1950
- "name" => cluster['name']
1951
- }
1952
- ]
1953
- worker_pool["run_list"] = ["mu-tools::eks"]
1496
+ MU::Config.addDependency(worker_pool, cluster["name"], "container_cluster")
1497
+ worker_pool["run_list"] = ["recipe[mu-tools::eks]"]
1954
1498
  worker_pool["run_list"].concat(cluster["run_list"]) if cluster["run_list"]
1955
1499
  MU::Config::Server.common_properties.keys.each { |k|
1956
1500
  if cluster[k] and !worker_pool[k]
1957
1501
  worker_pool[k] = cluster[k]
1958
1502
  end
1959
1503
  }
1960
-
1504
+ else
1505
+ worker_pool["groom"] = false # don't meddle with ECS workers unnecessarily
1961
1506
  end
1962
1507
 
1963
1508
  configurator.insertKitten(worker_pool, "server_pools")
1964
1509
 
1965
1510
  if cluster["flavor"] == "ECS"
1966
- cluster["dependencies"] << {
1967
- "name" => cluster["name"]+"workers",
1968
- "type" => "server_pool",
1969
- }
1511
+ MU::Config.addDependency(cluster, cluster["name"]+"workers", "server_pool")
1970
1512
  end
1971
1513
 
1972
1514
  end
@@ -1988,18 +1530,17 @@ MU.log c.name, MU::NOTICE, details: t
1988
1530
  role["tags"] = cluster["tags"] if !cluster["tags"].nil?
1989
1531
  role["optional_tags"] = cluster["optional_tags"] if !cluster["optional_tags"].nil?
1990
1532
  configurator.insertKitten(role, "roles")
1991
- cluster['dependencies'] << {
1992
- "type" => "role",
1993
- "name" => cluster["name"]+"controlplane",
1994
- "phase" => "groom"
1995
- }
1533
+ MU::Config.addDependency(cluster, cluster["name"]+"controlplane", "role", phase: "groom")
1996
1534
  end
1997
1535
 
1998
1536
  ok
1999
1537
  end
2000
1538
 
2001
- private
2002
-
1539
+ # Delete a Fargate profile, needed both for cleanup and regroom updates
1540
+ # @param profile [String]:
1541
+ # @param cluster [String]:
1542
+ # @param region [String]:
1543
+ # @param credentials [String]:
2003
1544
  def self.purge_fargate_profile(profile, cluster, region, credentials)
2004
1545
  check = begin
2005
1546
  MU::Cloud::AWS.eks(region: region, credentials: credentials).delete_fargate_profile(
@@ -2012,28 +1553,467 @@ MU.log c.name, MU::NOTICE, details: t
2012
1553
  sleep 10
2013
1554
  retry
2014
1555
  end
2015
- sleep 5
2016
- retries = 0
2017
- begin
2018
- begin
1556
+
1557
+ loop_if = Proc.new {
2019
1558
  check = MU::Cloud::AWS.eks(region: region, credentials: credentials).describe_fargate_profile(
2020
1559
  cluster_name: cluster,
2021
1560
  fargate_profile_name: profile
2022
1561
  )
2023
- rescue Aws::EKS::Errors::ResourceNotFoundException
2024
- break
2025
- end
1562
+ check.fargate_profile.status == "DELETING"
1563
+ }
2026
1564
 
1565
+ MU.retrier(ignoreme: [Aws::EKS::Errors::ResourceNotFoundException], wait: 30, max: 40, loop_if: loop_if) {
2027
1566
  if check.fargate_profile.status != "DELETING"
2028
- MU.log "Failed to delete Fargate EKS profile #{profile}", MU::ERR, details: check
2029
1567
  break
2030
- end
2031
- if retries > 0 and (retries % 3) == 0
1568
+ elsif retries > 0 and (retries % 3) == 0
2032
1569
  MU.log "Waiting for Fargate EKS profile #{profile} to delete (status #{check.fargate_profile.status})", MU::NOTICE
2033
1570
  end
2034
- sleep 30
2035
- retries += 1
2036
- end while retries < 40
1571
+ }
1572
+ end
1573
+
1574
+ private
1575
+
1576
+ def apply_kubernetes_resources
1577
+ kube = ERB.new(File.read(MU.myRoot+"/cookbooks/mu-tools/templates/default/kubeconfig-eks.erb"))
1578
+ configmap = ERB.new(File.read(MU.myRoot+"/extras/aws-auth-cm.yaml.erb"))
1579
+ @endpoint = cloud_desc.endpoint
1580
+ @cacert = cloud_desc.certificate_authority.data
1581
+ @cluster = @mu_name
1582
+ if @config['flavor'] != "Fargate"
1583
+ resp = MU::Cloud::AWS.iam(credentials: @config['credentials']).get_role(role_name: @mu_name+"WORKERS")
1584
+ @worker_role_arn = resp.role.arn
1585
+ end
1586
+ kube_conf = @deploy.deploy_dir+"/kubeconfig-#{@config['name']}"
1587
+ gitlab_helper = @deploy.deploy_dir+"/gitlab-eks-helper-#{@config['name']}.sh"
1588
+
1589
+ File.open(kube_conf, "w"){ |k|
1590
+ k.puts kube.result(binding)
1591
+ }
1592
+ gitlab = ERB.new(File.read(MU.myRoot+"/extras/gitlab-eks-helper.sh.erb"))
1593
+ File.open(gitlab_helper, "w"){ |k|
1594
+ k.puts gitlab.result(binding)
1595
+ }
1596
+
1597
+ if @config['flavor'] != "Fargate"
1598
+ eks_auth = @deploy.deploy_dir+"/eks-auth-cm-#{@config['name']}.yaml"
1599
+ File.open(eks_auth, "w"){ |k|
1600
+ k.puts configmap.result(binding)
1601
+ }
1602
+ authmap_cmd = %Q{#{MU::Master.kubectl} --kubeconfig "#{kube_conf}" apply -f "#{eks_auth}"}
1603
+
1604
+ MU.log "Configuring Kubernetes <=> IAM mapping for worker nodes", MU::NOTICE, details: authmap_cmd
1605
+
1606
+ MU.retrier(max: 10, wait: 10, loop_if: Proc.new {$?.exitstatus != 0}){
1607
+ puts %x{#{authmap_cmd}}
1608
+ }
1609
+ raise MuError, "Failed to apply #{authmap_cmd}" if $?.exitstatus != 0
1610
+ end
1611
+
1612
+ if MU::Master.kubectl
1613
+ admin_user_cmd = %Q{#{MU::Master.kubectl} --kubeconfig "#{kube_conf}" apply -f "#{MU.myRoot}/extras/admin-user.yaml"}
1614
+ admin_role_cmd = %Q{#{MU::Master.kubectl} --kubeconfig "#{kube_conf}" apply -f "#{MU.myRoot}/extras/admin-role-binding.yaml"}
1615
+ MU.log "Configuring Kubernetes admin-user and role", MU::NOTICE, details: admin_user_cmd+"\n"+admin_role_cmd
1616
+ %x{#{admin_user_cmd}}
1617
+ %x{#{admin_role_cmd}}
1618
+
1619
+ if @config['kubernetes_resources']
1620
+ MU::Master.applyKubernetesResources(
1621
+ @config['name'],
1622
+ @config['kubernetes_resources'],
1623
+ kubeconfig: kube_conf,
1624
+ outputdir: @deploy.deploy_dir
1625
+ )
1626
+ end
1627
+ end
1628
+
1629
+ MU.log %Q{How to interact with your EKS cluster\nkubectl --kubeconfig "#{kube_conf}" get all\nkubectl --kubeconfig "#{kube_conf}" create -f some_k8s_deploy.yml\nkubectl --kubeconfig "#{kube_conf}" get nodes}, MU::SUMMARY
1630
+ end
1631
+
1632
+ def create_fargate_kubernetes_profile
1633
+ fargate_subnets = mySubnets.map { |s| s.cloud_id }
1634
+
1635
+ podrole_arn = @deploy.findLitterMate(name: @config['name']+"pods", type: "roles").arn
1636
+ poolnum = 0
1637
+
1638
+ @config['kubernetes_pools'].each { |selectors|
1639
+ profname = @mu_name+"-"+poolnum.to_s
1640
+ poolnum += 1
1641
+ desc = {
1642
+ :fargate_profile_name => profname,
1643
+ :cluster_name => @mu_name,
1644
+ :pod_execution_role_arn => podrole_arn,
1645
+ :selectors => selectors,
1646
+ :subnets => fargate_subnets.sort,
1647
+ :tags => @tags
1648
+ }
1649
+ begin
1650
+ resp = MU::Cloud::AWS.eks(region: @config['region'], credentials: @config['credentials']).describe_fargate_profile(
1651
+ cluster_name: @mu_name,
1652
+ fargate_profile_name: profname
1653
+ )
1654
+ if resp and resp.fargate_profile
1655
+ old_desc = MU.structToHash(resp.fargate_profile, stringify_keys: true)
1656
+ new_desc = MU.structToHash(desc, stringify_keys: true)
1657
+ ["created_at", "status", "fargate_profile_arn"].each { |k|
1658
+ old_desc.delete(k)
1659
+ }
1660
+ old_desc["subnets"].sort!
1661
+ if !old_desc.eql?(new_desc)
1662
+ MU.log "Deleting Fargate profile #{profname} in order to apply changes", MU::WARN, details: desc
1663
+ MU::Cloud::AWS::ContainerCluster.purge_fargate_profile(profname, @mu_name, @config['region'], @credentials)
1664
+ else
1665
+ next
1666
+ end
1667
+ end
1668
+ rescue Aws::EKS::Errors::ResourceNotFoundException
1669
+ # This is just fine!
1670
+ end
1671
+ MU.log "Creating EKS Fargate profile #{profname}", details: desc
1672
+ resp = MU::Cloud::AWS.eks(region: @config['region'], credentials: @config['credentials']).create_fargate_profile(desc)
1673
+ begin
1674
+ resp = MU::Cloud::AWS.eks(region: @config['region'], credentials: @config['credentials']).describe_fargate_profile(
1675
+ cluster_name: @mu_name,
1676
+ fargate_profile_name: profname
1677
+ )
1678
+ sleep 1 if resp.fargate_profile.status == "CREATING"
1679
+ end while resp.fargate_profile.status == "CREATING"
1680
+ MU.log "Creation of EKS Fargate profile #{profname} complete"
1681
+ }
1682
+ end
1683
+
1684
+ def self.remove_kubernetes_tags(cluster, desc, region: MU.myRegion, credentials: nil, noop: false)
1685
+ untag = []
1686
+ untag << desc.resources_vpc_config.vpc_id
1687
+ subnets = MU::Cloud::AWS.ec2(credentials: credentials, region: region).describe_subnets(
1688
+ filters: [ { name: "vpc-id", values: [desc.resources_vpc_config.vpc_id] } ]
1689
+ ).subnets
1690
+
1691
+ # subnets
1692
+ untag.concat(subnets.map { |s| s.subnet_id } )
1693
+ rtbs = MU::Cloud::AWS.ec2(credentials: credentials, region: region).describe_route_tables(
1694
+ filters: [ { name: "vpc-id", values: [desc.resources_vpc_config.vpc_id] } ]
1695
+ ).route_tables
1696
+ untag.concat(rtbs.map { |r| r.route_table_id } )
1697
+ untag.concat(desc.resources_vpc_config.subnet_ids)
1698
+ untag.concat(desc.resources_vpc_config.security_group_ids)
1699
+ MU.log "Removing Kubernetes tags from VPC resources for #{cluster}", details: untag
1700
+ if !noop
1701
+ MU::Cloud::AWS.removeTag("kubernetes.io/cluster/#{cluster}", "shared", untag)
1702
+ MU::Cloud::AWS.removeTag("kubernetes.io/cluster/elb", cluster, untag)
1703
+ end
1704
+ end
1705
+ private_class_method :remove_kubernetes_tags
1706
+
1707
+ def apply_kubernetes_tags
1708
+ tagme = [@vpc.cloud_id]
1709
+ tagme_elb = []
1710
+ @vpc.subnets.each { |s|
1711
+ tagme << s.cloud_id
1712
+ tagme_elb << s.cloud_id if !s.private?
1713
+ }
1714
+ rtbs = MU::Cloud::AWS.ec2(region: @config['region'], credentials: @config['credentials']).describe_route_tables(
1715
+ filters: [ { name: "vpc-id", values: [@vpc.cloud_id] } ]
1716
+ ).route_tables
1717
+ tagme.concat(rtbs.map { |r| r.route_table_id } )
1718
+ main_sg = @deploy.findLitterMate(type: "firewall_rules", name: "server_pool#{@config['name']}workers")
1719
+ tagme << main_sg.cloud_id if main_sg
1720
+ MU.log "Applying kubernetes.io tags to VPC resources", details: tagme
1721
+ MU::Cloud::AWS.createTag(tagme, "kubernetes.io/cluster/#{@mu_name}", "shared", credentials: @config['credentials'])
1722
+ MU::Cloud::AWS.createTag(tagme_elb, "kubernetes.io/cluster/elb", @mu_name, credentials: @config['credentials'])
1723
+ end
1724
+
1725
+ def manage_ecs_workers
1726
+ resp = MU::Cloud::AWS.ecs(region: @config['region'], credentials: @config['credentials']).list_container_instances({
1727
+ cluster: @mu_name
1728
+ })
1729
+ existing = {}
1730
+ if resp
1731
+ uuids = []
1732
+ resp.container_instance_arns.each { |arn|
1733
+ uuids << arn.sub(/^.*?:container-instance\//, "")
1734
+ }
1735
+ if uuids.size > 0
1736
+ resp = MU::Cloud::AWS.ecs(region: @config['region'], credentials: @config['credentials']).describe_container_instances({
1737
+ cluster: @mu_name,
1738
+ container_instances: uuids
1739
+ })
1740
+ resp.container_instances.each { |i|
1741
+ existing[i.ec2_instance_id] = i
1742
+ }
1743
+ end
1744
+ end
1745
+
1746
+ threads = []
1747
+ resource_lookup = MU::Cloud::AWS.listInstanceTypes(@config['region'])[@config['region']]
1748
+ serverpool = if ['EKS', 'ECS'].include?(@config['flavor'])
1749
+ @deploy.findLitterMate(type: "server_pools", name: @config["name"]+"workers")
1750
+ end
1751
+ serverpool.listNodes.each { |mynode|
1752
+ resources = resource_lookup[mynode.cloud_desc.instance_type]
1753
+ threads << Thread.new(mynode) { |node|
1754
+ ident_doc = nil
1755
+ ident_doc_sig = nil
1756
+ if !node.windows?
1757
+ session = node.getSSHSession(10, 30)
1758
+ ident_doc = session.exec!("curl -s http://169.254.169.254/latest/dynamic/instance-identity/document/")
1759
+ ident_doc_sig = session.exec!("curl -s http://169.254.169.254/latest/dynamic/instance-identity/signature/")
1760
+ # else
1761
+ # begin
1762
+ # session = node.getWinRMSession(1, 60)
1763
+ # rescue StandardError # XXX
1764
+ # session = node.getSSHSession(1, 60)
1765
+ # end
1766
+ end
1767
+ MU.log "Identity document for #{node}", MU::DEBUG, details: ident_doc
1768
+ MU.log "Identity document signature for #{node}", MU::DEBUG, details: ident_doc_sig
1769
+ params = {
1770
+ :cluster => @mu_name,
1771
+ :instance_identity_document => ident_doc,
1772
+ :instance_identity_document_signature => ident_doc_sig,
1773
+ :total_resources => [
1774
+ {
1775
+ :name => "CPU",
1776
+ :type => "INTEGER",
1777
+ :integer_value => resources["vcpu"].to_i
1778
+ },
1779
+ {
1780
+ :name => "MEMORY",
1781
+ :type => "INTEGER",
1782
+ :integer_value => (resources["memory"]*1024*1024).to_i
1783
+ }
1784
+ ]
1785
+ }
1786
+ if !existing.has_key?(node.cloud_id)
1787
+ MU.log "Registering ECS instance #{node} in cluster #{@mu_name}", details: params
1788
+ else
1789
+ params[:container_instance_arn] = existing[node.cloud_id].container_instance_arn
1790
+ MU.log "Updating ECS instance #{node} in cluster #{@mu_name}", MU::NOTICE, details: params
1791
+ end
1792
+ MU::Cloud::AWS.ecs(region: @config['region'], credentials: @config['credentials']).register_container_instance(params)
1793
+
1794
+ }
1795
+ }
1796
+ threads.each { |t|
1797
+ t.join
1798
+ }
1799
+ end
1800
+
1801
+ def get_ecs_loadbalancers(container_name)
1802
+ lbs = []
1803
+
1804
+ if @loadbalancers and !@loadbalancers.empty?
1805
+ @loadbalancers.each {|lb|
1806
+ MU.log "Mapping LB #{lb.mu_name} to service #{c['name']}", MU::INFO
1807
+ if lb.cloud_desc.type != "classic"
1808
+ elb_groups = MU::Cloud::AWS.elb2(region: @config['region'], credentials: @config['credentials']).describe_target_groups({
1809
+ load_balancer_arn: lb.cloud_desc.load_balancer_arn
1810
+ })
1811
+ matching_target_groups = []
1812
+ elb_groups.target_groups.each { |tg|
1813
+ if tg.port.to_i == lb['container_port'].to_i
1814
+ matching_target_groups << {
1815
+ arn: tg['target_group_arn'],
1816
+ name: tg['target_group_name']
1817
+ }
1818
+ end
1819
+ }
1820
+ if matching_target_groups.length >= 1
1821
+ MU.log "#{matching_target_groups.length} matching target groups lb. Mapping #{container_name} to target group #{matching_target_groups.first['name']}", MU::INFO
1822
+ lbs << {
1823
+ container_name: container_name,
1824
+ container_port: lb['container_port'],
1825
+ target_group_arn: matching_target_groups.first[:arn]
1826
+ }
1827
+ else
1828
+ raise MuError, "No matching target groups lb"
1829
+ end
1830
+ elsif @config['flavor'] == "Fargate" && lb.cloud_desc.type == "classic"
1831
+ raise MuError, "Classic Load Balancers are not supported with Fargate."
1832
+ else
1833
+ MU.log "Mapping Classic LB #{lb.mu_name} to service #{container_name}", MU::INFO
1834
+ lbs << {
1835
+ container_name: container_name,
1836
+ container_port: lb['container_port'],
1837
+ load_balancer_name: lb.mu_name
1838
+ }
1839
+ end
1840
+ }
1841
+ end
1842
+
1843
+ lbs
1844
+ end
1845
+
1846
+ def get_ecs_container_definitions(containers)
1847
+ role_arn = nil
1848
+ lbs = []
1849
+
1850
+ defs = containers.map { |c|
1851
+ container_name = @mu_name+"-"+c['name'].upcase
1852
+ lbs.concat(get_ecs_loadbalancers(container_name))
1853
+
1854
+ if c["role"] and !role_arn
1855
+ found = MU::MommaCat.findStray(
1856
+ @config['cloud'],
1857
+ "role",
1858
+ cloud_id: c["role"]["id"],
1859
+ name: c["role"]["name"],
1860
+ deploy_id: c["role"]["deploy_id"] || @deploy.deploy_id,
1861
+ dummy_ok: false
1862
+ )
1863
+ if found
1864
+ found = found.first
1865
+ if found and found.cloudobj
1866
+ role_arn = found.cloudobj.arn
1867
+ end
1868
+ else
1869
+ raise MuError, "Unable to find execution role from #{c["role"]}"
1870
+ end
1871
+ end
1872
+
1873
+ params = {
1874
+ name: @mu_name+"-"+c['name'].upcase,
1875
+ image: c['image'],
1876
+ memory: c['memory'],
1877
+ cpu: c['cpu']
1878
+ }
1879
+ if !@config['vpc']
1880
+ c['hostname'] ||= @mu_name+"-"+c['name'].upcase
1881
+ end
1882
+ [:essential, :hostname, :start_timeout, :stop_timeout, :user, :working_directory, :disable_networking, :privileged, :readonly_root_filesystem, :interactive, :pseudo_terminal, :links, :entry_point, :command, :dns_servers, :dns_search_domains, :docker_security_options, :port_mappings, :repository_credentials, :mount_points, :environment, :volumes_from, :secrets, :depends_on, :extra_hosts, :docker_labels, :ulimits, :system_controls, :health_check, :resource_requirements].each { |param|
1883
+ if c.has_key?(param.to_s)
1884
+ params[param] = if !c[param.to_s].nil? and (c[param.to_s].is_a?(Hash) or c[param.to_s].is_a?(Array))
1885
+ MU.strToSym(c[param.to_s])
1886
+ else
1887
+ c[param.to_s]
1888
+ end
1889
+ end
1890
+ }
1891
+ if @config['vpc']
1892
+ [:hostname, :dns_servers, :dns_search_domains, :links].each { |param|
1893
+ if params[param]
1894
+ MU.log "Container parameter #{param.to_s} not supported in VPC clusters, ignoring", MU::WARN
1895
+ params.delete(param)
1896
+ end
1897
+ }
1898
+ end
1899
+ if @config['flavor'] == "Fargate"
1900
+ [:privileged, :docker_security_options].each { |param|
1901
+ if params[param]
1902
+ MU.log "Container parameter #{param.to_s} not supported in Fargate clusters, ignoring", MU::WARN
1903
+ params.delete(param)
1904
+ end
1905
+ }
1906
+ end
1907
+ if c['log_configuration']
1908
+ log_obj = @deploy.findLitterMate(name: c['log_configuration']['options']['awslogs-group'], type: "logs")
1909
+ if log_obj
1910
+ c['log_configuration']['options']['awslogs-group'] = log_obj.mu_name
1911
+ end
1912
+ params[:log_configuration] = MU.strToSym(c['log_configuration'])
1913
+ end
1914
+ params
1915
+ }
1916
+
1917
+ [defs, role_arn, lbs]
1918
+ end
1919
+
1920
+ def register_ecs_task(container_definitions, service_name, cpu_total = 2, mem_total = 2, role_arn: nil)
1921
+ task_params = {
1922
+ family: @deploy.deploy_id,
1923
+ container_definitions: container_definitions,
1924
+ requires_compatibilities: [@config['flavor'] == "ECS" ? "EC2" : "FARGATE"]
1925
+ }
1926
+
1927
+ if @config['volumes']
1928
+ task_params[:volumes] = []
1929
+ @config['volumes'].each { |v|
1930
+ vol = { :name => v['name'] }
1931
+ if v['type'] == "host"
1932
+ vol[:host] = {}
1933
+ if v['host_volume_source_path']
1934
+ vol[:host][:source_path] = v['host_volume_source_path']
1935
+ end
1936
+ elsif v['type'] == "docker"
1937
+ vol[:docker_volume_configuration] = MU.strToSym(v['docker_volume_configuration'])
1938
+ else
1939
+ raise MuError, "Invalid volume type '#{v['type']}' specified in ContainerCluster '#{@mu_name}'"
1940
+ end
1941
+ task_params[:volumes] << vol
1942
+ }
1943
+ end
1944
+
1945
+ if role_arn
1946
+ task_params[:execution_role_arn] = role_arn
1947
+ task_params[:task_role_arn] = role_arn
1948
+ end
1949
+ if @config['flavor'] == "Fargate"
1950
+ task_params[:network_mode] = "awsvpc"
1951
+ task_params[:cpu] = cpu_total.to_i.to_s
1952
+ task_params[:memory] = mem_total.to_i.to_s
1953
+ elsif @config['vpc']
1954
+ task_params[:network_mode] = "awsvpc"
1955
+ end
1956
+
1957
+ MU.log "Registering task definition #{service_name} with #{container_definitions.size.to_s} containers"
1958
+
1959
+ # XXX this helpfully keeps revisions, but let's compare anyway and avoid cluttering with identical ones
1960
+ resp = MU::Cloud::AWS.ecs(region: @config['region'], credentials: @config['credentials']).register_task_definition(task_params)
1961
+
1962
+ resp.task_definition.task_definition_arn
1963
+ end
1964
+
1965
+ def list_ecs_services
1966
+ svc_resp = nil
1967
+ MU.retrier([Aws::ECS::Errors::ClusterNotFoundException], wait: 5, max: 10){
1968
+ svc_resp = MU::Cloud::AWS.ecs(region: @config['region'], credentials: @config['credentials']).list_services(
1969
+ cluster: arn
1970
+ )
1971
+ }
1972
+
1973
+ svc_resp.service_arns.map { |s|
1974
+ s.gsub(/.*?:service\/(.*)/, '\1')
1975
+ }
1976
+ end
1977
+
1978
+ def create_update_ecs_service(task_def, service_name, lbs, existing_svcs)
1979
+ service_params = {
1980
+ :cluster => @mu_name,
1981
+ :desired_count => @config['instance_count'], # XXX this makes no sense
1982
+ :service_name => service_name,
1983
+ :launch_type => @config['flavor'] == "ECS" ? "EC2" : "FARGATE",
1984
+ :task_definition => task_def,
1985
+ :load_balancers => lbs
1986
+ }
1987
+ if @config['vpc']
1988
+ subnet_ids = []
1989
+ all_public = true
1990
+
1991
+ mySubnets.each { |subnet|
1992
+ subnet_ids << subnet.cloud_id
1993
+ all_public = false if subnet.private?
1994
+ }
1995
+
1996
+ service_params[:network_configuration] = {
1997
+ :awsvpc_configuration => {
1998
+ :subnets => subnet_ids,
1999
+ :security_groups => myFirewallRules.map { |fw| fw.cloud_id },
2000
+ :assign_public_ip => all_public ? "ENABLED" : "DISABLED"
2001
+ }
2002
+ }
2003
+ end
2004
+
2005
+ if !existing_svcs.include?(service_name)
2006
+ MU.log "Creating Service #{service_name}"
2007
+
2008
+ MU::Cloud::AWS.ecs(region: @config['region'], credentials: @config['credentials']).create_service(service_params)
2009
+ else
2010
+ service_params[:service] = service_params[:service_name].dup
2011
+ service_params.delete(:service_name)
2012
+ service_params.delete(:launch_type)
2013
+ MU.log "Updating Service #{service_name}", MU::NOTICE, details: service_params
2014
+
2015
+ MU::Cloud::AWS.ecs(region: @config['region'], credentials: @config['credentials']).update_service(service_params)
2016
+ end
2037
2017
  end
2038
2018
 
2039
2019
  end