Skip to content

Commit e4a4812

Browse files
authored
Merge pull request #7856 from melissa/ticket/master/pup-10039-serverlist-resolver
(PUP-10039) Add ServerList resolver
2 parents 4cae5c9 + a139ae8 commit e4a4812

File tree

9 files changed

+131
-38
lines changed

9 files changed

+131
-38
lines changed

lib/puppet/http.rb

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ module HTTP
2020
require 'puppet/http/service/ca'
2121
require 'puppet/http/session'
2222
require 'puppet/http/resolver'
23+
require 'puppet/http/resolver/server_list'
2324
require 'puppet/http/resolver/settings'
2425
require 'puppet/http/resolver/srv'
2526
require 'puppet/http/client'

lib/puppet/http/client.rb

+2-2
Original file line numberDiff line numberDiff line change
@@ -147,10 +147,10 @@ def build_resolvers
147147
resolvers = []
148148

149149
if Puppet[:use_srv_records]
150-
resolvers << Puppet::HTTP::Resolver::SRV.new(domain: Puppet[:srv_domain])
150+
resolvers << Puppet::HTTP::Resolver::SRV.new(self, domain: Puppet[:srv_domain])
151151
end
152152

153-
resolvers << Puppet::HTTP::Resolver::Settings.new
153+
resolvers << Puppet::HTTP::Resolver::Settings.new(self)
154154
resolvers.freeze
155155
end
156156
end

lib/puppet/http/resolver.rb

+14-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
11
class Puppet::HTTP::Resolver
2-
def resolve(session, name, &block)
2+
def initialize(client)
3+
@client = client
4+
end
5+
6+
def resolve(session, name, ssl_context: nil)
37
raise NotImplementedError
48
end
9+
10+
def check_connection?(session, service, ssl_context: nil)
11+
service.connect(ssl_context: ssl_context)
12+
return true
13+
rescue Puppet::HTTP::ConnectionError => e
14+
session.add_exception(e)
15+
Puppet.debug("Connection to #{service.url} failed, trying next route: #{e.message}")
16+
return false
17+
end
518
end
+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
class Puppet::HTTP::Resolver::ServerList < Puppet::HTTP::Resolver
2+
def initialize(client, server_list:, default_port:)
3+
@client = client
4+
@server_list = server_list
5+
@default_port = default_port
6+
end
7+
8+
def resolve(session, name, ssl_context: nil)
9+
@server_list.each do |server|
10+
host = server[0]
11+
port = server[1] || @default_port
12+
uri = URI("https://#{host}:#{port}/status/v1/simple/master")
13+
if get_success?(uri, session, ssl_context: ssl_context)
14+
return Puppet::HTTP::Service.create_service(@client, name, host, port)
15+
end
16+
end
17+
18+
raise Puppet::Error, _("Could not select a functional puppet master from server_list: '%{server_list}'") % { server_list: Puppet.settings.value(:server_list, Puppet[:environment].to_sym, true) }
19+
end
20+
21+
def get_success?(uri, session, ssl_context: nil)
22+
response = @client.get(uri, ssl_context: ssl_context)
23+
return true if response.success?
24+
25+
Puppet.debug(_("Puppet server %{host}:%{port} is unavailable: %{code} %{reason}") %
26+
{ host: host, port: port, code: response.code, reason: response.message })
27+
return false
28+
rescue => detail
29+
session.add_exception(detail)
30+
#TRANSLATORS 'server_list' is the name of a setting and should not be translated
31+
Puppet.debug _("Unable to connect to server from server_list setting: %{detail}") % {detail: detail}
32+
return false
33+
end
34+
end

lib/puppet/http/resolver/settings.rb

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
class Puppet::HTTP::Resolver::Settings < Puppet::HTTP::Resolver
2-
def resolve(session, name, &block)
3-
yield session.create_service(name)
2+
def resolve(session, name, ssl_context: nil)
3+
service = Puppet::HTTP::Service.create_service(@client, name)
4+
check_connection?(session, service, ssl_context: ssl_context) ? service : nil
45
end
56
end

lib/puppet/http/resolver/srv.rb

+7-3
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
class Puppet::HTTP::Resolver::SRV < Puppet::HTTP::Resolver
2-
def initialize(domain: srv_domain, dns: Resolv::DNS.new)
2+
def initialize(client, domain:, dns: Resolv::DNS.new)
3+
@client = client
34
@srv_domain = domain
45
@delegate = Puppet::Network::Resolver.new(dns)
56
end
67

7-
def resolve(session, name, &block)
8+
def resolve(session, name, ssl_context: nil)
89
# Here we pass our HTTP service name as the DNS SRV service name
910
# This is fine for :ca, but note that :puppet and :file are handled
1011
# specially in `each_srv_record`.
1112
@delegate.each_srv_record(@srv_domain, name) do |server, port|
12-
yield session.create_service(name, server, port)
13+
service = Puppet::HTTP::Service.create_service(@client, name, server, port)
14+
return service if check_connection?(session, service, ssl_context: ssl_context)
1315
end
16+
17+
return nil
1418
end
1519
end

lib/puppet/http/session.rb

+10-15
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ def initialize(client, resolvers)
33
@client = client
44
@resolvers = resolvers
55
@resolved_services = {}
6+
@resolution_exceptions = []
67
end
78

89
def route_to(name, ssl_context: nil)
@@ -11,29 +12,23 @@ def route_to(name, ssl_context: nil)
1112
cached = @resolved_services[name]
1213
return cached if cached
1314

14-
errors = []
15+
@resolution_exceptions = []
1516

1617
@resolvers.each do |resolver|
1718
Puppet.debug("Resolving service '#{name}' using #{resolver.class}")
18-
resolver.resolve(self, name) do |service|
19-
begin
20-
service.connect(ssl_context: ssl_context)
21-
@resolved_services[name] = service
22-
Puppet.debug("Resolved service '#{name}' to #{service.url}")
23-
return service
24-
rescue Puppet::HTTP::ConnectionError => e
25-
errors << e
26-
Puppet.debug("Connection to #{service.url} failed, trying next route: #{e.message}")
27-
end
19+
service = resolver.resolve(self, name, ssl_context: ssl_context)
20+
if service
21+
@resolved_services[name] = service
22+
Puppet.debug("Resolved service '#{name}' to #{service.url}")
23+
return service
2824
end
2925
end
3026

31-
errors.each { |e| Puppet.log_exception(e) }
32-
27+
@resolution_exceptions.each { |e| Puppet.log_exception(e) }
3328
raise Puppet::HTTP::RouteError, "No more routes to #{name}"
3429
end
3530

36-
def create_service(name, server = nil, port = nil)
37-
Puppet::HTTP::Service.create_service(@client, name, server, port)
31+
def add_exception(exception)
32+
@resolution_exceptions << exception
3833
end
3934
end

spec/unit/http/resolver_spec.rb

+48-12
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,59 @@
99
let(:uri) { URI.parse('https://www.example.com') }
1010

1111
context 'when resolving using settings' do
12-
let(:subject) { Puppet::HTTP::Resolver::Settings.new }
12+
let(:subject) { Puppet::HTTP::Resolver::Settings.new(client) }
1313

14-
it 'yields a service based on the current ca_server and ca_port settings' do
14+
it 'returns a service based on the current ca_server and ca_port settings' do
1515
Puppet[:ca_server] = 'ca.example.com'
1616
Puppet[:ca_port] = 8141
1717

18-
subject.resolve(session, :ca) do |service|
19-
expect(service).to be_an_instance_of(Puppet::HTTP::Service::Ca)
20-
expect(service.url.to_s).to eq("https://ca.example.com:8141/puppet-ca/v1")
21-
end
18+
service = subject.resolve(session, :ca)
19+
expect(service).to be_an_instance_of(Puppet::HTTP::Service::Ca)
20+
expect(service.url.to_s).to eq("https://ca.example.com:8141/puppet-ca/v1")
21+
end
22+
end
23+
24+
context 'when resolving using server_list' do
25+
let(:server_list) { [["ca.example.com", "8141"], ["apple.example.com"]] }
26+
let(:default_port) { '8142' }
27+
let(:subject) { Puppet::HTTP::Resolver::ServerList.new(client, server_list: server_list, default_port: default_port) }
28+
29+
it 'returns a service based on the current server_list setting' do
30+
stub_request(:get, "https://ca.example.com:8141/status/v1/simple/master").to_return(status: 200)
31+
32+
service = subject.resolve(session, :ca)
33+
expect(service).to be_an_instance_of(Puppet::HTTP::Service::Ca)
34+
expect(service.url.to_s).to eq("https://ca.example.com:8141/puppet-ca/v1")
35+
end
36+
37+
it 'returns a service based on the current server_list setting if the server returns any success codes' do
38+
stub_request(:get, "https://ca.example.com:8141/status/v1/simple/master").to_return(status: 202)
39+
40+
service = subject.resolve(session, :ca)
41+
expect(service).to be_an_instance_of(Puppet::HTTP::Service::Ca)
42+
expect(service.url.to_s).to eq("https://ca.example.com:8141/puppet-ca/v1")
43+
end
44+
45+
it 'falls fails if no servers in server_list are accessible' do
46+
stub_request(:get, "https://ca.example.com:8141/status/v1/simple/master").to_return(status: 503)
47+
stub_request(:get, "https://apple.example.com:8142/status/v1/simple/master").to_return(status: 503)
48+
49+
expect { subject.resolve(session, :ca) }.to raise_error(Puppet::Error, /^Could not select a functional puppet master from server_list:/)
50+
end
51+
52+
it 'cycles through server_list until a valid server is found' do
53+
stub_request(:get, "https://ca.example.com:8141/status/v1/simple/master").to_return(status: 503)
54+
stub_request(:get, "https://apple.example.com:8142/status/v1/simple/master").to_return(status: 200)
55+
56+
service = subject.resolve(session, :ca)
57+
expect(service).to be_an_instance_of(Puppet::HTTP::Service::Ca)
58+
expect(service.url.to_s).to eq("https://apple.example.com:8142/puppet-ca/v1")
2259
end
2360
end
2461

2562
context 'when resolving using SRV' do
2663
let(:dns) { double('dns') }
27-
let(:subject) { Puppet::HTTP::Resolver::SRV.new(domain: 'example.com', dns: dns) }
64+
let(:subject) { Puppet::HTTP::Resolver::SRV.new(client, domain: 'example.com', dns: dns) }
2865

2966
def stub_srv(host, port)
3067
srv = Resolv::DNS::Resource::IN::SRV.new(0, 0, port, host)
@@ -33,13 +70,12 @@ def stub_srv(host, port)
3370
allow(dns).to receive(:getresources).with("_x-puppet-ca._tcp.example.com", Resolv::DNS::Resource::IN::SRV).and_return([srv])
3471
end
3572

36-
it 'yields a service based on an SRV record' do
73+
it 'returns a service based on an SRV record' do
3774
stub_srv('ca1.example.com', 8142)
3875

39-
subject.resolve(session, :ca) do |service|
40-
expect(service).to be_an_instance_of(Puppet::HTTP::Service::Ca)
41-
expect(service.url.to_s).to eq("https://ca1.example.com:8142/puppet-ca/v1")
42-
end
76+
service = subject.resolve(session, :ca)
77+
expect(service).to be_an_instance_of(Puppet::HTTP::Service::Ca)
78+
expect(service.url.to_s).to eq("https://ca1.example.com:8142/puppet-ca/v1")
4379
end
4480
end
4581
end

spec/unit/http/session_spec.rb

+12-3
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
double('good', url: uri, connect: nil)
1111
}
1212
let(:bad_service) {
13-
service = double('good', url: uri)
13+
service = double('bad', url: uri)
1414
allow(service).to receive(:connect).and_raise(Puppet::HTTP::ConnectionError, 'whoops')
1515
service
1616
}
@@ -23,9 +23,18 @@ def initialize(service)
2323
@count = 0
2424
end
2525

26-
def resolve(session, name, &block)
26+
def resolve(session, name, ssl_context: nil)
2727
@count += 1
28-
yield @service
28+
return @service if check_connection?(session, @service, ssl_context: ssl_context)
29+
end
30+
31+
def check_connection?(session, service, ssl_context: nil)
32+
service.connect(ssl_context: ssl_context)
33+
return true
34+
rescue Puppet::HTTP::ConnectionError => e
35+
session.add_exception(e)
36+
Puppet.debug("Connection to #{service.url} failed, trying next route: #{e.message}")
37+
return false
2938
end
3039
end
3140

0 commit comments

Comments
 (0)