cloud-mu 3.2.0 → 3.5.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 (156) hide show
  1. checksums.yaml +4 -4
  2. data/Dockerfile +1 -1
  3. data/ansible/roles/mu-nat/tasks/main.yml +3 -0
  4. data/bin/mu-adopt +12 -1
  5. data/bin/mu-aws-setup +41 -7
  6. data/bin/mu-azure-setup +34 -0
  7. data/bin/mu-configure +214 -119
  8. data/bin/mu-gcp-setup +37 -2
  9. data/bin/mu-load-config.rb +2 -1
  10. data/bin/mu-node-manage +3 -0
  11. data/bin/mu-refresh-ssl +67 -0
  12. data/bin/mu-run-tests +28 -6
  13. data/bin/mu-self-update +30 -10
  14. data/bin/mu-upload-chef-artifacts +30 -26
  15. data/cloud-mu.gemspec +10 -8
  16. data/cookbooks/mu-master/attributes/default.rb +5 -1
  17. data/cookbooks/mu-master/metadata.rb +2 -2
  18. data/cookbooks/mu-master/recipes/default.rb +81 -26
  19. data/cookbooks/mu-master/recipes/init.rb +197 -62
  20. data/cookbooks/mu-master/recipes/update_nagios_only.rb +1 -1
  21. data/cookbooks/mu-master/recipes/vault.rb +78 -77
  22. data/cookbooks/mu-master/templates/default/mods/rewrite.conf.erb +1 -0
  23. data/cookbooks/mu-master/templates/default/nagios.conf.erb +103 -0
  24. data/cookbooks/mu-master/templates/default/web_app.conf.erb +14 -30
  25. data/cookbooks/mu-tools/attributes/default.rb +12 -0
  26. data/cookbooks/mu-tools/files/centos-6/CentOS-Base.repo +47 -0
  27. data/cookbooks/mu-tools/libraries/helper.rb +98 -4
  28. data/cookbooks/mu-tools/libraries/monkey.rb +1 -1
  29. data/cookbooks/mu-tools/recipes/apply_security.rb +31 -9
  30. data/cookbooks/mu-tools/recipes/aws_api.rb +8 -2
  31. data/cookbooks/mu-tools/recipes/base_repositories.rb +1 -1
  32. data/cookbooks/mu-tools/recipes/gcloud.rb +2 -9
  33. data/cookbooks/mu-tools/recipes/google_api.rb +7 -0
  34. data/cookbooks/mu-tools/recipes/rsyslog.rb +8 -1
  35. data/cookbooks/mu-tools/resources/disk.rb +113 -42
  36. data/cookbooks/mu-tools/resources/mommacat_request.rb +1 -2
  37. data/cookbooks/mu-tools/templates/centos-8/sshd_config.erb +215 -0
  38. data/extras/Gemfile.lock.bootstrap +394 -0
  39. data/extras/bucketstubs/error.html +0 -0
  40. data/extras/bucketstubs/index.html +0 -0
  41. data/extras/clean-stock-amis +11 -3
  42. data/extras/generate-stock-images +6 -3
  43. data/extras/git_rpm/build.sh +20 -0
  44. data/extras/git_rpm/mugit.spec +53 -0
  45. data/extras/image-generators/AWS/centos7.yaml +19 -16
  46. data/extras/image-generators/AWS/{rhel7.yaml → rhel71.yaml} +0 -0
  47. data/extras/image-generators/AWS/{win2k12.yaml → win2k12r2.yaml} +0 -0
  48. data/extras/image-generators/VMWare/centos8.yaml +15 -0
  49. data/extras/openssl_rpm/build.sh +19 -0
  50. data/extras/openssl_rpm/mussl.spec +46 -0
  51. data/extras/python_rpm/muthon.spec +14 -4
  52. data/extras/ruby_rpm/muby.spec +9 -5
  53. data/extras/sqlite_rpm/build.sh +19 -0
  54. data/extras/sqlite_rpm/muqlite.spec +47 -0
  55. data/install/installer +7 -5
  56. data/modules/mommacat.ru +2 -2
  57. data/modules/mu.rb +14 -7
  58. data/modules/mu/adoption.rb +5 -5
  59. data/modules/mu/cleanup.rb +47 -25
  60. data/modules/mu/cloud.rb +29 -1
  61. data/modules/mu/cloud/dnszone.rb +0 -2
  62. data/modules/mu/cloud/machine_images.rb +1 -1
  63. data/modules/mu/cloud/providers.rb +6 -1
  64. data/modules/mu/cloud/resource_base.rb +16 -7
  65. data/modules/mu/cloud/ssh_sessions.rb +5 -1
  66. data/modules/mu/cloud/wrappers.rb +20 -7
  67. data/modules/mu/config.rb +28 -12
  68. data/modules/mu/config/bucket.rb +31 -2
  69. data/modules/mu/config/cache_cluster.rb +1 -1
  70. data/modules/mu/config/cdn.rb +100 -0
  71. data/modules/mu/config/container_cluster.rb +1 -1
  72. data/modules/mu/config/database.rb +3 -3
  73. data/modules/mu/config/dnszone.rb +4 -3
  74. data/modules/mu/config/endpoint.rb +1 -0
  75. data/modules/mu/config/firewall_rule.rb +1 -1
  76. data/modules/mu/config/function.rb +16 -7
  77. data/modules/mu/config/job.rb +89 -0
  78. data/modules/mu/config/notifier.rb +7 -18
  79. data/modules/mu/config/ref.rb +55 -9
  80. data/modules/mu/config/schema_helpers.rb +12 -3
  81. data/modules/mu/config/server.rb +11 -5
  82. data/modules/mu/config/server_pool.rb +2 -2
  83. data/modules/mu/config/vpc.rb +11 -10
  84. data/modules/mu/defaults/AWS.yaml +106 -106
  85. data/modules/mu/deploy.rb +40 -14
  86. data/modules/mu/groomers/chef.rb +2 -2
  87. data/modules/mu/master.rb +70 -3
  88. data/modules/mu/mommacat.rb +28 -9
  89. data/modules/mu/mommacat/daemon.rb +13 -7
  90. data/modules/mu/mommacat/naming.rb +2 -2
  91. data/modules/mu/mommacat/search.rb +16 -5
  92. data/modules/mu/mommacat/storage.rb +67 -32
  93. data/modules/mu/providers/aws.rb +298 -85
  94. data/modules/mu/providers/aws/alarm.rb +5 -5
  95. data/modules/mu/providers/aws/bucket.rb +284 -50
  96. data/modules/mu/providers/aws/cache_cluster.rb +26 -26
  97. data/modules/mu/providers/aws/cdn.rb +782 -0
  98. data/modules/mu/providers/aws/collection.rb +16 -16
  99. data/modules/mu/providers/aws/container_cluster.rb +84 -64
  100. data/modules/mu/providers/aws/database.rb +59 -55
  101. data/modules/mu/providers/aws/dnszone.rb +29 -12
  102. data/modules/mu/providers/aws/endpoint.rb +535 -50
  103. data/modules/mu/providers/aws/firewall_rule.rb +32 -26
  104. data/modules/mu/providers/aws/folder.rb +1 -1
  105. data/modules/mu/providers/aws/function.rb +300 -134
  106. data/modules/mu/providers/aws/group.rb +16 -14
  107. data/modules/mu/providers/aws/habitat.rb +4 -4
  108. data/modules/mu/providers/aws/job.rb +469 -0
  109. data/modules/mu/providers/aws/loadbalancer.rb +67 -45
  110. data/modules/mu/providers/aws/log.rb +17 -17
  111. data/modules/mu/providers/aws/msg_queue.rb +22 -13
  112. data/modules/mu/providers/aws/nosqldb.rb +99 -8
  113. data/modules/mu/providers/aws/notifier.rb +137 -65
  114. data/modules/mu/providers/aws/role.rb +119 -83
  115. data/modules/mu/providers/aws/search_domain.rb +166 -30
  116. data/modules/mu/providers/aws/server.rb +209 -118
  117. data/modules/mu/providers/aws/server_pool.rb +95 -130
  118. data/modules/mu/providers/aws/storage_pool.rb +19 -11
  119. data/modules/mu/providers/aws/user.rb +5 -5
  120. data/modules/mu/providers/aws/userdata/linux.erb +5 -4
  121. data/modules/mu/providers/aws/vpc.rb +109 -54
  122. data/modules/mu/providers/aws/vpc_subnet.rb +43 -39
  123. data/modules/mu/providers/azure.rb +78 -12
  124. data/modules/mu/providers/azure/server.rb +20 -4
  125. data/modules/mu/providers/cloudformation/server.rb +1 -1
  126. data/modules/mu/providers/google.rb +21 -5
  127. data/modules/mu/providers/google/bucket.rb +1 -1
  128. data/modules/mu/providers/google/container_cluster.rb +1 -1
  129. data/modules/mu/providers/google/database.rb +1 -1
  130. data/modules/mu/providers/google/firewall_rule.rb +1 -1
  131. data/modules/mu/providers/google/folder.rb +7 -3
  132. data/modules/mu/providers/google/function.rb +66 -31
  133. data/modules/mu/providers/google/group.rb +1 -1
  134. data/modules/mu/providers/google/habitat.rb +1 -1
  135. data/modules/mu/providers/google/loadbalancer.rb +1 -1
  136. data/modules/mu/providers/google/role.rb +6 -3
  137. data/modules/mu/providers/google/server.rb +1 -1
  138. data/modules/mu/providers/google/server_pool.rb +1 -1
  139. data/modules/mu/providers/google/user.rb +1 -1
  140. data/modules/mu/providers/google/vpc.rb +28 -3
  141. data/modules/tests/aws-jobs-functions.yaml +46 -0
  142. data/modules/tests/aws-servers-with-handrolled-iam.yaml +37 -0
  143. data/modules/tests/centos6.yaml +4 -0
  144. data/modules/tests/centos7.yaml +4 -0
  145. data/modules/tests/ecs.yaml +2 -2
  146. data/modules/tests/eks.yaml +1 -1
  147. data/modules/tests/functions/node-function/lambda_function.js +10 -0
  148. data/modules/tests/functions/python-function/lambda_function.py +12 -0
  149. data/modules/tests/k8s.yaml +1 -1
  150. data/modules/tests/microservice_app.yaml +288 -0
  151. data/modules/tests/rds.yaml +5 -5
  152. data/modules/tests/regrooms/rds.yaml +5 -5
  153. data/modules/tests/server-with-scrub-muisms.yaml +1 -1
  154. data/modules/tests/super_complex_bok.yml +2 -2
  155. data/modules/tests/super_simple_bok.yml +2 -2
  156. metadata +42 -17
@@ -36,7 +36,7 @@ module MU
36
36
  @dependencies[dimension["depclass"]][dimension["name"]].cloudobj.cloud_id
37
37
  end
38
38
  elsif dimension["mu_name"] and dimension["deploy_id"]
39
- found = MU::MommaCat.findStray("AWS", deps_class, deploy_id: dimension["deploy_id"], mu_name: dimension["mu_name"], region: @config["region"])
39
+ found = MU::MommaCat.findStray("AWS", deps_class, deploy_id: dimension["deploy_id"], mu_name: dimension["mu_name"], region: @region)
40
40
  raise MuError, "Couldn't find #{deps_class} #{dimension["mu_name"]}" if found.nil? || found.empty?
41
41
  resp = found.first.deploydata["cloud_id"]
42
42
  resp.downcase if %w{database cache_cluster}.include?(deps_class)
@@ -79,8 +79,8 @@ module MU
79
79
  evaluation_periods: @config["evaluation_periods"],
80
80
  threshold: @config["threshold"],
81
81
  comparison_operator: @config["comparison_operator"],
82
- region: @config["region"],
83
- credentials: @config['credentials']
82
+ region: @region,
83
+ credentials: @credentials
84
84
  )
85
85
 
86
86
  @cloud_id = @mu_name
@@ -124,7 +124,7 @@ 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
128
  MU.log "AWS::Alarm.cleanup: need to support flags['known']", MU::DEBUG, details: flags
129
129
  MU.log "Placeholder: AWS Alarm artifacts do not support tags, so ignoremaster cleanup flag has no effect", MU::DEBUG, details: ignoremaster
130
130
  alarms = []
@@ -132,7 +132,7 @@ module MU
132
132
  # This can miss alarms in some cases (eg. cache_cluster) so we might want to delete alarms from each API as well.
133
133
  MU::Cloud::AWS.cloudwatch(credentials: credentials, region: region).describe_alarms.each { |page|
134
134
  page.metric_alarms.map(&:alarm_name).each { |alarm_name|
135
- alarms << alarm_name if alarm_name.match(MU.deploy_id)
135
+ alarms << alarm_name if alarm_name.match(deploy_id)
136
136
  }
137
137
  }
138
138
 
@@ -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,20 +39,20 @@ 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
- MU::Cloud::AWS.s3(credentials: @config['credentials'], region: @config['region']).create_bucket(
42
+ MU::Cloud::AWS.s3(credentials: @credentials, region: @region).create_bucket(
37
43
  acl: @config['acl'],
38
44
  bucket: bucket_name
39
45
  )
40
46
 
41
47
  @cloud_id = bucket_name
42
- is_live = MU::Cloud::AWS::Bucket.find(cloud_id: @cloud_id, region: @config['region'], credentials: @credentials).values.first
48
+ is_live = MU::Cloud::AWS::Bucket.find(cloud_id: @cloud_id, region: @region, credentials: @credentials).values.first
43
49
  begin
44
- is_live = MU::Cloud::AWS::Bucket.find(cloud_id: @cloud_id, region: @config['region'], credentials: @credentials).values.first
50
+ is_live = MU::Cloud::AWS::Bucket.find(cloud_id: @cloud_id, region: @region, credentials: @credentials).values.first
45
51
  sleep 3
46
52
  end while !is_live
47
53
 
48
54
  @@region_cache_semaphore.synchronize {
49
- @@region_cache[@cloud_id] ||= @config['region']
55
+ @@region_cache[@cloud_id] ||= @region
50
56
  }
51
57
 
52
58
  tagBucket if !@config['scrub_mu_isms']
@@ -72,7 +78,7 @@ module MU
72
78
  }
73
79
  end
74
80
 
75
- MU::Cloud::AWS.s3(credentials: @config['credentials'], region: @config['region']).put_bucket_tagging(
81
+ MU::Cloud::AWS.s3(credentials: @credentials, region: @region).put_bucket_tagging(
76
82
  bucket: @cloud_id,
77
83
  tagging: {
78
84
  tag_set: tagset
@@ -81,35 +87,90 @@ 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
 
87
122
  @@region_cache_semaphore.synchronize {
88
- @@region_cache[@cloud_id] ||= @config['region']
123
+ @@region_cache[@cloud_id] ||= @region
89
124
  }
90
125
  tagBucket if !@config['scrub_mu_isms']
91
126
 
92
127
  current = cloud_desc
93
- if @config['policies']
94
- @config['policies'].each { |pol|
95
- pol['grant_to'] ||= [
96
- { "id" => "*" }
97
- ]
98
- }
128
+ applyPolicies if @config['policies']
129
+
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: @credentials, region: @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: @credentials, region: @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
99
164
 
100
- policy_docs = MU::Cloud.resourceClass("AWS", "Role").genPolicyDocument(@config['policies'], deploy_obj: @deploy, bucket_style: true)
101
- policy_docs.each { |doc|
102
- MU.log "Applying S3 bucket policy #{doc.keys.first} to bucket #{@cloud_id}", MU::NOTICE, details: JSON.pretty_generate(doc.values.first)
103
- MU::Cloud::AWS.s3(credentials: @config['credentials'], region: @config['region']).put_bucket_policy(
104
- bucket: @cloud_id,
105
- policy: JSON.generate(doc.values.first)
106
- )
165
+ Hash[upload_me].each_pair { |file, url|
166
+ self.class.upload(url, file: file, credentials: @credentials, region: @region, acl: batch['acl'])
167
+ }
107
168
  }
108
169
  end
109
170
 
110
171
  if @config['web'] and current["website"].nil?
111
172
  MU.log "Enabling web service on S3 bucket #{@cloud_id}", MU::NOTICE
112
- MU::Cloud::AWS.s3(credentials: @config['credentials'], region: @config['region']).put_bucket_website(
173
+ MU::Cloud::AWS.s3(credentials: @credentials, region: @region).put_bucket_website(
113
174
  bucket: @cloud_id,
114
175
  website_configuration: {
115
176
  error_document: {
@@ -120,32 +181,62 @@ module MU
120
181
  }
121
182
  }
122
183
  )
184
+ ['web_error_object', 'web_index_object'].each { |key|
185
+ begin
186
+ MU::Cloud::AWS.s3(credentials: @credentials, region: @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: @credentials, region: @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
123
201
  elsif !@config['web'] and !current["website"].nil?
124
202
  MU.log "Disabling web service on S3 bucket #{@cloud_id}", MU::NOTICE
125
- MU::Cloud::AWS.s3(credentials: @config['credentials'], region: @config['region']).delete_bucket_website(
203
+ MU::Cloud::AWS.s3(credentials: @credentials, region: @region).delete_bucket_website(
126
204
  bucket: @cloud_id
127
205
  )
128
206
  end
129
207
 
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"
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)
137
213
  }
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(
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: @credentials, region: @region).put_bucket_cors(
142
229
  bucket: @cloud_id,
143
- versioning_configuration: {
144
- mfa_delete: "Disabled",
145
- status: "Suspended"
230
+ cors_configuration: {
231
+ cors_rules: symbolify_keys.call(@config['cors'])
146
232
  }
147
233
  )
148
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-#{@region}.amazonaws.com/", MU::SUMMARY
239
+ end
149
240
  end
150
241
 
151
242
  # Upload a file to a bucket.
@@ -160,7 +251,7 @@ module MU
160
251
 
161
252
  if file and !file.empty?
162
253
  if !File.exist?(file) or !File.readable?(file)
163
- 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}"
164
255
  else
165
256
  data = File.read(file)
166
257
  end
@@ -177,12 +268,19 @@ module MU
177
268
 
178
269
  begin
179
270
  MU.log "Writing #{path} to S3 bucket #{bucket}"
180
- MU::Cloud::AWS.s3(region: region, credentials: credentials).put_object(
271
+ params = {
181
272
  acl: acl,
182
273
  bucket: bucket,
183
274
  key: path,
184
275
  body: data
185
- )
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)
186
284
  rescue Aws::S3::Errors => e
187
285
  raise MuError, "Got #{e.inspect} trying to write #{path} to #{bucket} (region: #{region}, credentials: #{credentials})"
188
286
  end
@@ -207,7 +305,7 @@ module MU
207
305
  # @param ignoremaster [Boolean]: If true, will remove resources not flagged as originating from this Mu server
208
306
  # @param region [String]: The cloud provider region
209
307
  # @return [void]
210
- 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: {})
211
309
  MU.log "AWS::Bucket.cleanup: need to support flags['known']", MU::DEBUG, details: flags
212
310
 
213
311
  resp = MU::Cloud::AWS.s3(credentials: credentials, region: region).list_buckets
@@ -242,7 +340,7 @@ module MU
242
340
  deploy_match = false
243
341
  master_match = false
244
342
  tags.each { |tag|
245
- if tag.key == "MU-ID" and tag.value == MU.deploy_id
343
+ if tag.key == "MU-ID" and tag.value == deploy_id
246
344
  deploy_match = true
247
345
  elsif tag.key == "MU-MASTER-IP" and tag.value == MU.mu_public_ip
248
346
  master_match = true
@@ -254,6 +352,21 @@ module MU
254
352
  MU::Cloud::AWS.s3(credentials: credentials, region: region).delete_bucket(bucket: bucket.name)
255
353
  end
256
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
257
370
  rescue Aws::S3::Errors::NoSuchTagSet, Aws::S3::Errors::PermanentRedirect
258
371
  next
259
372
  end
@@ -264,13 +377,13 @@ module MU
264
377
  # Canonical Amazon Resource Number for this resource
265
378
  # @return [String]
266
379
  def arn
267
- "arn:"+(MU::Cloud::AWS.isGovCloud?(@config["region"]) ? "aws-us-gov" : "aws")+":s3:::"+@cloud_id
380
+ "arn:"+(MU::Cloud::AWS.isGovCloud?(@region) ? "aws-us-gov" : "aws")+":s3:::"+@cloud_id
268
381
  end
269
382
 
270
383
  # Return the metadata for this user cofiguration
271
384
  # @return [Hash]
272
385
  def notify
273
- desc = MU::Cloud::AWS::Bucket.describe_bucket(@cloud_id, credentials: @config['credentials'], region: @config['region'])
386
+ desc = MU::Cloud::AWS::Bucket.describe_bucket(@cloud_id, credentials: @credentials, region: @region)
274
387
  MU.structToHash(desc)
275
388
  end
276
389
 
@@ -279,9 +392,32 @@ module MU
279
392
  def self.find(**args)
280
393
  found = {}
281
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
+
282
415
  if args[:cloud_id]
283
416
  begin
284
- 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]
285
421
  rescue ::Aws::S3::Errors::NoSuchBucket
286
422
  end
287
423
  else
@@ -289,11 +425,14 @@ module MU
289
425
  if resp and resp.buckets
290
426
  resp.buckets.each { |b|
291
427
  begin
292
- loc_resp = MU::Cloud::AWS.s3(credentials: args[:credentials], region: args[:region]).get_bucket_location(bucket: b.name)
293
- 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]
294
430
  next
295
431
  end
296
- 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
297
436
  rescue Aws::S3::Errors::AccessDenied
298
437
  end
299
438
  }
@@ -303,6 +442,28 @@ module MU
303
442
  found
304
443
  end
305
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" => @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
+
306
467
  # Cloud-specific configuration properties.
307
468
  # @param _config [MU::Config]: The calling MU::Config object
308
469
  # @return [Array<Array,Hash>]: List of required fields, and json-schema Hash of cloud-specific configuration parameters for this resource
@@ -310,15 +471,67 @@ module MU
310
471
  toplevel_required = []
311
472
  schema = {
312
473
  "policies" => MU::Cloud.resourceClass("AWS", "Role").condition_schema,
313
- "acl" => {
314
- "type" => "string",
315
- "enum" => ["private", "public-read", "public-read-write", "authenticated-read"],
316
- "default" => "private"
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
+ }
317
484
  },
318
485
  "storage_class" => {
319
486
  "type" => "string",
320
487
  "enum" => ["STANDARD", "REDUCED_REDUNDANCY", "STANDARD_IA", "ONEZONE_IA", "INTELLIGENT_TIERING", "GLACIER"],
321
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
+ }
322
535
  }
323
536
  }
324
537
  [toplevel_required, schema]
@@ -384,6 +597,27 @@ module MU
384
597
  desc
385
598
  end
386
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: @credentials, region: @region).put_bucket_policy(
615
+ bucket: @cloud_id,
616
+ policy: JSON.generate(doc.values.first)
617
+ )
618
+ }
619
+ end
620
+
387
621
  end
388
622
  end
389
623
  end