firespring_dev_commands 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +83 -0
- data/lib/firespring_dev_commands/audit/report/item.rb +33 -0
- data/lib/firespring_dev_commands/audit/report/levels.rb +36 -0
- data/lib/firespring_dev_commands/audit/report.rb +49 -0
- data/lib/firespring_dev_commands/aws/account/info.rb +15 -0
- data/lib/firespring_dev_commands/aws/account.rb +164 -0
- data/lib/firespring_dev_commands/aws/cloudformation/parameters.rb +26 -0
- data/lib/firespring_dev_commands/aws/cloudformation.rb +188 -0
- data/lib/firespring_dev_commands/aws/codepipeline.rb +96 -0
- data/lib/firespring_dev_commands/aws/credentials.rb +136 -0
- data/lib/firespring_dev_commands/aws/login.rb +131 -0
- data/lib/firespring_dev_commands/aws/parameter.rb +32 -0
- data/lib/firespring_dev_commands/aws/profile.rb +55 -0
- data/lib/firespring_dev_commands/aws/s3.rb +42 -0
- data/lib/firespring_dev_commands/aws.rb +10 -0
- data/lib/firespring_dev_commands/boolean.rb +7 -0
- data/lib/firespring_dev_commands/common.rb +112 -0
- data/lib/firespring_dev_commands/daterange.rb +171 -0
- data/lib/firespring_dev_commands/docker/compose.rb +271 -0
- data/lib/firespring_dev_commands/docker/status.rb +38 -0
- data/lib/firespring_dev_commands/docker.rb +276 -0
- data/lib/firespring_dev_commands/dotenv.rb +6 -0
- data/lib/firespring_dev_commands/env.rb +38 -0
- data/lib/firespring_dev_commands/eol/product_version.rb +86 -0
- data/lib/firespring_dev_commands/eol.rb +58 -0
- data/lib/firespring_dev_commands/git/info.rb +13 -0
- data/lib/firespring_dev_commands/git.rb +420 -0
- data/lib/firespring_dev_commands/jira/issue.rb +33 -0
- data/lib/firespring_dev_commands/jira/project.rb +13 -0
- data/lib/firespring_dev_commands/jira/user/type.rb +20 -0
- data/lib/firespring_dev_commands/jira/user.rb +31 -0
- data/lib/firespring_dev_commands/jira.rb +78 -0
- data/lib/firespring_dev_commands/logger.rb +8 -0
- data/lib/firespring_dev_commands/node/audit.rb +39 -0
- data/lib/firespring_dev_commands/node.rb +107 -0
- data/lib/firespring_dev_commands/php/audit.rb +71 -0
- data/lib/firespring_dev_commands/php.rb +109 -0
- data/lib/firespring_dev_commands/rake.rb +24 -0
- data/lib/firespring_dev_commands/ruby/audit.rb +30 -0
- data/lib/firespring_dev_commands/ruby.rb +113 -0
- data/lib/firespring_dev_commands/second.rb +22 -0
- data/lib/firespring_dev_commands/tar/pax_header.rb +49 -0
- data/lib/firespring_dev_commands/tar/type_flag.rb +49 -0
- data/lib/firespring_dev_commands/tar.rb +149 -0
- data/lib/firespring_dev_commands/templates/aws.rb +84 -0
- data/lib/firespring_dev_commands/templates/base_interface.rb +54 -0
- data/lib/firespring_dev_commands/templates/ci.rb +138 -0
- data/lib/firespring_dev_commands/templates/docker/application.rb +177 -0
- data/lib/firespring_dev_commands/templates/docker/default.rb +200 -0
- data/lib/firespring_dev_commands/templates/docker/node/application.rb +145 -0
- data/lib/firespring_dev_commands/templates/docker/php/application.rb +190 -0
- data/lib/firespring_dev_commands/templates/docker/ruby/application.rb +146 -0
- data/lib/firespring_dev_commands/templates/eol.rb +23 -0
- data/lib/firespring_dev_commands/templates/git.rb +147 -0
- data/lib/firespring_dev_commands/version.rb +11 -0
- data/lib/firespring_dev_commands.rb +21 -0
- 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,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
|