firespring_dev_commands 2.3.4 → 2.5.0.pre.alpha.1

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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/lib/firespring_dev_commands/audit/report.rb +2 -9
  4. data/lib/firespring_dev_commands/aws/account.rb +1 -1
  5. data/lib/firespring_dev_commands/aws/cloudformation.rb +3 -10
  6. data/lib/firespring_dev_commands/aws/login.rb +11 -37
  7. data/lib/firespring_dev_commands/common.rb +2 -22
  8. data/lib/firespring_dev_commands/docker/compose.rb +1 -2
  9. data/lib/firespring_dev_commands/docker/status.rb +0 -20
  10. data/lib/firespring_dev_commands/docker.rb +16 -86
  11. data/lib/firespring_dev_commands/eol/aws.rb +2 -10
  12. data/lib/firespring_dev_commands/eol.rb +2 -22
  13. data/lib/firespring_dev_commands/git.rb +29 -44
  14. data/lib/firespring_dev_commands/jira/issue.rb +1 -3
  15. data/lib/firespring_dev_commands/node.rb +1 -1
  16. data/lib/firespring_dev_commands/php/audit.rb +0 -4
  17. data/lib/firespring_dev_commands/php.rb +12 -28
  18. data/lib/firespring_dev_commands/platform.rb +31 -38
  19. data/lib/firespring_dev_commands/ruby.rb +3 -6
  20. data/lib/firespring_dev_commands/target_process/query.rb +4 -30
  21. data/lib/firespring_dev_commands/target_process/release.rb +1 -1
  22. data/lib/firespring_dev_commands/target_process/team_assignment.rb +1 -1
  23. data/lib/firespring_dev_commands/target_process/user.rb +1 -13
  24. data/lib/firespring_dev_commands/target_process/user_story.rb +1 -1
  25. data/lib/firespring_dev_commands/target_process/user_story_history.rb +1 -1
  26. data/lib/firespring_dev_commands/target_process.rb +7 -24
  27. data/lib/firespring_dev_commands/templates/aws.rb +6 -33
  28. data/lib/firespring_dev_commands/templates/base_interface.rb +2 -2
  29. data/lib/firespring_dev_commands/templates/ci.rb +11 -16
  30. data/lib/firespring_dev_commands/templates/docker/application.rb +2 -2
  31. data/lib/firespring_dev_commands/templates/docker/node/application.rb +5 -55
  32. data/lib/firespring_dev_commands/templates/docker/php/application.rb +16 -58
  33. data/lib/firespring_dev_commands/templates/docker/ruby/application.rb +5 -54
  34. data/lib/firespring_dev_commands/templates/eol.rb +2 -9
  35. data/lib/firespring_dev_commands/templates/git.rb +4 -171
  36. data/lib/firespring_dev_commands/version.rb +1 -1
  37. data/lib/firespring_dev_commands.rb +1 -1
  38. metadata +37 -113
  39. data/lib/firespring_dev_commands/aws/route53.rb +0 -139
  40. data/lib/firespring_dev_commands/bloom_growth/rock.rb +0 -34
  41. data/lib/firespring_dev_commands/bloom_growth/seat.rb +0 -16
  42. data/lib/firespring_dev_commands/bloom_growth/user.rb +0 -43
  43. data/lib/firespring_dev_commands/bloom_growth.rb +0 -132
  44. data/lib/firespring_dev_commands/certificate.rb +0 -59
  45. data/lib/firespring_dev_commands/coverage/base.rb +0 -16
  46. data/lib/firespring_dev_commands/coverage/cobertura.rb +0 -86
  47. data/lib/firespring_dev_commands/coverage/none.rb +0 -21
  48. data/lib/firespring_dev_commands/dns/resource.rb +0 -83
  49. data/lib/firespring_dev_commands/docker/desktop.rb +0 -61
  50. data/lib/firespring_dev_commands/eol/node.rb +0 -42
  51. data/lib/firespring_dev_commands/eol/php.rb +0 -50
  52. data/lib/firespring_dev_commands/eol/ruby.rb +0 -42
  53. data/lib/firespring_dev_commands/jira/parent.rb +0 -19
  54. data/lib/firespring_dev_commands/os.rb +0 -35
  55. data/lib/firespring_dev_commands/port.rb +0 -24
  56. data/lib/firespring_dev_commands/target_process/time.rb +0 -32
  57. data/lib/firespring_dev_commands/templates/aws/services/route53.rb +0 -111
  58. data/lib/firespring_dev_commands/templates/certificate.rb +0 -41
@@ -1,139 +0,0 @@
1
- require 'aws-sdk-route53'
2
-
3
- module Dev
4
- class Aws
5
- # Class for performing Route53 functions
6
- class Route53
7
- attr_reader :client
8
-
9
- def initialize(domains = nil)
10
- @client = ::Aws::Route53::Client.new
11
- @domains = Array(domains || [])
12
- end
13
-
14
- def zones(&)
15
- if @domains.empty?
16
- each_zone(&)
17
- else
18
- each_zone_by_domains(&)
19
- end
20
- end
21
-
22
- private def each_zone
23
- Dev::Aws.each_page(client, :list_hosted_zones) do |response|
24
- response.hosted_zones&.each do |hosted_zone|
25
- next if hosted_zone.config.private_zone
26
-
27
- yield hosted_zone
28
- end
29
- rescue ::Aws::Route53::Errors::Throttling
30
- sleep(1)
31
- retry
32
- end
33
- end
34
-
35
- private def each_zone_by_domains(&)
36
- @domains.each do |domain|
37
- response = client.list_hosted_zones_by_name({dns_name: domain})
38
-
39
- # The 'list_hosted_zones_by_name' returns fuzzy matches (so "foo.com" would return both "bar.foo.com" and "foo.com"
40
- # So we are only selecting domains that match exactly since that's what we really want here
41
- targets = response.hosted_zones.select { |it| it.name.chomp('.') == domain }
42
- raise "The #{domain} hosted zone not found." if targets.empty?
43
-
44
- targets.each(&)
45
- rescue ::Aws::Route53::Errors::Throttling
46
- sleep(1)
47
- retry
48
- end
49
- end
50
-
51
- private def ip_address(domain)
52
- Addrinfo.ip(domain.to_s.strip)&.ip_address
53
- rescue SocketError
54
- "Unable to resolve domain: #{domain}"
55
- end
56
-
57
- private def target_config_id(zone_id)
58
- client.list_query_logging_configs(
59
- hosted_zone_id: zone_id,
60
- max_results: '1'
61
- ).query_logging_configs&.first&.id
62
- end
63
-
64
- # Get the hosted zone details for the zone id
65
- private def details(zone_id)
66
- response = client.get_hosted_zone(id: zone_id)
67
- [response.hosted_zone, response.delegation_set]
68
- end
69
-
70
- def list_zone_details
71
- zones do |zone|
72
- puts
73
- zone_details, delegation_set = details(zone.id)
74
- dns_resource = Dev::Dns::Resource.new(zone_details.name)
75
-
76
- puts "#{zone_details.name.chomp('.').light_white} (#{zone_details.id}):"
77
- puts format(' %-50s %s', 'Delegation Set:', delegation_set.id)
78
- puts format(' %-50s %s', 'Delegation Defined Nameservers:', delegation_set.name_servers.sort.join(', '))
79
- puts format(' %-50s %s', 'DNS Reported Nameservers:', dns_resource.recursive_nameserver_lookup.sort.join(', '))
80
- puts format(' %-50s %s', 'DNS Reported Nameserver IPs:', dns_resource.recursive_nameserver_lookup.sort.map { |it| dns_resource.recursive_a_lookup(it) }.join(', '))
81
- puts format(' %-50s %s', 'Domain Apex IP Resolution:', dns_resource.recursive_a_lookup.sort.join(', '))
82
- rescue ::Aws::Route53::Errors::Throttling
83
- sleep(1)
84
- retry
85
- end
86
- puts
87
- end
88
-
89
- def list_query_configs
90
- zones do |zone|
91
- target_config_id = target_config_id(zone.id)
92
- message = if target_config_id
93
- "Config\t=>\t#{target_config_id}".colorize(:green)
94
- else
95
- 'No query logging config assigned.'.colorize(:red)
96
- end
97
- puts format('%-50s => %s', zone.name, message)
98
- rescue ::Aws::Route53::Errors::Throttling
99
- sleep(1)
100
- retry
101
- end
102
- end
103
-
104
- def activate_query_logging(log_group)
105
- zones do |zone|
106
- response = client.create_query_logging_config(
107
- hosted_zone_id: zone.id,
108
- cloud_watch_logs_log_group_arn: log_group
109
- )
110
- puts format('%-50s => %s', zone.id, response.location)
111
- rescue ::Aws::Route53::Errors::Throttling
112
- sleep(1)
113
- retry
114
- rescue ::Aws::Route53::Errors::ServiceError => e
115
- raise "Error: #{e.message}" unless e.instance_of?(::Aws::Route53::Errors::QueryLoggingConfigAlreadyExists)
116
-
117
- puts format('%-50s => %s', zone.id, e.message)
118
- end
119
- end
120
-
121
- def deactivate_query_logging
122
- zones do |zone|
123
- target_config_id = target_config_id(zone.id)
124
- if target_config_id
125
- client.delete_query_logging_config(
126
- id: target_config_id
127
- )
128
- puts format('%-50s => %s', zone.id, 'Query logging config removed.'.colorize(:green))
129
- else
130
- puts format('%-50s => %s', zone.id, 'No query logging config assigned.'.colorize(:red))
131
- end
132
- rescue ::Aws::Route53::Errors::Throttling
133
- sleep(1)
134
- retry
135
- end
136
- end
137
- end
138
- end
139
- end
@@ -1,34 +0,0 @@
1
- module Dev
2
- class BloomGrowth
3
- # Class containing rock information
4
- class Rock
5
- attr_accessor :data, :id, :type, :name, :owner, :complete, :completion_id, :created, :due
6
- attr_reader :state
7
-
8
- def initialize(data)
9
- @data = data
10
- @id = data['Id']
11
- @type = data['Type']
12
- @name = data['Name'].to_s.strip
13
- @owner = User.new(data['Owner']) if data['Owner']
14
- @complete = data['Complete']
15
- @completion_id = data['Completion']
16
- @created = Time.parse(data['CreateTime']) if data['CreateTime']
17
- @due = Time.parse(data['DueDate']) if data['DueDate']
18
- @archived = data['Archived']
19
- end
20
-
21
- # Convert the completion_id bloom growth gives us into a text version
22
- def state
23
- case completion_id
24
- when 0
25
- 'Off Track'
26
- when 1
27
- 'On Track'
28
- when 2
29
- 'Complete'
30
- end
31
- end
32
- end
33
- end
34
- end
@@ -1,16 +0,0 @@
1
- module Dev
2
- class BloomGrowth
3
- # Class containing seat information
4
- class Seat
5
- attr_accessor :data, :id, :type, :name
6
-
7
- def initialize(data)
8
- @data = data
9
- position = data.dig('Group', 'Position')
10
- @id = position&.fetch('Id')
11
- @type = position&.fetch('Type')
12
- @name = position&.fetch('Name').to_s.strip
13
- end
14
- end
15
- end
16
- end
@@ -1,43 +0,0 @@
1
- module Dev
2
- class BloomGrowth
3
- # Class containing user information
4
- class User
5
- attr_accessor :data, :id, :type, :name, :rocks, :direct_reports, :seats
6
-
7
- def initialize(data)
8
- @data = data
9
- @id = data['Id']
10
- @type = data['Type']
11
- @name = data['Name'].to_s.strip
12
- @rocks = nil
13
- @direct_reports = nil
14
- @seats = nil
15
- end
16
-
17
- def rocks
18
- @rocks ||= [].tap do |ary|
19
- Dev::BloomGrowth.new.get("/api/v1/rocks/user/#{id}") do |data|
20
- ary << Rock.new(data)
21
- end
22
- end
23
- end
24
-
25
- def direct_reports
26
- @direct_reports ||= [].tap do |ary|
27
- Dev::BloomGrowth.new.get("/api/v1/users/#{id}/directreports") do |data|
28
- ary << User.new(data)
29
- end
30
- end
31
- end
32
-
33
- def seats
34
- @seats ||= [].tap do |ary|
35
- Dev::BloomGrowth.new.get("/api/v1/users/#{id}/seats") do |data|
36
- ary << Seat.new(data)
37
- puts ary.last.inspect
38
- end
39
- end
40
- end
41
- end
42
- end
43
- end
@@ -1,132 +0,0 @@
1
- require 'net/http'
2
-
3
- module Dev
4
- # Class for interacting with the Bloom Growth api
5
- class BloomGrowth
6
- # The config file to try to load credentials from
7
- CONFIG_FILE = "#{Dir.home}/.env.bloom".freeze
8
-
9
- # The text of the username variable key
10
- BLOOM_USERNAME = 'BLOOM_USERNAME'.freeze
11
-
12
- # The text of the password variable key
13
- BLOOM_PASSWORD = 'BLOOM_PASSWORD'.freeze
14
-
15
- # The text of the token variable key
16
- BLOOM_TOKEN = 'BLOOM_TOKEN'.freeze
17
-
18
- # The text of the url variable key
19
- BLOOM_URL = 'BLOOM_URL'.freeze
20
-
21
- # Config object for setting top level bloom growth config options
22
- Config = Struct.new(:username, :password, :url, :http_debug) do
23
- def initialize
24
- Dotenv.load(CONFIG_FILE) if File.exist?(CONFIG_FILE)
25
-
26
- self.username = ENV.fetch(BLOOM_USERNAME, nil)
27
- self.password = ENV.fetch(BLOOM_PASSWORD, nil)
28
- self.url = ENV.fetch(BLOOM_URL, 'https://app.bloomgrowth.com')
29
- self.http_debug = false
30
- end
31
- end
32
-
33
- class << self
34
- # Instantiates a new top level config object if one hasn't already been created
35
- # Yields that config object to any given block
36
- # Returns the resulting config object
37
- def config
38
- @config ||= Config.new
39
- yield(@config) if block_given?
40
- @config
41
- end
42
-
43
- # Alias the config method to configure for a slightly clearer access syntax
44
- alias_method :configure, :config
45
- end
46
-
47
- attr_accessor :username, :password, :url, :token, :client, :default_headers
48
-
49
- # Initialize a new target process client using the given inputs
50
- def initialize(username: self.class.config.username, password: self.class.config.password, url: self.class.config.url)
51
- raise 'username is required' if username.to_s.strip.empty?
52
- raise 'password is required' if password.to_s.strip.empty?
53
- raise 'url is required' if url.to_s.strip.empty?
54
-
55
- @username = username
56
- @password = password
57
- @url = url
58
- uri = URI.parse(@url)
59
- @client = Net::HTTP.new(uri.host, uri.port)
60
- @client.use_ssl = true
61
- @client.verify_mode = OpenSSL::SSL::VERIFY_PEER
62
- @client.set_debug_output(LOG) if self.class.config.http_debug
63
- @default_headers = {
64
- 'authorization' => "Bearer #{token}",
65
- 'content-type' => 'application/json',
66
- 'accept' => 'application/json'
67
- }
68
- end
69
-
70
- # Method for getting a bearer token for the bloom growth api. There are a couple of possible logic paths
71
- # - If a token has already been defined, use it
72
- # - If a token is found in the ENV, use it
73
- # - Otherwise, use the username and passowrd that has been configured to request a new token from bloom
74
- def token
75
- @token ||= ENV.fetch(BLOOM_TOKEN, nil)
76
-
77
- unless @token
78
- response = post(
79
- '/Token',
80
- {
81
- grant_type: 'password',
82
- userName: username,
83
- password:
84
- },
85
- headers: {
86
- 'content-type' => 'application/json',
87
- 'accept' => 'application/json'
88
- }
89
- )
90
- # TODO: Should we look at https://github.com/DannyBen/lightly for caching the token?
91
- @token = ENV[BLOOM_TOKEN] = response['access_token']
92
- LOG.info("Retrieved BloomGrowth token. Expires on #{Time.now + response['expires_in']}")
93
- end
94
-
95
- @token
96
- end
97
-
98
- # Return all user objects visible to the logged in user
99
- def visible_users(&)
100
- [].tap do |ary|
101
- get('/api/v1/users/mineviewable') do |user_data|
102
- ary << User.new(user_data)
103
- end
104
- ary.each(&)
105
- end
106
- end
107
-
108
- # Perform a get request to the given path using the given query
109
- # Call the given block (if present) with each piece of data
110
- # Return all pieces of data
111
- def get(path, query_string: nil, headers: default_headers, &)
112
- url = path
113
- url << "?#{URI.encode_www_form(query_string)}" unless query_string.to_s.strip.empty?
114
-
115
- response = client.request_get(url, headers)
116
- raise "Error querying #{url} [#{query_string}]: #{response.inspect}" unless response.response.is_a?(Net::HTTPSuccess)
117
-
118
- JSON.parse(response.body).each(&)
119
- nil
120
- end
121
-
122
- # Perform a post request to the given path using the gien data
123
- # Return the parsed json body
124
- def post(path, data, headers: default_headers)
125
- data = data.to_json unless data.is_a?(String)
126
- response = client.request_post(path, data, headers)
127
- raise "Error querying #{url}/#{path}: #{response.inspect}" unless response.response.is_a?(Net::HTTPSuccess)
128
-
129
- JSON.parse(response.body)
130
- end
131
- end
132
- end
@@ -1,59 +0,0 @@
1
- module Dev
2
- # Class contains methods for requesting a certificate from route53.
3
- # You must have a hosted zone defined for the desired domain
4
- class Certificate
5
- attr_accessor :domains, :email
6
-
7
- def initialize(domains, email)
8
- @domains = Array(domains)
9
- @email = email
10
- raise 'No certificate domains specified' if domains.empty?
11
- end
12
-
13
- # Request the certificate using the route53 docker image
14
- # Certificate is stored in /etc/letsencrypt
15
- def request
16
- puts
17
- puts 'Getting SSL Certs For:'
18
- puts domains.join("\n")
19
- puts
20
- puts 'This process can take up to 10 minutes'
21
- puts
22
- puts Time.now
23
-
24
- # TODO: Really should use the docker api for this
25
- cmd = %w(docker run -it --rm --name certbot)
26
- cmd << '-e' << 'AWS_ACCESS_KEY_ID'
27
- cmd << '-e' << 'AWS_SECRET_ACCESS_KEY'
28
- cmd << '-e' << 'AWS_SESSION_TOKEN'
29
- cmd << '-v' << '/etc/letsencrypt:/etc/letsencrypt'
30
- cmd << 'certbot/dns-route53:latest'
31
- cmd << 'certonly'
32
- cmd << '-n'
33
- cmd << '--agree-tos'
34
- cmd << '--dns-route53'
35
- cmd << '-d' << domains.join(',')
36
- cmd << '--email' << email
37
- cmd << '--server' << 'https://acme-v02.api.letsencrypt.org/directory'
38
- puts cmd.join(' ')
39
- Dev::Common.new.run_command(cmd)
40
- end
41
-
42
- # Saves the latest version of the certificate into the given dest_dir
43
- def save(dest_dir)
44
- raise "directory #{dest_dir} must be an existing directory" unless File.directory?(dest_dir)
45
-
46
- domain = domains.first.sub(/^\*\./, '') # Need to strip off the '*.' if this is a wildcard cert
47
- directories = Dir.glob("/etc/letsencrypt/live/#{domain}*/")
48
- no_suffix = directories.delete("/etc/letsencrypt/live/#{domain}/")
49
- biggest_suffix = directories.max
50
- source_dir = biggest_suffix || no_suffix
51
- raise "unable to determine certificate directory for #{domain}" unless source_dir
52
-
53
- FileUtils.cp("#{source_dir}privkey.pem", dest_dir, verbose: true)
54
- FileUtils.cp("#{source_dir}cert.pem", dest_dir, verbose: true)
55
- FileUtils.cp("#{source_dir}chain.pem", dest_dir, verbose: true)
56
- FileUtils.cp("#{source_dir}fullchain.pem", dest_dir, verbose: true)
57
- end
58
- end
59
- end
@@ -1,16 +0,0 @@
1
- module Dev
2
- module Coverage
3
- # Class which defines the methods which must be implemented to function as a coverage class
4
- class Base
5
- # Raises not implemented
6
- def php_options
7
- raise 'not implemented'
8
- end
9
-
10
- # Raises not implemented
11
- def check(*)
12
- raise 'not implemented'
13
- end
14
- end
15
- end
16
- end
@@ -1,86 +0,0 @@
1
- module Dev
2
- # Module containing different classes for interfacing with coverage files
3
- module Coverage
4
- # Class for checking code coverage using cobertura
5
- class Cobertura < Base
6
- attr_reader :local_filename, :container_filename, :filename, :threshold, :exclude
7
-
8
- def initialize(filename: File.join('coverage', 'cobertura.xml'), threshold: nil, container_path: nil, local_path: nil, exclude: nil)
9
- super()
10
-
11
- @filename = filename
12
- @local_filename = File.join(local_path || '.', @filename)
13
- @container_filename = File.join(container_path || '.', @filename)
14
- @threshold = threshold
15
- @exclude = (exclude || []).map do |it|
16
- next it if it.is_a?(Regex)
17
-
18
- Regex.new(it)
19
- end
20
- end
21
-
22
- # Remove any previous versions of the local file that will be output
23
- # return the phpunit options needed to regenerate the cobertura xml file
24
- def php_options
25
- # Remove any previous coverage info
26
- FileUtils.rm_f(local_filename, verbose: true)
27
-
28
- # Return the needed php commands to generate the cobertura report
29
- %W(--coverage-cobertura #{container_filename})
30
- end
31
-
32
- # Parse the cobertura file and check the lines missed against the desired threshold
33
- def check(application: nil)
34
- # If an application has been specified and the file does not exist locally, attempt to copy it back from the docker container
35
- if application && !File.exist?(local_filename)
36
- container = Dev::Docker::Compose.new.container_by_name(application)
37
- Dev::Docker.new.copy_from_container(container, container_filename, local_filename, required: true)
38
- end
39
-
40
- report = Ox.load(File.read(local_filename))
41
- total_missed = report.coverage.locate('packages/package').sum { |package| parse_package_missed(package) }
42
- puts "Lines missing coverage was #{total_missed}"
43
- puts "Configured threshold was #{threshold}" if threshold
44
- raise 'Code coverage not met' if threshold && total_missed > threshold
45
- end
46
-
47
- # Go through the package and add up all of the lines that were missed
48
- # Ignore if the file was in the exlude list
49
- private def parse_package_missed(package)
50
- filename = package.attributes[:name]
51
- return if exclude.any? { |it| it.match(filename) }
52
-
53
- missed = 0
54
- lines_processed = Set.new
55
- package.locate('classes/class/lines/line').each do |line|
56
- # Don't count lines multiple times
57
- line_number = line.attributes[:number]
58
- next if lines_processed.include?(line_number)
59
-
60
- lines_processed << line_number
61
- missed += 1 unless line.attributes[:hits].to_i.positive?
62
- end
63
- total = lines_processed.length
64
-
65
- sanity_check_coverage_against_cobertura_values(package, missed, total)
66
- missed
67
- end
68
-
69
- # Calculate the coverage percent based off the numbers we got and compare to the
70
- # value cobertura reported. This is meant as a sanity check that we are reading the data correctly
71
- # TODO: This should be removed after the above logic has been vetted
72
- private def sanity_check_coverage_against_cobertura_values(package, missed, total)
73
- line_rate = package.attributes[:'line-rate']
74
- cobertura_reported_coverage = line_rate.to_f
75
- cobertura_reported_precision = line_rate.split('.').last.length
76
-
77
- file_coverage = 0.0
78
- file_coverage = ((total - missed).to_f / total).round(cobertura_reported_precision) if total.positive?
79
- return if file_coverage == cobertura_reported_coverage
80
-
81
- filename = package.attributes[:name]
82
- puts "WARNINNG: #{filename} coverage (#{file_coverage}) differed from what cobertura reported (#{cobertura_reported_coverage})"
83
- end
84
- end
85
- end
86
- end
@@ -1,21 +0,0 @@
1
- module Dev
2
- # Module with a variety of coverage methods for different languages
3
- module Coverage
4
- # Class which provides methods to effectvely skip coverage
5
- class None < Base
6
- def initialize(*)
7
- super()
8
- end
9
-
10
- # Returns the php options for generating code coverage file
11
- def php_options
12
- []
13
- end
14
-
15
- # Checks the code coverage against the defined threshold
16
- def check(*)
17
- puts 'Coverage not configured'
18
- end
19
- end
20
- end
21
- end
@@ -1,83 +0,0 @@
1
- module Dev
2
- class Dns
3
- class Resource
4
- attr_reader :domain
5
-
6
- def initialize(domain)
7
- @domain = domain
8
- end
9
-
10
- # Returns whether or not the given value is a valid IPv4 or IPv6 address
11
- def self.ip?(value)
12
- ipv4?(value) || ipv6?(value)
13
- end
14
-
15
- # Returns whether or not the given value is a valid IPv4 address
16
- def self.ipv4?(value)
17
- value.match?(Resolv::IPv4::Regex)
18
- end
19
-
20
- # Returns whether or not the given value is a valid IPv6 address
21
- def self.ipv6?(value)
22
- value.match?(Resolv::IPv6::Regex)
23
- end
24
-
25
- # Recursively determine the correct nameservers for the given domain.
26
- # If nameservers are not found, strip subdomains off until we've reached the TLD
27
- def recursive_nameserver_lookup(name = domain)
28
- records = lookup(name, type: Resolv::DNS::Resource::IN::NS)
29
-
30
- # Strip the subdomain and try again if we didn't find any nameservers (this can happen with wildcards)
31
- return recursive_nameserver_lookup(name.split('.', 2).last) if records.empty?
32
-
33
- # Look up the IPs for the nameservers
34
- records
35
- end
36
-
37
- # Recursively attempt to find an A record for the given domain.
38
- # If one isn't found, also check for CNAMEs continually until we have either found an IP or run out of things to check
39
- def recursive_a_lookup(name = domain)
40
- # Try looking up an A record first. If we find one, we are done.
41
- records = lookup(name, type: Resolv::DNS::Resource::IN::A)
42
- return records unless records.empty?
43
-
44
- # Try looking up a CNAME record
45
- records = lookup(name, type: Resolv::DNS::Resource::IN::CNAME)
46
-
47
- # If we didn't find an A record _or_ a CNAME, just return empty
48
- return records if records.empty?
49
-
50
- # If we found more than one CNAME that is a DNS error
51
- raise "Found more than one CNAME entry for #{name}. This is not allowed by DNS" if records.length > 1
52
-
53
- recursive_a_lookup(records.first)
54
- end
55
-
56
- # Lookup the given name using the record type provided.
57
- def lookup(name = domain, type: Resolv::DNS::Resource::IN::A)
58
- # Validate the type
59
- raise 'lookup type must be a Resolv::DNS::Resource' unless type.ancestors.include?(Resolv::DNS::Resource)
60
-
61
- # If we were given a tld, return empty
62
- return [] unless name.include?('.')
63
-
64
- # Look up NS records for the given host
65
- records = Resolv::DNS.new.getresources(name, type)
66
-
67
- # Return the record names
68
- records.map do |record|
69
- if record.respond_to?(:address)
70
- record.address.to_s
71
- elsif record.respond_to?(:name)
72
- record.name.to_s
73
- else
74
- ''
75
- end
76
- end
77
- rescue
78
- sleep(1)
79
- retry
80
- end
81
- end
82
- end
83
- end