firespring_dev_commands 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +83 -0
  4. data/lib/firespring_dev_commands/audit/report/item.rb +33 -0
  5. data/lib/firespring_dev_commands/audit/report/levels.rb +36 -0
  6. data/lib/firespring_dev_commands/audit/report.rb +49 -0
  7. data/lib/firespring_dev_commands/aws/account/info.rb +15 -0
  8. data/lib/firespring_dev_commands/aws/account.rb +164 -0
  9. data/lib/firespring_dev_commands/aws/cloudformation/parameters.rb +26 -0
  10. data/lib/firespring_dev_commands/aws/cloudformation.rb +188 -0
  11. data/lib/firespring_dev_commands/aws/codepipeline.rb +96 -0
  12. data/lib/firespring_dev_commands/aws/credentials.rb +136 -0
  13. data/lib/firespring_dev_commands/aws/login.rb +131 -0
  14. data/lib/firespring_dev_commands/aws/parameter.rb +32 -0
  15. data/lib/firespring_dev_commands/aws/profile.rb +55 -0
  16. data/lib/firespring_dev_commands/aws/s3.rb +42 -0
  17. data/lib/firespring_dev_commands/aws.rb +10 -0
  18. data/lib/firespring_dev_commands/boolean.rb +7 -0
  19. data/lib/firespring_dev_commands/common.rb +112 -0
  20. data/lib/firespring_dev_commands/daterange.rb +171 -0
  21. data/lib/firespring_dev_commands/docker/compose.rb +271 -0
  22. data/lib/firespring_dev_commands/docker/status.rb +38 -0
  23. data/lib/firespring_dev_commands/docker.rb +276 -0
  24. data/lib/firespring_dev_commands/dotenv.rb +6 -0
  25. data/lib/firespring_dev_commands/env.rb +38 -0
  26. data/lib/firespring_dev_commands/eol/product_version.rb +86 -0
  27. data/lib/firespring_dev_commands/eol.rb +58 -0
  28. data/lib/firespring_dev_commands/git/info.rb +13 -0
  29. data/lib/firespring_dev_commands/git.rb +420 -0
  30. data/lib/firespring_dev_commands/jira/issue.rb +33 -0
  31. data/lib/firespring_dev_commands/jira/project.rb +13 -0
  32. data/lib/firespring_dev_commands/jira/user/type.rb +20 -0
  33. data/lib/firespring_dev_commands/jira/user.rb +31 -0
  34. data/lib/firespring_dev_commands/jira.rb +78 -0
  35. data/lib/firespring_dev_commands/logger.rb +8 -0
  36. data/lib/firespring_dev_commands/node/audit.rb +39 -0
  37. data/lib/firespring_dev_commands/node.rb +107 -0
  38. data/lib/firespring_dev_commands/php/audit.rb +71 -0
  39. data/lib/firespring_dev_commands/php.rb +109 -0
  40. data/lib/firespring_dev_commands/rake.rb +24 -0
  41. data/lib/firespring_dev_commands/ruby/audit.rb +30 -0
  42. data/lib/firespring_dev_commands/ruby.rb +113 -0
  43. data/lib/firespring_dev_commands/second.rb +22 -0
  44. data/lib/firespring_dev_commands/tar/pax_header.rb +49 -0
  45. data/lib/firespring_dev_commands/tar/type_flag.rb +49 -0
  46. data/lib/firespring_dev_commands/tar.rb +149 -0
  47. data/lib/firespring_dev_commands/templates/aws.rb +84 -0
  48. data/lib/firespring_dev_commands/templates/base_interface.rb +54 -0
  49. data/lib/firespring_dev_commands/templates/ci.rb +138 -0
  50. data/lib/firespring_dev_commands/templates/docker/application.rb +177 -0
  51. data/lib/firespring_dev_commands/templates/docker/default.rb +200 -0
  52. data/lib/firespring_dev_commands/templates/docker/node/application.rb +145 -0
  53. data/lib/firespring_dev_commands/templates/docker/php/application.rb +190 -0
  54. data/lib/firespring_dev_commands/templates/docker/ruby/application.rb +146 -0
  55. data/lib/firespring_dev_commands/templates/eol.rb +23 -0
  56. data/lib/firespring_dev_commands/templates/git.rb +147 -0
  57. data/lib/firespring_dev_commands/version.rb +11 -0
  58. data/lib/firespring_dev_commands.rb +21 -0
  59. metadata +436 -0
@@ -0,0 +1,276 @@
1
+ require 'docker'
2
+ require 'json'
3
+
4
+ # TODO: Make these configurable?
5
+ # Set docker write timeout to 1 hour
6
+ Excon.defaults[:write_timeout] = 3600
7
+
8
+ # Set docker read timeout to 1 hour
9
+ Excon.defaults[:read_timeout] = 3600
10
+
11
+ module Dev
12
+ # Class contains many useful methods for interfacing with the docker api
13
+ class Docker
14
+ # Config object for setting top level docker config options
15
+ Config = Struct.new(:min_version, :max_version) do
16
+ def initialize
17
+ self.min_version = nil
18
+ self.max_version = nil
19
+ end
20
+ end
21
+
22
+ class << self
23
+ # Instantiates a new top level config object if one hasn't already been created
24
+ # Yields that config object to any given block
25
+ # Returns the resulting config object
26
+ def config
27
+ @config ||= Config.new
28
+ yield(@config) if block_given?
29
+ @config
30
+ end
31
+
32
+ # Alias the config method to configure for a slightly clearer access syntax
33
+ alias_method :configure, :config
34
+
35
+ # Returns the version of the docker engine running on the system
36
+ def version
37
+ @version ||= JSON.parse(::Docker.connection.get('/version'))['Version']
38
+ end
39
+ end
40
+
41
+ def initialize
42
+ check_version
43
+ end
44
+
45
+ # Checks the min and max version against the current docker version if they have been configured
46
+ def check_version
47
+ min_version = self.class.config.min_version
48
+ raise "requires docker version >= #{min_version} (found #{self.class.version})" if min_version && !Dev::Common.new.version_greater_than(min_version, self.class.version)
49
+
50
+ max_version = self.class.config.max_version
51
+ raise "requires docker version < #{max_version} (found #{self.class.version})" if max_version && Dev::Common.new.version_greater_than(max_version, self.class.version)
52
+ end
53
+
54
+ # Prunes/removes all unused containers, networks, volumes, and images
55
+ def prune
56
+ prune_containers
57
+ prune_networks
58
+ prune_volumes
59
+ prune_images
60
+ end
61
+
62
+ # Prunes/removes all unused containers
63
+ def prune_containers
64
+ _prune('containers')
65
+ end
66
+
67
+ # Prunes/removes all unused networks
68
+ def prune_networks
69
+ _prune('networks')
70
+ end
71
+
72
+ # Prunes/removes all unused volumes
73
+ def prune_volumes
74
+ _prune('volumes')
75
+ end
76
+
77
+ # Prunes/removes all unused images
78
+ def prune_images
79
+ _prune('images')
80
+ end
81
+
82
+ # Private method which actually calls the prune endpoint on the docker api connection
83
+ private def _prune(type)
84
+ response = ::Docker.connection.post("/#{type.downcase}/prune", {})
85
+ format_prune(type, response)
86
+ rescue ::Docker::Error::ServerError => e
87
+ # Specifically check for 'prune already running' error and retry if found
88
+ if /already running/.match?(e.to_s)
89
+ sleep 2
90
+ retry
91
+ end
92
+ raise
93
+ end
94
+
95
+ # Callback method which formats the output from the prune method to give details on what was cleaned and how much space was reclaimed
96
+ private def format_prune(type, response)
97
+ info = JSON.parse(response)
98
+ type = type.capitalize
99
+
100
+ LOG.info "\nDeleted #{type.capitalize}"
101
+ deleted_items = info["#{type}Deleted"] || []
102
+ deleted_items.each { |it| LOG.info " #{it}" }
103
+ LOG.info "Total reclaimed space: #{filesize(info['SpaceReclaimed'])}"
104
+ end
105
+
106
+ # Print the given filesize using the most appropriate units
107
+ private def filesize(size)
108
+ return '0.0 B' if size.to_i.zero?
109
+
110
+ units = %w(B KB MB GB TB Pb EB)
111
+ exp = (Math.log(size) / Math.log(1024)).to_i
112
+ exp = 6 if exp > 6
113
+
114
+ format('%.1f %s', size.to_f / (1024**exp), units[exp])
115
+ end
116
+
117
+ # Remove docker images with the "force" option set to true
118
+ # This will remove the images even if they are currently in use and cause unintended side effects.
119
+ def force_remove_images(name_and_tag)
120
+ images = ::Docker::Image.all(filter: name_and_tag)
121
+ ::Docker::Image.remove(images[0].id, force: true) unless images.empty?
122
+ end
123
+
124
+ # Calls the docker compose method with the given inputs
125
+ # @deprecated Please use {Docker::Compose#container_by_name} instead
126
+ def container_by_name(service_name, prefix = nil, status: [Docker::Status::RUNNING])
127
+ warn '[DEPRECATION] `Docker#container_by_name` is deprecated. Please use `Docker::Compose#container_by_name` instead.'
128
+ Docker::Compose.new.container_by_name(service_name, prefix, status)
129
+ end
130
+
131
+ # Calls the docker compose method with the given inputs
132
+ # @deprecated Please use {Docker::Compose#mapped_public_port} instead
133
+ def mapped_public_port(name, private_port)
134
+ warn '[DEPRECATION] `Docker#mapped_public_port` is deprecated. Please use `Docker::Compose#mapped_public_port` instead.'
135
+ Docker::Compose.new.mapped_public_port(name, private_port)
136
+ end
137
+
138
+ # Copies the source path on your local machine to the destination path on the container
139
+ def copy_to_container(container, source_path, dest_path)
140
+ LOG.info "Copying #{source_path} to #{dest_path}... "
141
+
142
+ container.archive_in(source_path, dest_path, overwrite: true)
143
+ return unless File.directory?(source_path)
144
+
145
+ dest_file = File.basename(source_path)
146
+ # TODO: Can we find a better solution for this? Seems pretty brittle
147
+ retcode = container.exec(['bash', '-c', "cd #{dest_path}; tar -xf #{dest_file}; rm -f #{dest_file}"])[-1]
148
+ raise 'Unable to unpack on container' unless retcode.zero?
149
+ end
150
+
151
+ # Copies the source path on the container to the destination path on your local machine
152
+ # If required is set to true, the command will fail if the source path does not exist on the container
153
+ def copy_from_container(container, source_path, dest_path, required: true)
154
+ LOG.info "Copying #{source_path} to #{dest_path}... "
155
+
156
+ tar = StringIO.new
157
+ begin
158
+ container.archive_out(source_path) do |chunk|
159
+ tar.write(chunk)
160
+ end
161
+ rescue => e
162
+ raise e if required
163
+
164
+ puts 'Not Found'
165
+ end
166
+
167
+ Dev::Tar.new(tar).unpack(source_path, dest_path)
168
+ end
169
+
170
+ # Display a nicely formatted table of images and their associated information
171
+ def print_images
172
+ reposize = 70
173
+ tagsize = 79
174
+ imagesize = 15
175
+ createsize = 20
176
+ sizesize = 10
177
+ total = reposize + tagsize + imagesize + createsize + sizesize
178
+
179
+ # If there is additional width available, add it to the repo and tag columns
180
+ additional = [((Rake.application.terminal_width - total) / 2).floor, 0].max
181
+ reposize += additional
182
+ tagsize += additional
183
+
184
+ format = "%-#{reposize}s%-#{tagsize}s%-#{imagesize}s%-#{createsize}s%s"
185
+ puts format(format, :REPOSITORY, :TAG, :'IMAGE ID', :CREATED, :SIZE)
186
+ ::Docker::Image.all.each do |image|
187
+ image_info(image).each do |repo, tag, id, created, size|
188
+ puts format(format, repo, tag, id, created, size)
189
+ end
190
+ end
191
+ end
192
+
193
+ # rubocop:disable Metrics/CyclomaticComplexity
194
+ # Take the given image and grab all of the parts of it which will be used to display the image info
195
+ private def image_info(image)
196
+ [].tap do |ary|
197
+ id = image.info&.dig('id')&.split(':')&.last&.slice(0..11)
198
+ created = timesince(Time.at(image.info&.dig('Created')))
199
+ size = filesize(image.info&.dig('Size'))
200
+
201
+ repo_urls = image.info&.dig('RepoTags')
202
+ repo_urls ||= ["#{image.info&.dig('RepoDigests')&.first&.split(':')&.first&.split('@')&.first}:<none>"]
203
+ repo_urls.each do |repo_url|
204
+ repo, tag = repo_url.split(':')
205
+ tag ||= '<none>'
206
+ ary << [repo, tag, id, created, size]
207
+ end
208
+ end
209
+ end
210
+ # rubocop:enable Metrics/CyclomaticComplexity
211
+
212
+ # Display a nicely formatted table of containers and their associated information
213
+ def print_containers
214
+ idsize = 15
215
+ imagesize = 25
216
+ commandsize = 25
217
+ createsize = 17
218
+ statussize = 16
219
+ portsize = 25
220
+ namesize = 10
221
+ total = idsize + imagesize + commandsize + createsize + statussize + portsize + namesize
222
+
223
+ # If there is additional width available, add it to the repo and tag columns
224
+ additional = [((Rake.application.terminal_width - total) / 2).floor, 0].max
225
+ imagesize += additional
226
+ portsize += additional
227
+
228
+ format = "%-#{idsize}s%-#{imagesize}s%-#{commandsize}s%-#{createsize}s%-#{statussize}s%-#{portsize}s%s"
229
+ puts format(format, :'CONTAINER ID', :IMAGE, :COMMAND, :CREATED, :STATUS, :PORTS, :NAMES)
230
+ ::Docker::Container.all.each do |container|
231
+ id, image, command, created, status, ports, names = container_info(container)
232
+ puts format(format, id, image, command, created, status, ports, names)
233
+ end
234
+ end
235
+
236
+ # rubocop:disable Metrics/CyclomaticComplexity
237
+ # Take the given container and grab all of the parts of it which will be used to display the container info
238
+ private def container_info(container)
239
+ id = container.id&.slice(0..11)
240
+ image = container.info&.dig('Image')
241
+
242
+ command = container.info&.dig('Command')&.truncate(20)
243
+ created = timesince(Time.at(container.info&.dig('Created')))
244
+ status = container.info&.dig('Status')
245
+
246
+ ports = container.info&.dig('Ports')&.map do |port_info|
247
+ ip = port_info['IP']
248
+ private_port = port_info['PrivatePort']
249
+ public_port = port_info['PublicPort']
250
+ type = port_info['Type']
251
+ next "#{private_port}/#{type}" unless ip && public_port
252
+
253
+ "#{ip}:#{public_port}->#{private_port}/#{type}"
254
+ end&.join(', ')
255
+ names = container.info&.dig('Names')&.map { |name| name.split('/').last }&.join(', ')
256
+
257
+ [id, image, command, created, status, ports, names]
258
+ end
259
+ # rubocop:enable Metrics/CyclomaticComplexity
260
+
261
+ # Print the time since the given time in the most appropriate unit
262
+ private def timesince(time)
263
+ return '' unless time && time.is_a?(Time)
264
+
265
+ time_since = (Time.now - time).to_i
266
+ return "#{time_since} seconds ago" if time_since <= Dev::Second::PER_MINUTE
267
+ return "#{(time_since / Dev::Second::PER_MINUTE).ceil} minutes ago" if time_since <= Dev::Second::PER_HOUR
268
+ return "#{(time_since / Dev::Second::PER_HOUR).ceil} hours ago" if time_since <= Dev::Second::PER_DAY
269
+ return "#{(time_since / Dev::Second::PER_DAY).ceil} days ago" if time_since <= Dev::Second::PER_WEEK
270
+ return "#{(time_since / Dev::Second::PER_WEEK).ceil} weeks ago" if time_since <= Dev::Second::PER_MONTH
271
+ return "#{(time_since / Dev::Second::PER_MONTH).ceil} months ago" if time_since <= Dev::Second::PER_YEAR
272
+
273
+ "#{(time_since / Dev::Second::PER_YEAR).ceil} years ago"
274
+ end
275
+ end
276
+ end
@@ -0,0 +1,6 @@
1
+ # require 'dotenv'
2
+
3
+ ## Load the override and the env file
4
+ # DOT_ENV_OVERRIDE = '.env.override'.freeze
5
+ # Dotenv.load(DOT_ENV_OVERRIDE)
6
+ # Dotenv.load
@@ -0,0 +1,38 @@
1
+ module Dev
2
+ # Class instance represents a '.env' file and includes methods to read/write the given filename
3
+ class Env
4
+ require 'pathname'
5
+
6
+ attr_accessor :envfile, :data
7
+
8
+ def initialize(filename = '.env')
9
+ @envfile = Pathname.new(filename)
10
+ @data = {}
11
+ return unless File.exist?(@envfile)
12
+
13
+ File.readlines(@envfile).each do |line|
14
+ key, value = line.split('=')
15
+ @data[key.to_s.strip.to_sym] = value.to_s.strip
16
+ end
17
+ end
18
+
19
+ # Get the value of the key from the loaded env data
20
+ def get(key)
21
+ data[key.to_s.strip.to_sym]
22
+ end
23
+
24
+ # Set the value of the key in the loaded env data
25
+ def set(key, value)
26
+ data[key.to_s.strip.to_sym] = value.to_s.strip
27
+ end
28
+
29
+ # Write the current env data back to the original file
30
+ def write
31
+ File.open(envfile, 'w') do |file|
32
+ data.each do |key, value|
33
+ file.write("#{key}=#{value}\n")
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,86 @@
1
+ require 'date'
2
+
3
+ module Dev
4
+ class EndOfLife
5
+ # Class which tracks a specific product and provides methods for determining the end of life date
6
+ class ProductVersion
7
+ attr_accessor :name, :cycle, :eol, :description
8
+
9
+ def initialize(name, cycle, description = nil)
10
+ @name = name
11
+ @cycle = cycle
12
+ @eol_date = nil
13
+ @eol = nil
14
+ @description = description
15
+ end
16
+
17
+ # Print the status information for the product with additional coloring to show eol status
18
+ def print_status
19
+ puts to_s_colorize
20
+ end
21
+
22
+ # Returns whether this product version is currently EOL
23
+ def eol
24
+ populate_eol_info unless @eol
25
+ @eol
26
+ end
27
+
28
+ # Returns the date at which this product is EOL
29
+ def eol_date
30
+ populate_eol_info unless @eol_date
31
+ @eol_date
32
+ end
33
+
34
+ # Populates the eol and eol_date values
35
+ # If eol is a boolean then the eol_date will be set to nil
36
+ def populate_eol_info
37
+ detail = product_detail(name, cycle)
38
+ eol = detail['eol']
39
+ if eol.boolean?
40
+ @eol = eol
41
+ else
42
+ @eol_date = Date.parse(eol)
43
+ @eol = @eol_date < Date.today
44
+ end
45
+ end
46
+
47
+ # Returns the product details for the product and cycle based off the api response and any manually configured dates
48
+ def product_detail(product, cycle)
49
+ detail = {}
50
+
51
+ uri = URI.parse("#{END_OF_LIFE_API_URL}/#{product}/#{cycle}.json")
52
+ response = Net::HTTP.get_response(uri)
53
+ detail = JSON.parse(response.body) if response.is_a?(Net::HTTPSuccess)
54
+
55
+ # If EOL info is a boolean or missing from the current details, overwrite with the manual date (if present)
56
+ manual_date = Dev::EndOfLife.config.manual_dates["#{product}_#{cycle.tr('.', '_')}".to_sym]
57
+ detail['eol'] = manual_date if manual_date && (detail['eol'].boolean? || detail['eol'].nil?)
58
+
59
+ raise "unable to query eol detail for #{name}, #{cycle}" if detail.empty?
60
+
61
+ detail
62
+ end
63
+
64
+ # Returns a string representation of the product and its eol status
65
+ def to_s
66
+ message = " #{name} (#{cycle}) is EOL on #{eol_date || 'n/a'}"
67
+ message << " (#{(eol_date - Date.today).to_i} days)" if eol_date
68
+ format '%-60s %s', message, description
69
+ end
70
+
71
+ # Returns the string representation of the product with additional coloring
72
+ def to_s_colorize
73
+ return to_s.light_red if eol
74
+
75
+ if eol_date
76
+ return to_s.light_green if eol_date > (Date.today + 240)
77
+ return to_s.light_yellow if eol_date > (Date.today + 60)
78
+
79
+ return to_s.light_magenta
80
+ end
81
+
82
+ to_s.light_white
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,58 @@
1
+ module Dev
2
+ # Class that contains methods for checking product versions of all tracked projects
3
+ class EndOfLife
4
+ # The URL of the end of life project api
5
+ END_OF_LIFE_API_URL = 'https://endoflife.date/api'.freeze
6
+
7
+ # Config object for setting top level git config options
8
+ Config = Struct.new(:product_versions, :manual_dates) do
9
+ def initialize
10
+ self.product_versions = []
11
+ self.manual_dates = {}
12
+ end
13
+ end
14
+
15
+ class << self
16
+ # Instantiates a new top level config object if one hasn't already been created
17
+ # Yields that config object to any given block
18
+ # Returns the resulting config object
19
+ def config
20
+ @config ||= Config.new
21
+ yield(@config) if block_given?
22
+ @config
23
+ end
24
+
25
+ # Alias the config method to configure for a slightly clearer access syntax
26
+ alias_method :configure, :config
27
+ end
28
+
29
+ attr_accessor :url, :products, :product_versions
30
+
31
+ def initialize(product_versions: self.class.config.product_versions)
32
+ @product_versions = Array(product_versions)
33
+ raise 'product version must be of type Dev::EndOfLife::ProductVersions' unless @product_versions.all?(Dev::EndOfLife::ProductVersion)
34
+ end
35
+
36
+ # Returns all products supported by the EOL api
37
+ def products
38
+ unless @products
39
+ uri = URI.parse("#{END_OF_LIFE_API_URL}/all.json")
40
+ response = Net::HTTP.get_response(uri)
41
+ raise 'unable to query products' unless response.is_a?(Net::HTTPSuccess)
42
+
43
+ @products = JSON.parse(response.body)
44
+ end
45
+
46
+ @products
47
+ end
48
+
49
+ # Prints all of the product version statuses
50
+ # Raises an error if any products are EOL
51
+ def check
52
+ puts
53
+ product_versions.sort_by(&:name).each(&:print_status)
54
+ puts
55
+ raise 'found EOL versions' if product_versions.any?(&:eol)
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,13 @@
1
+ module Dev
2
+ class Git
3
+ # Class which contains information about the git repository
4
+ class Info
5
+ attr_accessor :name, :path
6
+
7
+ def initialize(name, path)
8
+ @name = name
9
+ @path = path
10
+ end
11
+ end
12
+ end
13
+ end