firespring_dev_commands 1.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 (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