Skip to content

Commit

Permalink
Merge pull request voxpupuli#288 from powerhome/preparedQueries
Browse files Browse the repository at this point in the history
Prepared Queries
  • Loading branch information
solarkennedy authored Oct 13, 2016
2 parents 179d9fd + bdebfad commit e6eda6b
Show file tree
Hide file tree
Showing 4 changed files with 361 additions and 0 deletions.
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,22 @@ ACLs if the anonymous token doesn't permit ACL changes (which is likely).
The api token may be the master token, another management token, or any
client token with sufficient privileges.

## Prepared Queries

```puppet
consul_prepared_query { 'consul':
ensure => 'present',
service_name => 'consul',
service_failover_n => 1,
service_failover_dcs => [ 'dc1', 'dc2' ],
service_only_passing => true,
service_tags => [ 'tag1', 'tag2' ],
ttl => 10,
}
```

This provider currently only has support for basic prepared queries (not templated queries).

## Limitations

Depends on the JSON gem, or a modern ruby. (Ruby 1.8.7 is not officially supported)
Expand Down
203 changes: 203 additions & 0 deletions lib/puppet/provider/consul_prepared_query/default.rb
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
111 changes: 111 additions & 0 deletions lib/puppet/type/consul_prepared_query.rb
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
31 changes: 31 additions & 0 deletions spec/unit/puppet/type/consul_prepared_query_spec.rb
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

0 comments on commit e6eda6b

Please sign in to comment.