jn_services 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.coco.yml +5 -0
- data/.gitignore +10 -0
- data/.rubocop.yml +13 -0
- data/.travis.yml +11 -0
- data/CHANGELOG.md +20 -0
- data/Gemfile +13 -0
- data/LICENSE.txt +22 -0
- data/README.md +5 -0
- data/Rakefile +32 -0
- data/build_etcd +8 -0
- data/jn_services.gemspec +26 -0
- data/lib/services/connection.rb +148 -0
- data/lib/services/endpoint.rb +24 -0
- data/lib/services/entity.rb +71 -0
- data/lib/services/member.rb +32 -0
- data/lib/services/service.rb +59 -0
- data/lib/services/version.rb +4 -0
- data/lib/services.rb +53 -0
- data/spec/.rubocop.yml +5 -0
- data/spec/endpoint_spec.rb +47 -0
- data/spec/member_spec.rb +37 -0
- data/spec/service_spec.rb +18 -0
- data/spec/services_spec.rb +46 -0
- data/spec/spec_helper.rb +104 -0
- metadata +130 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: cee0b10f7814fc15d743fbec9be6c8b727fb7f34
|
4
|
+
data.tar.gz: 29f644614ad66c76051774e77f798873b3be4a3e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 9ab1f4d25e5b846a14f929bfe289c32692446199db1a4a8cbaef12c17c7e5e2aab2575081d622991afaca6d57732d7b388c799ab8920169ccfa27b266c2622e2
|
7
|
+
data.tar.gz: 262b787d5ea448e9a187e0fcbf399f4ef173304439cdeb570b6bd489134b78b620058b1067269fa690c95ef6f6156a42d9e3bdf6beaf839d5ebfb6136bb9c73c
|
data/.coco.yml
ADDED
data/.gitignore
ADDED
data/.rubocop.yml
ADDED
data/.travis.yml
ADDED
data/CHANGELOG.md
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
### 1.2.0
|
2
|
+
* switch to rubocop
|
3
|
+
* update etcd gem to 0.0.6
|
4
|
+
* compatable with etcd 0.2.0 & 0.3.0
|
5
|
+
|
6
|
+
### 1.1.1
|
7
|
+
* bugfix remove of options attr from endoint
|
8
|
+
* cleanup tailor issues
|
9
|
+
|
10
|
+
### 1.1.0
|
11
|
+
* add all method to services that returns an array of all services
|
12
|
+
* add subscribed method to services that returns array of all services a node is mapped too
|
13
|
+
|
14
|
+
### 1.0.6
|
15
|
+
* revert naming change
|
16
|
+
|
17
|
+
### 1.0.5
|
18
|
+
* Fix bug where .load method was returnign a hash instead of self
|
19
|
+
* make compatable with the 0.0.4 etcd-ruby gem
|
20
|
+
* make service return objects for endpoint and members instead of hashes.
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Jesse Nelson
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require "rspec/core/rake_task"
|
3
|
+
require "rdoc/task"
|
4
|
+
|
5
|
+
task default: 'test:quick'
|
6
|
+
|
7
|
+
|
8
|
+
RDoc::Task.new do |rdoc|
|
9
|
+
rdoc.main = "README.rdoc"
|
10
|
+
rdoc.rdoc_files.include("lib /*.rb")
|
11
|
+
end
|
12
|
+
|
13
|
+
namespace :test do
|
14
|
+
RSpec::Core::RakeTask.new("spec") do |t|
|
15
|
+
t.rspec_opts = '--color --fail-fast'
|
16
|
+
end
|
17
|
+
|
18
|
+
begin
|
19
|
+
require 'rubocop/rake_task'
|
20
|
+
Rubocop::RakeTask.new do |task|
|
21
|
+
task.fail_on_error = true
|
22
|
+
end
|
23
|
+
rescue LoadError
|
24
|
+
warn 'Rubocop gem not installed, now the code will look like crap!'
|
25
|
+
end
|
26
|
+
|
27
|
+
desc 'Run all of the quick tests.'
|
28
|
+
task :quick do
|
29
|
+
Rake::Task['test:rubocop'].invoke
|
30
|
+
Rake::Task['test:spec'].invoke
|
31
|
+
end
|
32
|
+
end
|
data/build_etcd
ADDED
data/jn_services.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
|
5
|
+
require 'services/version'
|
6
|
+
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.name = "jn_services"
|
9
|
+
spec.version = Services::VERSION
|
10
|
+
spec.authors = ["Jesse Nelson"]
|
11
|
+
spec.email = ["spheromak@gmail.com"]
|
12
|
+
spec.description = %q{Consitently model servives with etcd in and outside chef}
|
13
|
+
spec.summary = spec.description
|
14
|
+
spec.homepage = "https://github.com/spheromak/services"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
spec.files = `git ls-files`.split($/)
|
18
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
19
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
20
|
+
spec.require_paths = ["lib"]
|
21
|
+
|
22
|
+
spec.add_development_dependency "etcd"
|
23
|
+
spec.add_development_dependency "bundler"
|
24
|
+
spec.add_development_dependency "rake"
|
25
|
+
spec.add_development_dependency "rspec"
|
26
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
module Services
|
2
|
+
#
|
3
|
+
# Setup ETCD connection via chef or plain host
|
4
|
+
# Also stores that aconnection in Services.connection
|
5
|
+
# Most other classes require this to be setup
|
6
|
+
#
|
7
|
+
class Connection
|
8
|
+
require 'openssl'
|
9
|
+
|
10
|
+
attr_reader :node, :run_context, :client, :host, :port, :ssl_verify
|
11
|
+
|
12
|
+
if defined?(Chef) == 'constant' && Chef.class == Class
|
13
|
+
if Chef::Version.new(Chef::VERSION) <= Chef::Version.new('11.0.0')
|
14
|
+
include ::Chef::Mixin::Language
|
15
|
+
else
|
16
|
+
include ::Chef::DSL::DataQuery
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
#
|
21
|
+
# Initialize etcd client
|
22
|
+
#
|
23
|
+
# You should pass either a run_context or explicit host/port arguments
|
24
|
+
# the run_context will take prescedence
|
25
|
+
#
|
26
|
+
# @param [Hash] options
|
27
|
+
# @option args [Chef::RunContext] :run_context (nil)
|
28
|
+
# The chef run context to find things in
|
29
|
+
# @option args [String] :host (nil) The host address to connect too
|
30
|
+
# @option args [String] :port (4001) The etcd port to connect too
|
31
|
+
def initialize(args)
|
32
|
+
@run_context = args.fetch(:run_context, nil)
|
33
|
+
Services.run_context = args[:run_context]
|
34
|
+
@node = args[:run_context].node if run_context
|
35
|
+
@host = args[:host]
|
36
|
+
@port = args[:port] || 4001
|
37
|
+
@ssl_verify = args[:verify] || OpenSSL::SSL::VERIFY_NONE
|
38
|
+
|
39
|
+
validate
|
40
|
+
load_gem
|
41
|
+
Services.connection = get_connection(find_servers)
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
#
|
47
|
+
# Validate args passeed in on initialize
|
48
|
+
#
|
49
|
+
# We require run_context OR host to function
|
50
|
+
#
|
51
|
+
def validate
|
52
|
+
unless run_context || host
|
53
|
+
fail ArgumentError, 'Must provide a run_context OR host to initialize'
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
#
|
58
|
+
# Lazily Load the gem requirement so we can run inside chef
|
59
|
+
#
|
60
|
+
# If @run_context exists it will use that to install the gem via
|
61
|
+
# Chefs chef_gem resource
|
62
|
+
#
|
63
|
+
def load_gem
|
64
|
+
require 'etcd'
|
65
|
+
rescue LoadError
|
66
|
+
if run_context
|
67
|
+
Chef::Log.info 'etcd gem not found. attempting to install'
|
68
|
+
g = Chef::Resource::ChefGem.new 'etcd', run_context
|
69
|
+
g.version '0.0.6'
|
70
|
+
run_context.resource_collection.insert g
|
71
|
+
g.run_action :install
|
72
|
+
require 'etcd'
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
#
|
77
|
+
# Find other Etd Servers by looking at node attributes or via Chef Search
|
78
|
+
#
|
79
|
+
def find_servers
|
80
|
+
# need a run_context to find anything in
|
81
|
+
return nil unless run_context
|
82
|
+
# If there are already servers in attribs use those
|
83
|
+
return node[:etcd][:servers] if node.key?(:etcd) &&
|
84
|
+
node[:etcd].key?(:servers)
|
85
|
+
|
86
|
+
# if we have already searched in this run use those
|
87
|
+
return node.run_state[:etcd_servers] if node.run_state.key? :etcd_servers
|
88
|
+
|
89
|
+
# find nodes and build array of ip's
|
90
|
+
etcd_nodes = search(:node, search_query)
|
91
|
+
servers = etcd_nodes.map { |n| n[:ipaddress] }
|
92
|
+
|
93
|
+
# store that in the run_state
|
94
|
+
node.run_state[:etcd_servers] = servers
|
95
|
+
end
|
96
|
+
|
97
|
+
#
|
98
|
+
# Setup proper chef search term for other etcd boxen
|
99
|
+
#
|
100
|
+
# Will search for known recipe in run_list or specified term
|
101
|
+
#
|
102
|
+
def search_query
|
103
|
+
query = "(chef_environment:#{node.chef_environment} "
|
104
|
+
query << 'AND recipes:etcd) '
|
105
|
+
if node[:etcd][:recipe]
|
106
|
+
query << "OR (chef_environment:#{node.chef_environment} "
|
107
|
+
query << "AND #{node[:etcd][:search_term]})"
|
108
|
+
end
|
109
|
+
query
|
110
|
+
end
|
111
|
+
|
112
|
+
#
|
113
|
+
# connect to ip/port and store in @@client
|
114
|
+
# If given an arry of servers then try each until we
|
115
|
+
# connect
|
116
|
+
# TODO: refactor
|
117
|
+
# rubocop:disable MethodLength
|
118
|
+
def get_connection(servers = nil)
|
119
|
+
c = nil
|
120
|
+
if servers
|
121
|
+
servers.each do |s|
|
122
|
+
c = try_connect(s)
|
123
|
+
break if c
|
124
|
+
end
|
125
|
+
else
|
126
|
+
c = try_connect host
|
127
|
+
end
|
128
|
+
fail 'Unable to get a valid connection to Etcd' unless c
|
129
|
+
c
|
130
|
+
end
|
131
|
+
|
132
|
+
#
|
133
|
+
# Try to grab an etcd connection
|
134
|
+
#
|
135
|
+
# @param [String] server () The server to try to connect too
|
136
|
+
#
|
137
|
+
def try_connect(server)
|
138
|
+
c = ::Etcd.client(host: server, port: port)
|
139
|
+
begin
|
140
|
+
c.get '/_etcd/machines'
|
141
|
+
return c
|
142
|
+
rescue
|
143
|
+
puts "ETCD: failed to connect to #{c.host}:#{c.port}"
|
144
|
+
return nil
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# Services::Endpoint
|
2
|
+
# The VIP of the service. This class describes where an endpoint lives
|
3
|
+
#
|
4
|
+
module Services
|
5
|
+
require_relative 'entity'
|
6
|
+
|
7
|
+
# endpoint describes a VIP ip
|
8
|
+
class Endpoint < Services::Entity
|
9
|
+
attr_accessor :ip, :port, :proto
|
10
|
+
def initialize(name, args = {})
|
11
|
+
@ip = args[:ip] || ''
|
12
|
+
@proto = args[:proto] || 'http'
|
13
|
+
@port = args[:port] || 80
|
14
|
+
@path = "#{name}/endpoint"
|
15
|
+
super
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def validate
|
21
|
+
fail 'endpont requires a service name' unless name
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
#
|
2
|
+
# Services::Entity:
|
3
|
+
# This is the generic 'entity' for anything you want to attach to a service.
|
4
|
+
# Inherit this for any other type a service may have.
|
5
|
+
module Services
|
6
|
+
require_relative '../services'
|
7
|
+
|
8
|
+
# entity base class.
|
9
|
+
# member,service,endpoint are all "services::entity"
|
10
|
+
class Entity
|
11
|
+
attr_reader :name, :path
|
12
|
+
def initialize(name, args = {})
|
13
|
+
@name = name
|
14
|
+
validate
|
15
|
+
end
|
16
|
+
|
17
|
+
def store
|
18
|
+
_store
|
19
|
+
end
|
20
|
+
alias_method :save, :store
|
21
|
+
|
22
|
+
def load
|
23
|
+
_load
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_hash
|
27
|
+
vars = {}
|
28
|
+
instance_variables.each do |name|
|
29
|
+
key = name[1..-1].to_s
|
30
|
+
# don't store "path"
|
31
|
+
next if key == 'path'
|
32
|
+
vars[key] = instance_variable_get(name).to_s
|
33
|
+
end
|
34
|
+
vars
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def validate
|
40
|
+
fail 'This class should be extended. Not used directly' unless path
|
41
|
+
end
|
42
|
+
|
43
|
+
def _store
|
44
|
+
to_hash.each do |k, v|
|
45
|
+
next if k == 'name'
|
46
|
+
# etcd doesn't like nil
|
47
|
+
v ||= ''
|
48
|
+
Services.set "#{KEY}/#{path}/#{k}", v
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def _load
|
53
|
+
return unless valid_path
|
54
|
+
to_hash.each do |k, v|
|
55
|
+
next if k == 'name'
|
56
|
+
value = Services.get("#{KEY}/#{path}/#{k}").value
|
57
|
+
instance_variable_set "@#{k}", value
|
58
|
+
end
|
59
|
+
self
|
60
|
+
end
|
61
|
+
|
62
|
+
def valid_path
|
63
|
+
begin
|
64
|
+
Services.get("#{KEY}/#{path}")
|
65
|
+
rescue
|
66
|
+
return false
|
67
|
+
end
|
68
|
+
true
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require_relative 'entity'
|
2
|
+
|
3
|
+
module Services
|
4
|
+
# This is a service member usually something that
|
5
|
+
# would be sitting behind some VIP
|
6
|
+
class Member < Services::Entity
|
7
|
+
attr_accessor :ip, :port, :proto, :service, :weight
|
8
|
+
def initialize(name, args = {})
|
9
|
+
@ip = args[:ip] || ''
|
10
|
+
@proto = args[:proto] || 'http'
|
11
|
+
@port = args[:port] || 80
|
12
|
+
@weight = args[:weight] || 20
|
13
|
+
@service = args[:service]
|
14
|
+
@path = "#{service}/members/#{name}"
|
15
|
+
super
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def validate
|
21
|
+
unless @service
|
22
|
+
fail ArgumentError,
|
23
|
+
"#{self.class} requires service: argument"
|
24
|
+
end
|
25
|
+
|
26
|
+
unless name
|
27
|
+
fail ARgumentError,
|
28
|
+
"#{self.class} requires name argument"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# Services::Service
|
2
|
+
# this is the almalgamate service class which allows you to load a service
|
3
|
+
# and it's endpoint/members
|
4
|
+
#
|
5
|
+
# TODO: Allow arbitrary entity loading.
|
6
|
+
#
|
7
|
+
module Services
|
8
|
+
require_relative 'connection'
|
9
|
+
require_relative 'endpoint'
|
10
|
+
require_relative 'member'
|
11
|
+
|
12
|
+
# service container
|
13
|
+
class Service
|
14
|
+
attr_reader :name
|
15
|
+
attr_reader :members
|
16
|
+
attr_reader :endpoint
|
17
|
+
|
18
|
+
def initialize(name)
|
19
|
+
@name = name
|
20
|
+
@members = []
|
21
|
+
@endpoint = Services::Endpoint.new name
|
22
|
+
|
23
|
+
create_if_missing
|
24
|
+
load_members
|
25
|
+
load_endpoint
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def create_if_missing
|
31
|
+
Services.get "#{KEY}/#{name}"
|
32
|
+
rescue Net::HTTPServerException => e
|
33
|
+
Services.set "#{KEY}/#{name}/_created", Time.now if e.message.match 'Not Found'
|
34
|
+
end
|
35
|
+
|
36
|
+
def load_endpoint
|
37
|
+
endpoint.load
|
38
|
+
endpoint
|
39
|
+
end
|
40
|
+
|
41
|
+
# rubocop:disable MethodLength
|
42
|
+
def load_members
|
43
|
+
begin
|
44
|
+
etcd_members = Services.get "#{KEY}/#{name}/members"
|
45
|
+
rescue Net::HTTPServerException => e
|
46
|
+
etcd_members = nil if e.message.match 'Not Found'
|
47
|
+
end
|
48
|
+
|
49
|
+
unless etcd_members.nil? || etcd_members.empty?
|
50
|
+
etcd_members.node.nodes.each do |m|
|
51
|
+
m_name = File.basename m['key']
|
52
|
+
m1 = Services::Member.new(m_name, service: name)
|
53
|
+
m1.load
|
54
|
+
@members.push m1
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
data/lib/services.rb
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
#
|
2
|
+
# Services Module
|
3
|
+
#
|
4
|
+
# Uses etcd to manage state of Service Endpoint & Members
|
5
|
+
#
|
6
|
+
module Services
|
7
|
+
require_relative 'services/version'
|
8
|
+
require_relative 'services/connection'
|
9
|
+
require_relative 'services/entity'
|
10
|
+
require_relative 'services/service'
|
11
|
+
require_relative 'services/endpoint'
|
12
|
+
require_relative 'services/member'
|
13
|
+
|
14
|
+
# this will change or be slurped up from a config/node attrib
|
15
|
+
KEY = '/services'
|
16
|
+
|
17
|
+
#
|
18
|
+
# Share a connection between all classess using this module
|
19
|
+
#
|
20
|
+
class << self
|
21
|
+
attr_accessor :connection, :run_context
|
22
|
+
|
23
|
+
def get(*args)
|
24
|
+
Chef::Log.debug "connection.get args #{args}" unless run_context.nil?
|
25
|
+
connection.get(*args)
|
26
|
+
end
|
27
|
+
|
28
|
+
def set(*args)
|
29
|
+
Chef::Log.debug "connection.set args #{args}" unless run_context.nil?
|
30
|
+
connection.set(*args)
|
31
|
+
end
|
32
|
+
|
33
|
+
# return a list of all services
|
34
|
+
def all
|
35
|
+
services = []
|
36
|
+
get(KEY).each do |s|
|
37
|
+
name = File.basename s.key
|
38
|
+
puts "loading #{name}"
|
39
|
+
services << Services::Service.new(name)
|
40
|
+
end
|
41
|
+
services
|
42
|
+
end
|
43
|
+
|
44
|
+
# return all services a node is subscribed to
|
45
|
+
def subscribed(f = nil)
|
46
|
+
fail 'param and run_context can not both be nil' if f.nil? && run_context.nil?
|
47
|
+
|
48
|
+
fqdn = f.nil? ? rteraun_context.node.fqdn : f
|
49
|
+
services = all.map { |s| s.members.include?(fqdn) ? s : nil }
|
50
|
+
services.compact
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
data/spec/.rubocop.yml
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
describe 'Services::Endpoint' do
|
2
|
+
before(:each) do
|
3
|
+
@ep_data = { 'name' => 'test', 'ip' => '127.0.0.1', 'proto' => 'http', 'port' => '80' }
|
4
|
+
Services::Connection.new host: 'localhost'
|
5
|
+
@ep = Services::Endpoint.new 'test', ip: '127.0.0.1', port: 80
|
6
|
+
end
|
7
|
+
|
8
|
+
it 'should raise without a service name' do
|
9
|
+
expect { Services::Endpoint.new }.to raise_error
|
10
|
+
end
|
11
|
+
|
12
|
+
describe '#to_hash' do
|
13
|
+
it 'should return each pair of insance vars' do
|
14
|
+
@ep.to_hash.should eql @ep_data
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
describe '#store' do
|
19
|
+
it 'should store' do
|
20
|
+
puts "storing #{@ep.inspect}"
|
21
|
+
@ep.store
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe '#load' do
|
26
|
+
before do
|
27
|
+
@ep = Services::Endpoint.new 'test'
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'should handle no endpoint' do
|
31
|
+
e = Services::Endpoint.new 'test_endpoint_fail'
|
32
|
+
e.load
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'should load' do
|
36
|
+
@ep.load
|
37
|
+
@ep.to_hash.should eql @ep_data
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'should get ip' do
|
41
|
+
@ep.ip.should eql ''
|
42
|
+
@ep.load
|
43
|
+
@ep.ip.should eql '127.0.0.1'
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
data/spec/member_spec.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
describe 'Services::Member' do
|
2
|
+
before(:each) do
|
3
|
+
@m_data = { 'ip' => '127.0.0.2', 'proto' => 'http', 'port' => '80', 'weight' => '20', 'service' => 'test', 'name' => 'test_member' }
|
4
|
+
Services::Connection.new host: 'localhost'
|
5
|
+
@m = Services::Member.new 'test_member', service: 'test', ip: '127.0.0.2', port: 80, weight: 20
|
6
|
+
@m2 = Services::Member.new 'test_member2', service: 'test', ip: '127.0.0.3', port: 80, weight: 20
|
7
|
+
end
|
8
|
+
|
9
|
+
it 'should raise without a service name' do
|
10
|
+
expect { Services::Member.new 'foo' }.to raise_error
|
11
|
+
end
|
12
|
+
|
13
|
+
describe '#to_hash' do
|
14
|
+
it 'should return each pair of insance vars' do
|
15
|
+
@m.to_hash.should == @m_data
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe '#store' do
|
20
|
+
it 'should store' do
|
21
|
+
@m.store
|
22
|
+
@m2.store
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe '#load' do
|
27
|
+
before do
|
28
|
+
@m = Services::Member.new 'test_member', service: 'test'
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'should load' do
|
32
|
+
@m.load
|
33
|
+
@m.to_hash.should == @m_data
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require_relative 'spec_helper'
|
2
|
+
describe 'Services::Service' do
|
3
|
+
let(:c) { Services::Connection.new host: 'localhost' }
|
4
|
+
let(:m1) { Services::Member.new 'test_member', service: 'test', ip: '127.0.0.2', port: 80, weight: 20 }
|
5
|
+
let(:m2) { Services::Member.new 'test_member2', service: 'test', ip: '127.0.0.3', port: 80, weight: 20 }
|
6
|
+
|
7
|
+
it 'should load the test service' do
|
8
|
+
Services::Connection.new host: 'localhost'
|
9
|
+
members = Services::Service.new('test').members
|
10
|
+
members.length.should eql 2
|
11
|
+
members[0].to_hash.should eql m1.to_hash
|
12
|
+
members[1].to_hash.should eql m2.to_hash
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'should handle non-existant services' do
|
16
|
+
expect { Services::Service.new 'should_not_exist' }.to_not raise_error
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require_relative 'spec_helper.rb'
|
2
|
+
|
3
|
+
describe 'Services' do
|
4
|
+
describe '::Connection' do
|
5
|
+
it 'should setup' do
|
6
|
+
Services::Connection.new host: 'localhost'
|
7
|
+
end
|
8
|
+
|
9
|
+
it 'should fail without something to connect too' do
|
10
|
+
expect { Services::Connection.new }.to raise_error
|
11
|
+
end
|
12
|
+
|
13
|
+
# chef functions in chef-spec specs ?
|
14
|
+
# it "should accept a chef run_context" do
|
15
|
+
# Services::Connection.new(
|
16
|
+
# run_context: Chef::RunContext.new(
|
17
|
+
# Chef::Node.new,
|
18
|
+
# Chef::CookbookCollection.new,
|
19
|
+
# Chef::EventDispatch::Dispatcher.new
|
20
|
+
# )
|
21
|
+
# )
|
22
|
+
# end
|
23
|
+
end
|
24
|
+
|
25
|
+
before(:each) do
|
26
|
+
Services::Connection.new host: 'localhost'
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'can set' do
|
30
|
+
Services.set '/test/1', 1
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'can get' do
|
34
|
+
Services.get '/_etcd/machines'
|
35
|
+
end
|
36
|
+
|
37
|
+
describe '::Entity' do
|
38
|
+
before(:each) do
|
39
|
+
Services::Connection.new host: 'localhost'
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'should raise when directly instanced' do
|
43
|
+
expect { Services::Entity.new('foo') }.to raise_error(RuntimeError)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
require 'simplecov'
|
2
|
+
SimpleCov.start
|
3
|
+
require_relative '../lib/services.rb'
|
4
|
+
|
5
|
+
require 'uuid'
|
6
|
+
require 'etcd'
|
7
|
+
# thanks @ranjib for this handy helper
|
8
|
+
module Etcd
|
9
|
+
# helpers to start an etcd cluster
|
10
|
+
# rubocop:disable ClassVars
|
11
|
+
module SpecHelper
|
12
|
+
@@pids = []
|
13
|
+
|
14
|
+
def self.etcd_binary
|
15
|
+
if File.exists? './etcd/etcd'
|
16
|
+
'./etcd/etcd'
|
17
|
+
elsif !!ENV['ETCD_BIN']
|
18
|
+
ENV['ETCD_BIN']
|
19
|
+
elsif File.exists? '/usr/local/bin/etcd'
|
20
|
+
'/usr/local/bin/etcd'
|
21
|
+
else
|
22
|
+
fail 'etcd binary not found., you need to set ETCD_BIN'
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.start_etcd_servers
|
27
|
+
@@tmpdir = Dir.mktmpdir
|
28
|
+
pid = spawn_etcd_server(@@tmpdir + '/leader')
|
29
|
+
@@pids = Array(pid)
|
30
|
+
leader = '127.0.0.1:7001'
|
31
|
+
4.times do |n|
|
32
|
+
client_port = 4002 + n
|
33
|
+
server_port = 7002 + n
|
34
|
+
pid = spawn_etcd_server(@@tmpdir + client_port.to_s, client_port, server_port, leader)
|
35
|
+
@@pids << pid
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.stop_etcd_servers
|
40
|
+
@@pids.each do |pid|
|
41
|
+
Process.kill('TERM', pid)
|
42
|
+
end
|
43
|
+
FileUtils.remove_entry_secure(@@tmpdir, true)
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.spawn_etcd_server(dir, client_port = 4001, server_port = 7001, leader = nil)
|
47
|
+
args = " -addr 127.0.0.1:#{client_port} -peer-addr 127.0.0.1:#{server_port} -data-dir #{dir} -name node_#{client_port}"
|
48
|
+
command = if leader.nil?
|
49
|
+
etcd_binary + args
|
50
|
+
else
|
51
|
+
etcd_binary + args + " -peers #{leader}"
|
52
|
+
end
|
53
|
+
pid = spawn(command, out: '/dev/null')
|
54
|
+
Process.detach(pid)
|
55
|
+
sleep 1
|
56
|
+
pid
|
57
|
+
end
|
58
|
+
|
59
|
+
def uuid
|
60
|
+
@uuid ||= UUID.new
|
61
|
+
end
|
62
|
+
|
63
|
+
def random_key(n = 1)
|
64
|
+
key = ''
|
65
|
+
n.times do
|
66
|
+
key << '/' + uuid.generate
|
67
|
+
end
|
68
|
+
key
|
69
|
+
end
|
70
|
+
|
71
|
+
def etcd_servers
|
72
|
+
(1..5).map { |n| "http://127.0.0.1:700#{n}" }
|
73
|
+
end
|
74
|
+
|
75
|
+
def other_client
|
76
|
+
Etcd.client
|
77
|
+
end
|
78
|
+
|
79
|
+
def read_only_client
|
80
|
+
Etcd.client(allow_redirect: false, port: 4004)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
RSpec.configure do |config|
|
86
|
+
|
87
|
+
config.include Etcd::SpecHelper
|
88
|
+
|
89
|
+
config.before(:suite) do
|
90
|
+
Etcd::SpecHelper.start_etcd_servers
|
91
|
+
end
|
92
|
+
|
93
|
+
config.after(:suite) do
|
94
|
+
Etcd::SpecHelper.stop_etcd_servers
|
95
|
+
end
|
96
|
+
# Use color in STDOUT
|
97
|
+
config.color_enabled = true
|
98
|
+
|
99
|
+
# Use color not only in STDOUT but also in pagers and files
|
100
|
+
config.tty = true
|
101
|
+
|
102
|
+
# Use the specified formatter
|
103
|
+
config.formatter = :documentation # :progress, :html, :textmate
|
104
|
+
end
|
metadata
ADDED
@@ -0,0 +1,130 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: jn_services
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jesse Nelson
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-02-20 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: etcd
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - '>='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
description: Consitently model servives with etcd in and outside chef
|
70
|
+
email:
|
71
|
+
- spheromak@gmail.com
|
72
|
+
executables: []
|
73
|
+
extensions: []
|
74
|
+
extra_rdoc_files: []
|
75
|
+
files:
|
76
|
+
- .coco.yml
|
77
|
+
- .gitignore
|
78
|
+
- .rubocop.yml
|
79
|
+
- .travis.yml
|
80
|
+
- CHANGELOG.md
|
81
|
+
- Gemfile
|
82
|
+
- LICENSE.txt
|
83
|
+
- README.md
|
84
|
+
- Rakefile
|
85
|
+
- build_etcd
|
86
|
+
- jn_services.gemspec
|
87
|
+
- lib/services.rb
|
88
|
+
- lib/services/connection.rb
|
89
|
+
- lib/services/endpoint.rb
|
90
|
+
- lib/services/entity.rb
|
91
|
+
- lib/services/member.rb
|
92
|
+
- lib/services/service.rb
|
93
|
+
- lib/services/version.rb
|
94
|
+
- spec/.rubocop.yml
|
95
|
+
- spec/endpoint_spec.rb
|
96
|
+
- spec/member_spec.rb
|
97
|
+
- spec/service_spec.rb
|
98
|
+
- spec/services_spec.rb
|
99
|
+
- spec/spec_helper.rb
|
100
|
+
homepage: https://github.com/spheromak/services
|
101
|
+
licenses:
|
102
|
+
- MIT
|
103
|
+
metadata: {}
|
104
|
+
post_install_message:
|
105
|
+
rdoc_options: []
|
106
|
+
require_paths:
|
107
|
+
- lib
|
108
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
109
|
+
requirements:
|
110
|
+
- - '>='
|
111
|
+
- !ruby/object:Gem::Version
|
112
|
+
version: '0'
|
113
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - '>='
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
requirements: []
|
119
|
+
rubyforge_project:
|
120
|
+
rubygems_version: 2.0.14
|
121
|
+
signing_key:
|
122
|
+
specification_version: 4
|
123
|
+
summary: Consitently model servives with etcd in and outside chef
|
124
|
+
test_files:
|
125
|
+
- spec/.rubocop.yml
|
126
|
+
- spec/endpoint_spec.rb
|
127
|
+
- spec/member_spec.rb
|
128
|
+
- spec/service_spec.rb
|
129
|
+
- spec/services_spec.rb
|
130
|
+
- spec/spec_helper.rb
|