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
@@ -124,13 +124,15 @@ module MU
124
124
  # @param ignoremaster [Boolean]: If true, will remove resources not flagged as originating from this Mu server
125
125
  # @param region [String]: The cloud provider region
126
126
  # @return [void]
127
- def self.cleanup(noop: false, ignoremaster: false, region: MU.curRegion, credentials: nil, flags: {})
127
+ def self.cleanup(noop: false, deploy_id: MU.deploy_id, ignoremaster: false, region: MU.curRegion, credentials: nil, flags: {})
128
+ MU.log "AWS::Alarm.cleanup: need to support flags['known']", MU::DEBUG, details: flags
129
+ MU.log "Placeholder: AWS Alarm artifacts do not support tags, so ignoremaster cleanup flag has no effect", MU::DEBUG, details: ignoremaster
128
130
  alarms = []
129
131
  # We don't have a way to tag alarms, so we try to delete them by the deploy ID.
130
132
  # This can miss alarms in some cases (eg. cache_cluster) so we might want to delete alarms from each API as well.
131
133
  MU::Cloud::AWS.cloudwatch(credentials: credentials, region: region).describe_alarms.each { |page|
132
134
  page.metric_alarms.map(&:alarm_name).each { |alarm_name|
133
- alarms << alarm_name if alarm_name.match(MU.deploy_id)
135
+ alarms << alarm_name if alarm_name.match(deploy_id)
134
136
  }
135
137
  }
136
138
 
@@ -252,9 +254,9 @@ module MU
252
254
  end
253
255
 
254
256
  # Cloud-specific configuration properties.
255
- # @param config [MU::Config]: The calling MU::Config object
257
+ # @param _config [MU::Config]: The calling MU::Config object
256
258
  # @return [Array<Array,Hash>]: List of required fields, and json-schema Hash of cloud-specific configuration parameters for this resource
257
- def self.schema(config)
259
+ def self.schema(_config)
258
260
  toplevel_required = []
259
261
  schema = {}
260
262
  [toplevel_required, schema]
@@ -319,7 +321,7 @@ module MU
319
321
  if !depclass.nil?
320
322
  dimension["depclass"] = depclass
321
323
  if !dimension["name"].nil? and !dimension["name"].empty?
322
- alarm["dependencies"] << { "name" => dimension["name"], "type" => depclass }
324
+ MU::Config.addDependency(alarm, dimension["name"], depclass)
323
325
  end
324
326
  end
325
327
  }
@@ -21,6 +21,12 @@ module MU
21
21
  @@region_cache = {}
22
22
  @@region_cache_semaphore = Mutex.new
23
23
 
24
+ # Map some filename extensions to mime types. S3 does most of this on
25
+ # its own, add to this for cases it doesn't cover.
26
+ MIME_MAP = {
27
+ ".svg" => "image/svg+xml"
28
+ }
29
+
24
30
  # 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.
25
31
  # @param args [Hash]: Hash of named arguments passed via Ruby's double-splat
26
32
  def initialize(**args)
@@ -33,7 +39,7 @@ module MU
33
39
  bucket_name = @deploy.getResourceName(@config["name"], max_length: 63).downcase
34
40
 
35
41
  MU.log "Creating S3 bucket #{bucket_name}"
36
- resp = MU::Cloud::AWS.s3(credentials: @config['credentials'], region: @config['region']).create_bucket(
42
+ MU::Cloud::AWS.s3(credentials: @config['credentials'], region: @config['region']).create_bucket(
37
43
  acl: @config['acl'],
38
44
  bucket: bucket_name
39
45
  )
@@ -81,6 +87,35 @@ module MU
81
87
 
82
88
  end
83
89
 
90
+ # @return [String]
91
+ def url
92
+ "https://#{@cloud_id}.s3.amazonaws.com"
93
+ end
94
+
95
+ # Grant access via our bucket policy to the specified resource
96
+ # @param principal [String]
97
+ # @param permissions [Array<String>]
98
+ # @param paths [Array<String>]
99
+ def allowPrincipal(principal, permissions: ["GetObject", "ListBucket"], paths: [""], doc_id: nil, name: nil)
100
+ @config['policies'] ||= []
101
+ name ||= principal.sub(/.*?([0-9a-z\-_]+)$/i, '\1')
102
+ @config['policies'] << {
103
+ "name" => name,
104
+ "grant_to" => [ { "identifier" => principal } ],
105
+ "permissions" => permissions.map { |p| "s3:"+p },
106
+ "flag" => "allow",
107
+ "targets" => paths.map { |p|
108
+ {
109
+ "path" => p,
110
+ "type" => "bucket",
111
+ "identifier" => @config['name']
112
+ }
113
+ }
114
+ }
115
+
116
+ applyPolicies(doc_id: doc_id)
117
+ end
118
+
84
119
  # Called automatically by {MU::Deploy#createResources}
85
120
  def groom
86
121
 
@@ -90,21 +125,46 @@ module MU
90
125
  tagBucket if !@config['scrub_mu_isms']
91
126
 
92
127
  current = cloud_desc
128
+ applyPolicies if @config['policies']
93
129
 
94
- if @config['policies']
95
- @config['policies'].each { |pol|
96
- pol['grant_to'] ||= [
97
- { "id" => "*" }
98
- ]
99
- }
130
+ if @config['versioning'] and current["versioning"].status != "Enabled"
131
+ MU.log "Enabling versioning on S3 bucket #{@cloud_id}", MU::NOTICE
132
+ MU::Cloud::AWS.s3(credentials: @config['credentials'], region: @config['region']).put_bucket_versioning(
133
+ bucket: @cloud_id,
134
+ versioning_configuration: {
135
+ mfa_delete: "Disabled",
136
+ status: "Enabled"
137
+ }
138
+ )
139
+ elsif !@config['versioning'] and current["versioning"].status == "Enabled"
140
+ MU.log "Suspending versioning on S3 bucket #{@cloud_id}", MU::NOTICE
141
+ MU::Cloud::AWS.s3(credentials: @config['credentials'], region: @config['region']).put_bucket_versioning(
142
+ bucket: @cloud_id,
143
+ versioning_configuration: {
144
+ mfa_delete: "Disabled",
145
+ status: "Suspended"
146
+ }
147
+ )
148
+ end
149
+
150
+ if @config['upload']
151
+ @config['upload'].each { |batch|
152
+ urlbase = "s3://"+@cloud_id+batch['destination']
153
+ urlbase += "/" if urlbase !~ /\/$/
154
+ upload_me = if File.directory?(batch['source'])
155
+ Dir[batch['source']+'/**/*'].reject {|d|
156
+ File.directory?(d)
157
+ }.map { |f|
158
+ [ f, urlbase+f.sub(/^#{Regexp.quote(batch['source'])}\/?/, '') ]
159
+ }
160
+ else
161
+ batch['source'].match(/([^\/]+)$/)
162
+ [ [batch['source'], urlbase+Regexp.last_match[1]] ]
163
+ end
100
164
 
101
- policy_docs = MU::Cloud::AWS::Role.genPolicyDocument(@config['policies'], deploy_obj: @deploy)
102
- policy_docs.each { |doc|
103
- MU.log "Applying S3 bucket policy #{doc.keys.first} to bucket #{@cloud_id}", MU::NOTICE, details: doc.values.first
104
- MU::Cloud::AWS.s3(credentials: @config['credentials'], region: @config['region']).put_bucket_policy(
105
- bucket: @cloud_id,
106
- policy: JSON.generate(doc.values.first)
107
- )
165
+ Hash[upload_me].each_pair { |file, url|
166
+ self.class.upload(url, file: file, credentials: @credentials, region: @config['region'], acl: batch['acl'])
167
+ }
108
168
  }
109
169
  end
110
170
 
@@ -121,6 +181,23 @@ module MU
121
181
  }
122
182
  }
123
183
  )
184
+ ['web_error_object', 'web_index_object'].each { |key|
185
+ begin
186
+ MU::Cloud::AWS.s3(credentials: @config['credentials'], region: @config['region']).head_object(
187
+ bucket: @cloud_id,
188
+ key: @config[key]
189
+ )
190
+ rescue Aws::S3::Errors::NotFound
191
+ MU.log "Uploading placeholder #{@config[key]} to bucket #{@cloud_id}"
192
+ MU::Cloud::AWS.s3(credentials: @config['credentials'], region: @config['region']).put_object(
193
+ acl: "public-read",
194
+ bucket: @cloud_id,
195
+ key: @config[key],
196
+ body: ""
197
+ )
198
+ end
199
+ }
200
+ # XXX check if error and index objs exist, and if not provide placeholders
124
201
  elsif !@config['web'] and !current["website"].nil?
125
202
  MU.log "Disabling web service on S3 bucket #{@cloud_id}", MU::NOTICE
126
203
  MU::Cloud::AWS.s3(credentials: @config['credentials'], region: @config['region']).delete_bucket_website(
@@ -128,25 +205,38 @@ module MU
128
205
  )
129
206
  end
130
207
 
131
- if @config['versioning'] and current["versioning"].status != "Enabled"
132
- MU.log "Enabling versioning on S3 bucket #{@cloud_id}", MU::NOTICE
133
- MU::Cloud::AWS.s3(credentials: @config['credentials'], region: @config['region']).put_bucket_versioning(
134
- bucket: @cloud_id,
135
- versioning_configuration: {
136
- mfa_delete: "Disabled",
137
- status: "Enabled"
208
+ symbolify_keys = Proc.new { |parent|
209
+ if parent.is_a?(Hash)
210
+ newhash = {}
211
+ parent.each_pair { |k, v|
212
+ newhash[k.to_sym] = symbolify_keys.call(v)
138
213
  }
139
- )
140
- elsif !@config['versioning'] and current["versioning"].status == "Enabled"
141
- MU.log "Suspending versioning on S3 bucket #{@cloud_id}", MU::NOTICE
142
- MU::Cloud::AWS.s3(credentials: @config['credentials'], region: @config['region']).put_bucket_versioning(
214
+ newhash
215
+ elsif parent.is_a?(Array)
216
+ newarr = []
217
+ parent.each { |child|
218
+ newarr << symbolify_keys.call(child)
219
+ }
220
+ newarr
221
+ else
222
+ parent
223
+ end
224
+ }
225
+
226
+ if @config['cors']
227
+ MU.log "Setting CORS rules on #{@cloud_id}", details: @config['cors']
228
+ MU::Cloud::AWS.s3(credentials: @config['credentials'], region: @config['region']).put_bucket_cors(
143
229
  bucket: @cloud_id,
144
- versioning_configuration: {
145
- mfa_delete: "Disabled",
146
- status: "Suspended"
230
+ cors_configuration: {
231
+ cors_rules: symbolify_keys.call(@config['cors'])
147
232
  }
148
233
  )
149
234
  end
235
+
236
+ MU.log "Bucket #{@config['name']}: s3://#{@cloud_id}", MU::SUMMARY
237
+ if @config['web']
238
+ MU.log "Bucket #{@config['name']} web access: http://#{@cloud_id}.s3-website-#{@config['region']}.amazonaws.com/", MU::SUMMARY
239
+ end
150
240
  end
151
241
 
152
242
  # Upload a file to a bucket.
@@ -161,7 +251,7 @@ module MU
161
251
 
162
252
  if file and !file.empty?
163
253
  if !File.exist?(file) or !File.readable?(file)
164
- raise MuError, "Unable to read #{file} for upload to #{url}"
254
+ raise MuError, "Unable to read #{file} for upload to #{url} (I'm at #{Dir.pwd}"
165
255
  else
166
256
  data = File.read(file)
167
257
  end
@@ -177,17 +267,20 @@ module MU
177
267
  end
178
268
 
179
269
  begin
180
- puts data
181
- puts acl
182
- puts bucket
183
- puts path
184
270
  MU.log "Writing #{path} to S3 bucket #{bucket}"
185
- MU::Cloud::AWS.s3(region: region, credentials: credentials).put_object(
271
+ params = {
186
272
  acl: acl,
187
273
  bucket: bucket,
188
274
  key: path,
189
275
  body: data
190
- )
276
+ }
277
+
278
+ MIME_MAP.each_pair { |extension, content_type|
279
+ if path =~ /#{Regexp.quote(extension)}$/i
280
+ params[:content_type] = content_type
281
+ end
282
+ }
283
+ MU::Cloud::AWS.s3(region: region, credentials: credentials).put_object(params)
191
284
  rescue Aws::S3::Errors => e
192
285
  raise MuError, "Got #{e.inspect} trying to write #{path} to #{bucket} (region: #{region}, credentials: #{credentials})"
193
286
  end
@@ -212,7 +305,8 @@ puts path
212
305
  # @param ignoremaster [Boolean]: If true, will remove resources not flagged as originating from this Mu server
213
306
  # @param region [String]: The cloud provider region
214
307
  # @return [void]
215
- def self.cleanup(noop: false, ignoremaster: false, region: MU.curRegion, credentials: nil, flags: {})
308
+ def self.cleanup(noop: false, deploy_id: MU.deploy_id, ignoremaster: false, region: MU.curRegion, credentials: nil, flags: {})
309
+ MU.log "AWS::Bucket.cleanup: need to support flags['known']", MU::DEBUG, details: flags
216
310
 
217
311
  resp = MU::Cloud::AWS.s3(credentials: credentials, region: region).list_buckets
218
312
  if resp and resp.buckets
@@ -243,15 +337,36 @@ puts path
243
337
 
244
338
  begin
245
339
  tags = MU::Cloud::AWS.s3(credentials: credentials, region: region).get_bucket_tagging(bucket: bucket.name).tag_set
340
+ deploy_match = false
341
+ master_match = false
246
342
  tags.each { |tag|
247
- if tag.key == "MU-ID" and tag.value == MU.deploy_id
248
- MU.log "Deleting S3 Bucket #{bucket.name}"
249
- if !noop
250
- MU::Cloud::AWS.s3(credentials: credentials, region: region).delete_bucket(bucket: bucket.name)
251
- end
252
- break
343
+ if tag.key == "MU-ID" and tag.value == deploy_id
344
+ deploy_match = true
345
+ elsif tag.key == "MU-MASTER-IP" and tag.value == MU.mu_public_ip
346
+ master_match = true
253
347
  end
254
348
  }
349
+ if deploy_match and (ignoremaster or master_match)
350
+ MU.log "Deleting S3 Bucket #{bucket.name}"
351
+ if !noop
352
+ MU::Cloud::AWS.s3(credentials: credentials, region: region).delete_bucket(bucket: bucket.name)
353
+ end
354
+ end
355
+ rescue Aws::S3::Errors::BucketNotEmpty => e
356
+ if flags["skipsnapshots"]
357
+ del = MU::Cloud::AWS.s3(credentials: credentials, region: region).list_objects(bucket: bucket.name).contents.map { |o| { key: o.key } }
358
+ del.concat(MU::Cloud::AWS.s3(credentials: credentials, region: region).list_object_versions(bucket: bucket.name).versions.map { |o| { key: o.key, version_id: o.version_id } })
359
+
360
+ MU.log "Purging #{del.size.to_s} objects and versions from #{bucket.name}"
361
+ begin
362
+ batch = del.slice!(0, (del.length >= 1000 ? 1000 : del.length))
363
+ MU::Cloud::AWS.s3(credentials: credentials, region: region).delete_objects(bucket: bucket.name, delete: { objects: batch } ) if !noop
364
+ end while del.size > 0
365
+
366
+ retry if !noop
367
+ else
368
+ MU.log "Bucket #{bucket.name} is non-empty, will preserve it and its contents. Use --skipsnapshots to forcibly remove.", MU::WARN
369
+ end
255
370
  rescue Aws::S3::Errors::NoSuchTagSet, Aws::S3::Errors::PermanentRedirect
256
371
  next
257
372
  end
@@ -277,9 +392,32 @@ puts path
277
392
  def self.find(**args)
278
393
  found = {}
279
394
 
395
+ args[:region] ||= MU::Cloud::AWS.myRegion(args[:credentials])
396
+ if args[:flags] and args[:flags][:allregions]
397
+ args[:allregions] = args[:flags][:allregions]
398
+ end
399
+ minimal = args[:full] ? false : true
400
+
401
+ location = Proc.new { |name|
402
+ begin
403
+ loc_resp = MU::Cloud::AWS.s3(credentials: args[:credentials], region: args[:region]).get_bucket_location(bucket: name)
404
+
405
+ if loc_resp.location_constraint and !loc_resp.location_constraint.empty?
406
+ loc_resp.location_constraint
407
+ else
408
+ nil
409
+ end
410
+ rescue Aws::S3::Errors::AccessDenied
411
+ nil
412
+ end
413
+ }
414
+
280
415
  if args[:cloud_id]
281
416
  begin
282
- found[args[:cloud_id]] = describe_bucket(args[:cloud_id], minimal: true, credentials: args[:credentials], region: args[:region])
417
+ found[args[:cloud_id]] = describe_bucket(args[:cloud_id], minimal: minimal, credentials: args[:credentials], region: args[:region])
418
+ found[args[:cloud_id]]['region'] ||= location.call(args[:cloud_id])
419
+ found[args[:cloud_id]]['region'] ||= args[:region]
420
+ found[args[:cloud_id]]['name'] ||= args[:cloud_id]
283
421
  rescue ::Aws::S3::Errors::NoSuchBucket
284
422
  end
285
423
  else
@@ -287,11 +425,14 @@ puts path
287
425
  if resp and resp.buckets
288
426
  resp.buckets.each { |b|
289
427
  begin
290
- loc_resp = MU::Cloud::AWS.s3(credentials: args[:credentials], region: args[:region]).get_bucket_location(bucket: b.name)
291
- if !loc_resp or loc_resp.location_constraint != args[:region]
428
+ bucket_region = location.call(b.name)
429
+ if !args[:allregions] and bucket_region != args[:region]
292
430
  next
293
431
  end
294
- found[b.name] = describe_bucket(b.name, minimal: true, credentials: args[:credentials], region: args[:region])
432
+ bucket_region ||= args[:region]
433
+ found[b.name] = describe_bucket(b.name, minimal: minimal, credentials: args[:credentials], region: bucket_region)
434
+ found[b.name]["region"] ||= bucket_region
435
+ found[b.name]['name'] ||= b.name
295
436
  rescue Aws::S3::Errors::AccessDenied
296
437
  end
297
438
  }
@@ -301,22 +442,96 @@ puts path
301
442
  found
302
443
  end
303
444
 
445
+ # Reverse-map our cloud description into a runnable config hash.
446
+ # We assume that any values we have in +@config+ are placeholders, and
447
+ # calculate our own accordingly based on what's live in the cloud.
448
+ def toKitten(**_args)
449
+ bok = {
450
+ "cloud" => "AWS",
451
+ "credentials" => @config['credentials'],
452
+ "cloud_id" => @cloud_id
453
+ }
454
+
455
+ if @cloud_id =~ /espier/i
456
+ MU.log @cloud_id, MU::WARN, details: cloud_desc
457
+ end
458
+
459
+ if !cloud_desc
460
+ MU.log "toKitten failed to load a cloud_desc from #{@cloud_id}", MU::ERR, details: @config
461
+ return nil
462
+ end
463
+
464
+ nil
465
+ end
466
+
304
467
  # Cloud-specific configuration properties.
305
- # @param config [MU::Config]: The calling MU::Config object
468
+ # @param _config [MU::Config]: The calling MU::Config object
306
469
  # @return [Array<Array,Hash>]: List of required fields, and json-schema Hash of cloud-specific configuration parameters for this resource
307
- def self.schema(config)
470
+ def self.schema(_config)
308
471
  toplevel_required = []
309
472
  schema = {
310
- "policies" => MU::Cloud::AWS::Role.condition_schema,
311
- "acl" => {
312
- "type" => "string",
313
- "enum" => ["private", "public-read", "public-read-write", "authenticated-read"],
314
- "default" => "private"
473
+ "policies" => MU::Cloud.resourceClass("AWS", "Role").condition_schema,
474
+ "upload" => {
475
+ "items" => {
476
+ "properties" => {
477
+ "acl" => {
478
+ "type" => "string",
479
+ "enum" => ["private", "public-read", "public-read-write", "authenticated-read"],
480
+ "default" => "private"
481
+ }
482
+ }
483
+ }
315
484
  },
316
485
  "storage_class" => {
317
486
  "type" => "string",
318
487
  "enum" => ["STANDARD", "REDUCED_REDUNDANCY", "STANDARD_IA", "ONEZONE_IA", "INTELLIGENT_TIERING", "GLACIER"],
319
488
  "default" => "STANDARD"
489
+ },
490
+ "cors" => {
491
+ "type" => "array",
492
+ "items" => {
493
+ "type" => "object",
494
+ "description" => "AWS S3 Cross-origin resource sharing policy",
495
+ "required" => ["allowed_origins"],
496
+ "properties" => {
497
+ "allowed_headers" => {
498
+ "type" => "array",
499
+ "default" => ["*"],
500
+ "items" => {
501
+ "type" => "string",
502
+ "description" => "Specifies which headers are allowed in a preflight request through the +Access-Control-Request-Headers+ header."
503
+ }
504
+ },
505
+ "allowed_methods" => {
506
+ "type" => "array",
507
+ "default" => ["GET"],
508
+ "items" => {
509
+ "type" => "string",
510
+ "enum" => %w{GET PUT POST DELETE HEAD},
511
+ "description" => "Specifies which HTTP methods for which cross-domain request are permitted"
512
+ }
513
+ },
514
+ "allowed_origins" => {
515
+ "type" => "array",
516
+ "items" => {
517
+ "type" => "string",
518
+ "description" => "Origins (in URL form) for which cross-domain request are permitted"
519
+ }
520
+ },
521
+ "expose_headers" => {
522
+ "type" => "array",
523
+ "items" => {
524
+ "type" => "string",
525
+ "description" => "Headers in the response which should be visible to the requesting application"
526
+ }
527
+ },
528
+ "max_age_seconds" => {
529
+ "type" => "integer",
530
+ "default" => 3600,
531
+ "description" => "Maximum cache time for preflight requests"
532
+ }
533
+ }
534
+ }
320
535
  }
321
536
  }
322
537
  [toplevel_required, schema]
@@ -325,15 +540,15 @@ puts path
325
540
  # Cloud-specific pre-processing of {MU::Config::BasketofKittens::bucket}, bare and unvalidated.
326
541
 
327
542
  # @param bucket [Hash]: The resource to process and validate
328
- # @param configurator [MU::Config]: The overall deployment configurator of which this resource is a member
543
+ # @param _configurator [MU::Config]: The overall deployment configurator of which this resource is a member
329
544
  # @return [Boolean]: True if validation succeeded, False otherwise
330
- def self.validateConfig(bucket, configurator)
545
+ def self.validateConfig(bucket, _configurator)
331
546
  ok = true
332
547
 
333
548
  if bucket['policies']
334
549
  bucket['policies'].each { |pol|
335
550
  if !pol['permissions'] or pol['permissions'].empty?
336
- pol['permissions'] = ["s3:GetObject"]
551
+ pol['permissions'] = ["s3:GetObject", "s3:ListBucket"]
337
552
  end
338
553
  }
339
554
  end
@@ -341,11 +556,13 @@ puts path
341
556
  ok
342
557
  end
343
558
 
344
- private
345
-
346
559
  # AWS doesn't really implement a useful describe_ method for S3 buckets;
347
560
  # instead we run the million little individual API calls to construct
348
561
  # an approximation for our uses
562
+ # @param bucket [String]:
563
+ # @param minimal [Boolean]:
564
+ # @param credentials [String]:
565
+ # @param region [String]:
349
566
  def self.describe_bucket(bucket, minimal: false, credentials: nil, region: nil)
350
567
  @@region_cache = {}
351
568
  @@region_cache_semaphore = Mutex.new
@@ -372,7 +589,7 @@ puts path
372
589
  }
373
590
  end
374
591
 
375
- rescue Aws::S3::Errors::NoSuchCORSConfiguration, Aws::S3::Errors::ServerSideEncryptionConfigurationNotFoundError, Aws::S3::Errors::NoSuchLifecycleConfiguration, Aws::S3::Errors::NoSuchBucketPolicy, Aws::S3::Errors::ReplicationConfigurationNotFoundError, Aws::S3::Errors::NoSuchTagSet, Aws::S3::Errors::NoSuchWebsiteConfiguration => e
592
+ rescue Aws::S3::Errors::NoSuchCORSConfiguration, Aws::S3::Errors::ServerSideEncryptionConfigurationNotFoundError, Aws::S3::Errors::NoSuchLifecycleConfiguration, Aws::S3::Errors::NoSuchBucketPolicy, Aws::S3::Errors::ReplicationConfigurationNotFoundError, Aws::S3::Errors::NoSuchTagSet, Aws::S3::Errors::NoSuchWebsiteConfiguration
376
593
  desc[method] = nil
377
594
  next
378
595
  end
@@ -380,6 +597,27 @@ puts path
380
597
  desc
381
598
  end
382
599
 
600
+ private
601
+
602
+ def applyPolicies(doc_id: nil)
603
+ return if !@config['policies']
604
+
605
+ @config['policies'].each { |pol|
606
+ pol['grant_to'] ||= [
607
+ { "id" => "*" }
608
+ ]
609
+ }
610
+
611
+ policy_docs = MU::Cloud.resourceClass("AWS", "Role").genPolicyDocument(@config['policies'], deploy_obj: @deploy, bucket_style: true, version: "2008-10-17", doc_id: doc_id)
612
+ policy_docs.each { |doc|
613
+ MU.log "Applying S3 bucket policy #{doc.keys.first} to bucket #{@cloud_id}", MU::NOTICE, details: JSON.pretty_generate(doc.values.first)
614
+ MU::Cloud::AWS.s3(credentials: @config['credentials'], region: @config['region']).put_bucket_policy(
615
+ bucket: @cloud_id,
616
+ policy: JSON.generate(doc.values.first)
617
+ )
618
+ }
619
+ end
620
+
383
621
  end
384
622
  end
385
623
  end