openstax_aws 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +19 -0
  3. data/.travis.yml +12 -0
  4. data/CHANGELOG.md +11 -0
  5. data/Gemfile +6 -0
  6. data/Gemfile.lock +120 -0
  7. data/LICENSE.txt +1 -0
  8. data/README.md +927 -0
  9. data/Rakefile +6 -0
  10. data/TODO.md +1 -0
  11. data/assets/secrets_sequence_diagram.png +0 -0
  12. data/bin/console +14 -0
  13. data/bin/create_development_environment +26 -0
  14. data/bin/get_latest_ubuntu_ami +31 -0
  15. data/bin/setup +8 -0
  16. data/bin/templates/aws_ruby_development.yml +221 -0
  17. data/examples/deployment.rb +90 -0
  18. data/ideas.md +15 -0
  19. data/lib/openstax/aws/auto_scaling_group.rb +28 -0
  20. data/lib/openstax/aws/auto_scaling_instance.rb +96 -0
  21. data/lib/openstax/aws/build_image_command_1.rb +53 -0
  22. data/lib/openstax/aws/change_set.rb +100 -0
  23. data/lib/openstax/aws/deployment_base.rb +372 -0
  24. data/lib/openstax/aws/distribution.rb +56 -0
  25. data/lib/openstax/aws/ec2_instance_data.rb +18 -0
  26. data/lib/openstax/aws/extensions.rb +19 -0
  27. data/lib/openstax/aws/git_helper.rb +18 -0
  28. data/lib/openstax/aws/image.rb +34 -0
  29. data/lib/openstax/aws/msk_cluster.rb +19 -0
  30. data/lib/openstax/aws/packer_1_2_5.rb +63 -0
  31. data/lib/openstax/aws/packer_1_4_1.rb +72 -0
  32. data/lib/openstax/aws/packer_factory.rb +25 -0
  33. data/lib/openstax/aws/rds_instance.rb +25 -0
  34. data/lib/openstax/aws/s3_text_file.rb +50 -0
  35. data/lib/openstax/aws/sam_stack.rb +85 -0
  36. data/lib/openstax/aws/secrets.rb +302 -0
  37. data/lib/openstax/aws/secrets_factory.rb +126 -0
  38. data/lib/openstax/aws/secrets_set.rb +21 -0
  39. data/lib/openstax/aws/secrets_specification.rb +68 -0
  40. data/lib/openstax/aws/stack.rb +465 -0
  41. data/lib/openstax/aws/stack_event.rb +28 -0
  42. data/lib/openstax/aws/stack_factory.rb +153 -0
  43. data/lib/openstax/aws/stack_parameters.rb +19 -0
  44. data/lib/openstax/aws/stack_status.rb +125 -0
  45. data/lib/openstax/aws/system.rb +21 -0
  46. data/lib/openstax/aws/tag.rb +31 -0
  47. data/lib/openstax/aws/template.rb +129 -0
  48. data/lib/openstax/aws/version.rb +5 -0
  49. data/lib/openstax/aws/wait_message.rb +20 -0
  50. data/lib/openstax_aws.rb +154 -0
  51. data/openstax_aws.gemspec +58 -0
  52. metadata +350 -0
@@ -0,0 +1,302 @@
1
+ module OpenStax::Aws
2
+ class Secrets
3
+
4
+ # We store "secrets" in the AWS Parameter store. Secrets can be truly secret
5
+ # values (e.g. keys) or just some configuration values.
6
+
7
+ GENERATED_WITH_PREFIX = "Generated with"
8
+
9
+ attr_reader :region, :dry_run, :namespace
10
+
11
+ def initialize(region:, dry_run: true, namespace:)
12
+ @region = region
13
+ @dry_run = dry_run
14
+ @namespace = namespace
15
+ @client = Aws::SSM::Client.new(region: region)
16
+ @substitutions = {}
17
+ end
18
+
19
+ # `create` and `update` take secrets specifications and secrets substitutions.
20
+ # if you just want to call `create` and `update` with no arguments, you can
21
+ # define the specifications and substitutions ahead of time with this method, e.g.
22
+ #
23
+ # my_secrets.define(specifications: my_specifications, substitutions: my_substitutions)
24
+ # ...
25
+ # my_secrets.create
26
+ #
27
+ # See the README for more discussion about specifications and substitutions.
28
+ #
29
+ def define(specifications:, substitutions: {})
30
+ @specifications = specifications
31
+ @substitutions = substitutions
32
+ end
33
+
34
+ def create(specifications: nil, substitutions: nil)
35
+ # Build all secrets first so we hit any errors before we send any to AWS
36
+ built_secrets = build_secrets(specifications: specifications, substitutions: substitutions)
37
+
38
+ OpenStax::Aws.logger.info("**** DRY RUN ****") if dry_run
39
+
40
+ OpenStax::Aws.logger.info("Creating the following secrets in the AWS parameter store: #{built_secrets}")
41
+
42
+ # Ship 'em
43
+ if !dry_run
44
+ built_secrets.each do |built_secret|
45
+ client.put_parameter(built_secret.merge(overwrite: true))
46
+ sleep(0.1)
47
+ end
48
+ end
49
+ end
50
+
51
+ def update(specifications: nil, substitutions: nil, force_update_these: [])
52
+ existing_secrets = data!
53
+ built_secrets = build_secrets(specifications: specifications, substitutions: substitutions)
54
+ changed_secrets = self.class.changed_secrets(existing_secrets, built_secrets)
55
+
56
+ force_update_these.each do |force_update_this|
57
+ built_secrets.select{|built_secret| built_secret[:name].match(force_update_this)}.each do |forced|
58
+ changed_secrets.push(forced)
59
+ end
60
+ end
61
+ changed_secrets.uniq!
62
+
63
+ OpenStax::Aws.logger.info("**** DRY RUN ****") if dry_run
64
+
65
+ if changed_secrets.empty?
66
+ OpenStax::Aws.logger.info("Secrets did not change")
67
+ return false
68
+ else
69
+ OpenStax::Aws.logger.info("Updating the following secrets in the AWS parameter store: #{changed_secrets}")
70
+
71
+ # Ship 'em
72
+ if !dry_run
73
+ changed_secrets.each do |changed_secret|
74
+ client.put_parameter(changed_secret.merge(overwrite: true))
75
+ end
76
+ end
77
+
78
+ return true
79
+ end
80
+ end
81
+
82
+ def self.changed_secrets(existing_secrets_hash, new_secrets_array)
83
+ existing_secrets_hash = existing_secrets_hash.with_indifferent_access
84
+ new_secrets_array = new_secrets_array.map(&:with_indifferent_access)
85
+
86
+ new_secrets_array.each_with_object([]) do |new_secret, array|
87
+
88
+ existing_secret = existing_secrets_hash[new_secret[:name]]
89
+
90
+ if existing_secret
91
+ # No need to update if the value is the same
92
+ next if existing_secret[:value] == new_secret[:value]
93
+
94
+ # Don't update if different values but generated from same specification
95
+ next if new_secret[:description].try(:starts_with?, GENERATED_WITH_PREFIX) &&
96
+ new_secret[:description] == existing_secret[:description]
97
+ end
98
+
99
+ array.push(new_secret)
100
+ end
101
+ end
102
+
103
+ def build_secrets(specifications:, substitutions:)
104
+ specifications ||= @specifications
105
+ substitutions ||= @substitutions
106
+
107
+ specifications = [specifications].flatten
108
+
109
+ raise "Cannot build secrets without a specification" if specifications.empty?
110
+
111
+ expanded_data = {}
112
+
113
+ # later specifications override earlier ones
114
+ specifications.reverse.each do |specification|
115
+ expanded_data.merge!(specification.expanded_data)
116
+ end
117
+
118
+ expanded_data.map do |secret_name, spec_value|
119
+ build_secret(secret_name, spec_value, substitutions)
120
+ end
121
+ end
122
+
123
+ def build_secret(secret_name, spec_value, substitutions)
124
+ secret = {
125
+ name: "#{key_prefix}/#{secret_name}"
126
+ }
127
+
128
+ case spec_value
129
+ when Array
130
+ processed_items = spec_value.map do |item|
131
+ process_individual_spec_value(item, substitutions)[:value]
132
+ end
133
+ secret[:value] = processed_items.join(",")
134
+ secret[:type] = "StringList"
135
+ else
136
+ spec_value = spec_value.to_s.strip
137
+ secret.merge!(process_individual_spec_value(spec_value, substitutions))
138
+ end
139
+
140
+ if (generated = secret.delete(:generated))
141
+ secret[:description] = "#{GENERATED_WITH_PREFIX} #{spec_value}"
142
+ end
143
+
144
+ secret
145
+ end
146
+
147
+ def process_individual_spec_value(spec_value, substitutions)
148
+ generated = false
149
+ type = "String"
150
+
151
+ value = case spec_value
152
+ when /^random\(hex,(\d+)\)$/
153
+ generated = true
154
+ num_characters = $1.to_i
155
+ SecureRandom.hex(num_characters)[0..num_characters-1]
156
+ when /^random\(base64,(\d+)\)$/
157
+ generated = true
158
+ num_characters = $1.to_i
159
+ SecureRandom.urlsafe_base64(num_characters)[0..num_characters-1]
160
+ when /^rsa\((\d+)\)$/
161
+ type = "SecureString"
162
+ generated = true
163
+ key_length = $1.to_i
164
+ OpenSSL::PKey::RSA.new(key_length).to_s
165
+ when "uuid"
166
+ generated = true
167
+ SecureRandom.uuid
168
+ when /{([^{}]+)}/
169
+ spec_value.gsub(/({{\W*(\w+)\W*}})/) do |match|
170
+ if (!substitutions.has_key?($2) && !substitutions.has_key?($2.to_sym))
171
+ raise "no substitution provided for #{$2}"
172
+ end
173
+
174
+ substitutions[$2] || substitutions[$2.to_sym]
175
+ end
176
+ when /^ssm\((.*)\)$/
177
+ begin
178
+ parameter_name = $1.starts_with?("/") ? $1 : (substitutions[$1] || substitutions[$1.to_sym])
179
+
180
+ if parameter_name.blank?
181
+ raise "#{$1} is neither a literal parameter name nor available in the given substitutions"
182
+ end
183
+
184
+ parameter = client.get_parameter({
185
+ name: parameter_name,
186
+ with_decryption: true
187
+ }).parameter
188
+
189
+ type = parameter.type
190
+
191
+ parameter.value
192
+ rescue Aws::SSM::Errors::ParameterNotFound => ee
193
+ raise "Could not get secret '#{$1}'"
194
+ end
195
+ else # use literal value
196
+ spec_value
197
+ end
198
+
199
+ {
200
+ value: value,
201
+ type: type,
202
+ generated: generated
203
+ }
204
+ end
205
+
206
+ def data
207
+ @data ||= data!
208
+ end
209
+
210
+ def data!
211
+ {}.tap do |hash|
212
+ client.get_parameters_by_path({
213
+ path: key_prefix,
214
+ recursive: true,
215
+ with_decryption: true
216
+ }).each do |response|
217
+ response.parameters.each do |parameter|
218
+ hash[parameter.name] = ReadOnlyParameter.new(parameter, client)
219
+ end
220
+ end
221
+ end
222
+ end
223
+
224
+ class ReadOnlyParameter
225
+ # Helper object to hide the fact that tags and descriptions have to be accessed
226
+ # through separate API calls
227
+
228
+ def initialize(parameter, client)
229
+ @raw_parameter = parameter
230
+ @client = client
231
+ end
232
+
233
+ def [](key)
234
+ case key
235
+ when :type
236
+ @raw_parameter.type
237
+ when :value
238
+ @raw_parameter.value
239
+ when :tags
240
+ raise "Not yet tested!"
241
+ @tags ||= begin
242
+ (@client.list_tags_for_resource({
243
+ resource_type: "Parameter",
244
+ resource_id: @raw_parameter.arn
245
+ }).tag_list || []).map do |tag| {
246
+ key: tag.key,
247
+ value: tag.value
248
+ } end
249
+ end
250
+ when :description
251
+ @description ||= begin
252
+ @client.describe_parameters({
253
+ parameter_filters: [{
254
+ key: "Name",
255
+ option: "Equals",
256
+ values: [@raw_parameter.name]
257
+ }],
258
+ max_results: 1
259
+ }).parameters[0].description
260
+ end
261
+ end
262
+ end
263
+ end
264
+
265
+ def get(local_name)
266
+ local_name = local_name.to_s
267
+ local_name = "/#{local_name}" unless local_name.chr == "/"
268
+ data["#{key_prefix}#{local_name}"].try(:[], :value)
269
+ end
270
+
271
+ def delete
272
+ secret_names = data!.keys
273
+ return if secret_names.empty?
274
+
275
+ OpenStax::Aws.logger.info("**** DRY RUN ****") if dry_run
276
+
277
+ OpenStax::Aws.logger.info("Deleting the following secrets in the AWS parameter store: #{secret_names}")
278
+
279
+ if !dry_run
280
+ @data = nil # remove cached values as they are about to get cleared remotely
281
+
282
+ # Can send max 10 secret names at a time
283
+ secret_names.each_slice(10) do |some_secret_names|
284
+ response = client.delete_parameters({names: some_secret_names})
285
+
286
+ if response.invalid_parameters.any?
287
+ OpenStax::Aws.logger.debug("Unable to delete some secrets (likely already deleted): #{response.invalid_parameters}")
288
+ end
289
+ end
290
+ end
291
+ end
292
+
293
+ def key_prefix
294
+ "/" + [namespace].flatten.reject(&:blank?).join("/")
295
+ end
296
+
297
+ protected
298
+
299
+ attr_reader :client
300
+
301
+ end
302
+ end
@@ -0,0 +1,126 @@
1
+ module OpenStax::Aws
2
+ class SecretsFactory
3
+ def initialize(region:, context:, namespace:, dry_run: true,
4
+ for_create_or_update:, shared_substitutions_block: nil)
5
+ raise ArgumentError, "context cannot be nil" if context.nil?
6
+ @context = context
7
+ @specification_blocks = []
8
+ @substitutions_block = nil
9
+ @region = region
10
+ @top_namespace = namespace
11
+ @next_namespace = nil
12
+ @dry_run = dry_run
13
+ @for_create_or_update = for_create_or_update
14
+ @shared_substitutions_block = shared_substitutions_block
15
+ end
16
+
17
+ def namespace(*args, &block)
18
+ @next_namespace = args.any? ? args[0] : @context.instance_eval(&block)
19
+ end
20
+
21
+ def specification(&block)
22
+ @specification_blocks.push(block)
23
+ end
24
+
25
+ def substitutions(&block)
26
+ @substitutions_block = block
27
+ end
28
+
29
+ def specification_instances
30
+ raise "Must define secrets specification" if @specification_blocks.empty?
31
+
32
+ @specification_blocks.map do |specification_block|
33
+ factory = SecretsSpecificationFactory.new(@context)
34
+ factory.instance_eval &specification_block
35
+ attributes = factory.attributes
36
+
37
+ if attributes.has_key?(:org_slash_repo)
38
+ OpenStax::Aws::SecretsSpecification.from_git(
39
+ org_slash_repo: attributes[:org_slash_repo],
40
+ sha: attributes[:sha],
41
+ path: attributes[:path],
42
+ format: attributes[:format].to_sym,
43
+ top_key: attributes[:top_key].to_sym,
44
+ preparser: attributes[:preparser]
45
+ )
46
+ elsif attributes.has_key?(:content)
47
+ OpenStax::Aws::SecretsSpecification.from_content(
48
+ content: attributes[:content],
49
+ format: attributes[:format],
50
+ top_key: attributes[:top_key],
51
+ preparser: attributes[:preparser]
52
+ )
53
+ else
54
+ raise "Cannot build a secrets specification"
55
+ end
56
+ end
57
+ end
58
+
59
+ def substitutions_hash
60
+ shared_substitutions = substitutions_hash_from_block(@shared_substitutions_block)
61
+ local_substitutions = substitutions_hash_from_block(@substitutions_block)
62
+
63
+ shared_substitutions.merge(local_substitutions)
64
+ end
65
+
66
+ def substitutions_hash_from_block(block)
67
+ return {} if block.nil?
68
+
69
+ factory = SecretsSubstitutionsFactory.new(@context)
70
+ factory.instance_eval &block
71
+ factory.attributes
72
+ end
73
+
74
+ def full_namespace
75
+ [@top_namespace, @next_namespace].flatten.reject(&:blank?)
76
+ end
77
+
78
+ def instance
79
+ Secrets.new(region: @region, namespace: full_namespace, dry_run: @dry_run).tap do |secrets|
80
+ if @for_create_or_update
81
+ secrets.define(specifications: specification_instances, substitutions: substitutions_hash)
82
+ end
83
+ end
84
+ end
85
+ end
86
+
87
+ class SecretsSpecificationFactory
88
+ attr_reader :attributes
89
+
90
+ def initialize(context)
91
+ raise ArgumentError, "context cannot be nil" if context.nil?
92
+ @context = context
93
+ @attributes = {}
94
+ end
95
+
96
+ def format(&block)
97
+ store_attribute(:format, &block)
98
+ end
99
+
100
+ def method_missing(name, *args, &block)
101
+ store_attribute(name, *args, &block)
102
+ end
103
+
104
+ def store_attribute(name, *args, &block)
105
+ raise "Secrets specification option `#{name}` cannot be called with arguments, only a block" if !args.empty?
106
+ raise "Secrets specification option `#{name}` must be called with a block to set the value" if !block_given?
107
+ attributes[name.to_sym] = @context.instance_eval(&block)
108
+ end
109
+ end
110
+
111
+ class SecretsSubstitutionsFactory
112
+ attr_reader :attributes
113
+
114
+ def initialize(context)
115
+ raise ArgumentError, "context cannot be nil" if context.nil?
116
+ @context = context
117
+ @attributes = {}
118
+ end
119
+
120
+ def method_missing(name, *args, &block)
121
+ raise "Secrets substitition `#{name}` cannot be called with arguments, only a block" if !args.empty?
122
+ raise "Secrets substitution `#{name}` must be called with a block to set the substitution value" if !block_given?
123
+ attributes[name.to_sym] = @context.instance_eval(&block)
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,21 @@
1
+ module OpenStax::Aws
2
+ class SecretsSet
3
+
4
+ def initialize(secrets_array)
5
+ @secrets_array = [secrets_array].flatten
6
+ end
7
+
8
+ def create
9
+ @secrets_array.each(&:create)
10
+ end
11
+
12
+ def update
13
+ @secrets_array.map(&:update).any?
14
+ end
15
+
16
+ def delete
17
+ @secrets_array.each(&:delete)
18
+ end
19
+
20
+ end
21
+ end