jn_services 1.0.0
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.
- 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
|