-
-
Notifications
You must be signed in to change notification settings - Fork 313
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #288 from powerhome/preparedQueries
Prepared Queries
- Loading branch information
Showing
4 changed files
with
361 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,203 @@ | ||
require 'json' | ||
require 'net/http' | ||
require 'uri' | ||
Puppet::Type.type(:consul_prepared_query).provide( | ||
:default | ||
) do | ||
mk_resource_methods | ||
|
||
def self.prefetch(resources) | ||
resources.each do |name, resource| | ||
Puppet.debug("prefetching for #{name}") | ||
port = resource[:port] | ||
hostname = resource[:hostname] | ||
protocol = resource[:protocol] | ||
token = resource[:acl_api_token] | ||
tries = resource[:api_tries] | ||
|
||
found_prepared_queries = list_resources(token, port, hostname, protocol, tries).select do |prepared_query| | ||
prepared_query[:name] == name | ||
end | ||
|
||
found_prepared_query = found_prepared_queries.first || nil | ||
if found_prepared_query | ||
Puppet.debug("found #{found_prepared_query}") | ||
resource.provider = new(found_prepared_query) | ||
else | ||
Puppet.debug("found none #{name}") | ||
resource.provider = new({:ensure => :absent}) | ||
end | ||
end | ||
end | ||
|
||
def self.list_resources(acl_api_token, port, hostname, protocol, tries) | ||
if @prepared_queries | ||
return @prepared_queries | ||
end | ||
|
||
# this might be configurable by searching /etc/consul.d | ||
# but would break for anyone using nonstandard paths | ||
uri = URI("#{protocol}://#{hostname}:#{port}/v1/query") | ||
http = Net::HTTP.new(uri.host, uri.port) | ||
|
||
path=uri.request_uri + "?token=#{acl_api_token}" | ||
req = Net::HTTP::Get.new(path) | ||
res = nil | ||
|
||
# retry Consul API query for ACLs, in case Consul has just started | ||
(1..tries).each do |i| | ||
unless i == 1 | ||
Puppet.debug("retrying Consul API query in #{i} seconds") | ||
sleep i | ||
end | ||
res = http.request(req) | ||
break if res.code == '200' | ||
end | ||
|
||
if res.code == '200' | ||
prepared_queries = JSON.parse(res.body) | ||
else | ||
Puppet.warning("Cannot retrieve prepared_queries: invalid return code #{res.code} uri: #{path} body: #{req.body}") | ||
return {} | ||
end | ||
|
||
nprepared_queries = prepared_queries.collect do |prepared_query| | ||
{ | ||
:name => prepared_query["Name"], | ||
:id => prepared_query["ID"], | ||
:session => prepared_query["Session"], | ||
:token => prepared_query["Token"], | ||
:service => prepared_query["Service"], | ||
:dns => prepared_query["DNS"], | ||
:ensure => :present, | ||
} | ||
end | ||
|
||
@prepared_queries = nprepared_queries | ||
nprepared_queries | ||
end | ||
|
||
def get_path(id) | ||
idstr = id ? "/#{id}" : '' | ||
uri = URI("#{@resource[:protocol]}://#{@resource[:hostname]}:#{@resource[:port]}/v1/query#{idstr}") | ||
http = Net::HTTP.new(uri.host, uri.port) | ||
acl_api_token = @resource[:acl_api_token] | ||
return uri.request_uri + "?token=#{acl_api_token}", http | ||
end | ||
|
||
def create_prepared_query(body) | ||
path, http = get_path(false) | ||
req = Net::HTTP::Post.new(path) | ||
if body | ||
req.body = body.to_json | ||
end | ||
res = http.request(req) | ||
if res.code != '200' | ||
raise(Puppet::Error,"Session #{name} create: invalid return code #{res.code} uri: #{path} body: #{req.body}") | ||
end | ||
end | ||
|
||
def update_prepared_query(id, body) | ||
path, http = get_path(id) | ||
req = Net::HTTP::Put.new(path) | ||
if body | ||
body[:id] = id | ||
req.body = body.to_json | ||
end | ||
res = http.request(req) | ||
if res.code != '200' | ||
raise(Puppet::Error,"Session #{name} update: invalid return code #{res.code} uri: #{path} body: #{req.body}") | ||
end | ||
end | ||
|
||
def delete_prepared_query(id) | ||
path, http = get_path(id) | ||
req = Net::HTTP::Delete.new(path) | ||
res = http.request(req) | ||
if res.code != '200' | ||
raise(Puppet::Error,"Session #{name} delete: invalid return code #{res.code} uri: #{path} body: #{req.body}") | ||
end | ||
end | ||
|
||
def get_resource(name, port, hostname, protocol, tries) | ||
acl_api_token = @resource[:acl_api_token] | ||
resources = self.class.list_resources(acl_api_token, port, hostname, protocol, tries).select do |res| | ||
res[:name] == name | ||
end | ||
# if the user creates multiple with the same name this will do odd things | ||
resources.first || nil | ||
end | ||
|
||
def initialize(value={}) | ||
super(value) | ||
@property_flush = {} | ||
end | ||
|
||
def exists? | ||
@property_hash[:ensure] == :present | ||
end | ||
|
||
def create | ||
@property_flush[:ensure] = :present | ||
end | ||
|
||
def destroy | ||
@property_flush[:ensure] = :absent | ||
end | ||
|
||
def flush | ||
name = @resource[:name] | ||
token = @resource[:token] | ||
service_name = @resource[:service_name] | ||
service_failover_n = @resource[:service_failover_n] | ||
service_failover_dcs = @resource[:service_failover_dcs] | ||
service_only_passing = @resource[:service_only_passing] | ||
service_tags = @resource[:service_tags] | ||
ttl = @resource[:ttl] | ||
port = @resource[:port] | ||
hostname = @resource[:hostname] | ||
protocol = @resource[:protocol] | ||
tries = @resource[:api_tries] | ||
prepared_query = self.get_resource(name, port, hostname, protocol, tries) | ||
if prepared_query | ||
id = prepared_query[:id] | ||
if @property_flush[:ensure] == :absent | ||
delete_prepared_query(id) | ||
return | ||
end | ||
update_prepared_query(id, { | ||
"Name" => "#{name}", | ||
"Token" => "#{token}", | ||
"Service" => { | ||
"Service" => "#{service_name}", | ||
"Failover" => { | ||
"NearestN" => service_failover_n, | ||
"Datacenters" => service_failover_dcs, | ||
}, | ||
"OnlyPassing" => service_only_passing, | ||
"Tags" => service_tags, | ||
}, | ||
"DNS" => { | ||
"TTL" => "#{ttl}s" | ||
}}) | ||
|
||
else | ||
create_prepared_query({ | ||
"Name" => "#{name}", | ||
"Token" => "#{token}", | ||
"Service" => { | ||
"Service" => "#{service_name}", | ||
"Failover" => { | ||
"NearestN" => service_failover_n, | ||
"Datacenters" => service_failover_dcs, | ||
}, | ||
"OnlyPassing" => service_only_passing, | ||
"Tags" => service_tags, | ||
}, | ||
"DNS" => { | ||
"TTL" => "#{ttl}s" | ||
}}) | ||
end | ||
@property_hash.clear | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
Puppet::Type.newtype(:consul_prepared_query) do | ||
|
||
desc <<-'EOD' | ||
Manage a consul prepared query. | ||
EOD | ||
ensurable | ||
|
||
newparam(:name, :namevar => true) do | ||
desc 'Name of the prepared query' | ||
validate do |value| | ||
raise ArgumentError, "Prepared query name must be a string" if not value.is_a?(String) | ||
end | ||
end | ||
|
||
newparam(:token) do | ||
desc 'The prepared query token' | ||
validate do |value| | ||
raise ArgumentError, "The prepared query token must be a string" if not value.is_a?(String) | ||
end | ||
defaultto '' | ||
end | ||
|
||
newparam(:acl_api_token) do | ||
desc 'Token for accessing the ACL API' | ||
validate do |value| | ||
raise ArgumentError, "ACL API token must be a string" if not value.is_a?(String) | ||
end | ||
defaultto '' | ||
end | ||
|
||
newparam(:service_name) do | ||
desc 'Service name for the prepared query' | ||
validate do |value| | ||
raise ArgumentError, "Prepared query service definition must be a string" if not value.is_a?(String) | ||
end | ||
end | ||
|
||
newparam(:service_failover_n) do | ||
desc 'Failover to the nearest <n> datacenters' | ||
defaultto 0 | ||
validate do |value| | ||
raise ArgumentError, "Nearest failover datacenters must be an integer" if not value.is_a?(Integer) | ||
end | ||
end | ||
|
||
newparam(:service_failover_dcs) do | ||
desc 'List of datacenters to forward queries to if no health services found locally' | ||
defaultto [] | ||
validate do |value| | ||
raise ArgumentError, "Nearest failover datacenters must be an array" if not value.is_a?(Array) | ||
end | ||
end | ||
|
||
newparam(:service_only_passing) do | ||
desc 'Only return services in the passing state' | ||
defaultto false | ||
validate do |value| | ||
raise ArgumentError, "Use only passing state must be a boolean" if not !!value == value | ||
end | ||
end | ||
|
||
newparam(:service_tags) do | ||
desc 'List of tags to filter the query with' | ||
defaultto [] | ||
validate do |value| | ||
raise ArgumentError, "Query tag filters must be an array" if not value.is_a?(Array) | ||
end | ||
end | ||
|
||
newparam(:ttl) do | ||
desc 'TTL for the DNS lookup' | ||
defaultto 0 | ||
validate do |value| | ||
raise ArgumentError, "Prepared query TTL must be an integer" if not value.is_a?(Integer) | ||
end | ||
end | ||
|
||
newproperty(:id) do | ||
desc 'ID of prepared query' | ||
end | ||
|
||
newproperty(:protocol) do | ||
desc 'consul protocol' | ||
newvalues('http', 'https') | ||
defaultto 'http' | ||
end | ||
|
||
newparam(:port) do | ||
desc 'consul port' | ||
defaultto 8500 | ||
validate do |value| | ||
raise ArgumentError, "The port number must be a number" if not value.is_a?(Integer) | ||
end | ||
end | ||
|
||
newparam(:hostname) do | ||
desc 'consul hostname' | ||
validate do |value| | ||
raise ArgumentError, "The hostname must be a string" if not value.is_a?(String) | ||
end | ||
defaultto 'localhost' | ||
end | ||
|
||
newparam(:api_tries) do | ||
desc 'number of tries when contacting the Consul REST API' | ||
defaultto 3 | ||
validate do |value| | ||
raise ArgumentError, "Number of API tries must be a number" if not value.is_a?(Integer) | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
describe Puppet::Type.type(:consul_prepared_query) do | ||
|
||
it 'should fail if no name is provided' do | ||
expect do | ||
Puppet::Type.type(:consul_prepared_query).new(:type => 'client') | ||
end.to raise_error(Puppet::Error, /Title or name must be provided/) | ||
end | ||
|
||
context 'with query parameters provided' do | ||
before :each do | ||
@acl = Puppet::Type.type(:consul_prepared_query).new( | ||
:name => 'testing', | ||
:token => '', | ||
:service_name => 'testing', | ||
:service_failover_n => 1, | ||
:service_failover_dcs => [ 'dc1', 'dc2' ], | ||
:service_tags => [ 'tag1', 'tag2' ], | ||
:service_only_passing => true, | ||
:ttl => 10 | ||
) | ||
end | ||
|
||
it 'should default to localhost' do | ||
expect(@acl[:hostname]).to eq('localhost') | ||
end | ||
|
||
it 'should default to http' do | ||
expect(@acl[:protocol]).to eq(:http) | ||
end | ||
end | ||
end |