amoeba_deploy_tools 0.0.1 → 0.0.2
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 +4 -4
- data/.gitignore +34 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +83 -0
- data/Rakefile +1 -0
- data/amoeba-deploy-tools.gemspec +36 -0
- data/lib/amoeba_deploy_tools/capistrano/common.rb +5 -0
- data/lib/amoeba_deploy_tools/capistrano/recipes.rb +14 -0
- data/lib/amoeba_deploy_tools/capistrano.rb +11 -0
- data/lib/amoeba_deploy_tools/command.rb +105 -0
- data/lib/amoeba_deploy_tools/commands/amoeba.rb +118 -0
- data/lib/amoeba_deploy_tools/commands/app.rb +27 -0
- data/lib/amoeba_deploy_tools/commands/concerns/hooks.rb +45 -0
- data/lib/amoeba_deploy_tools/commands/concerns/ssh.rb +109 -0
- data/lib/amoeba_deploy_tools/commands/key.rb +22 -0
- data/lib/amoeba_deploy_tools/commands/node.rb +119 -0
- data/lib/amoeba_deploy_tools/commands/ssl.rb +45 -0
- data/lib/amoeba_deploy_tools/config.rb +133 -0
- data/lib/amoeba_deploy_tools/data_bag.rb +30 -0
- data/lib/amoeba_deploy_tools/helpers.rb +51 -0
- data/lib/amoeba_deploy_tools/interactive_cocaine_runner.rb +27 -0
- data/lib/amoeba_deploy_tools/logger.rb +67 -0
- data/lib/amoeba_deploy_tools/noisey_cocaine_runner.rb +42 -0
- data/lib/amoeba_deploy_tools/version.rb +3 -0
- data/spec/amoeba_deploy_tools/command_spec.rb +4 -0
- data/spec/amoeba_deploy_tools/commands_spec.rb +91 -0
- data/spec/amoeba_deploy_tools/config_spec.rb +48 -0
- data/spec/amoeba_deploy_tools/helpers_spec.rb +18 -0
- data/spec/spec_helper.rb +21 -0
- metadata +37 -2
@@ -0,0 +1,119 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module AmoebaDeployTools
|
4
|
+
class Node < Command
|
5
|
+
include AmoebaDeployTools::Concerns::SSH
|
6
|
+
|
7
|
+
desc 'bootstrap', 'Setup the node initially (run the first time you want to deploy a node)'
|
8
|
+
long_desc <<-LONGDESC
|
9
|
+
`bootstrap` will load Chef and the `basenode` recipe on the node. This is effectively the way
|
10
|
+
to setup a node initially. Subsequent runs can use the `node push` command.
|
11
|
+
LONGDESC
|
12
|
+
option :version, desc: 'Chef version to bootstrap', default: '11.8.2'
|
13
|
+
option :interactive, desc: 'Run interactively (useful for entering passwords)', type: :boolean, default: false
|
14
|
+
def bootstrap
|
15
|
+
logger.info 'Starting `bootstrap`!'
|
16
|
+
|
17
|
+
refresh
|
18
|
+
knife_solo :prepare, 'bootstrap-version' => options[:version], ssh: true, interactive: options[:interactive]
|
19
|
+
|
20
|
+
knife_solo :cook, ssh: true, include_private_key: true, interactive: options[:interactive] do |j|
|
21
|
+
j.run_list = ['role[base]']
|
22
|
+
end
|
23
|
+
|
24
|
+
force_deployer
|
25
|
+
|
26
|
+
pull
|
27
|
+
|
28
|
+
logger.warn 'Node bootstrapped successfully, you can now push to the node:'
|
29
|
+
logger.warn "\tamoeba node push --node #{options[:node]}\n"
|
30
|
+
end
|
31
|
+
|
32
|
+
desc 'force_deployer', 'Forces the deploy user to be `deploy` or that specified in node.json'
|
33
|
+
def force_deployer
|
34
|
+
logger.info 'Starting force_deployer'
|
35
|
+
data_bag(:nodes)[node.name] = { deployment: { user: node.depoyment_.user || 'deploy' } }
|
36
|
+
puts "Remote node: #{remote_node}"
|
37
|
+
end
|
38
|
+
|
39
|
+
desc 'push', 'Push any changes to the node'
|
40
|
+
def push
|
41
|
+
logger.info 'Starting push...'
|
42
|
+
refresh
|
43
|
+
knife_solo :cook, ssh: true, include_private_key: true
|
44
|
+
pull
|
45
|
+
end
|
46
|
+
|
47
|
+
desc 'pull', 'Pull down node state and store in local node databag (run automatically after push)'
|
48
|
+
def pull
|
49
|
+
logger.info 'Starting `pull`!'
|
50
|
+
force_deployer unless remote_node.deployment_.user
|
51
|
+
|
52
|
+
raw_json = ssh_run('sudo cat ~deploy/node.json', silent: true)
|
53
|
+
data_bag(:nodes)[node.name] = JSON.load raw_json
|
54
|
+
end
|
55
|
+
|
56
|
+
desc 'list', 'Show available nodes in kitchen'
|
57
|
+
def list
|
58
|
+
inside_kitchen do
|
59
|
+
puts Dir.glob('nodes/*.json').sort.map {|n| File.basename(n).sub(/\.json$/, '')}
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
desc 'refresh', 'Refresh data bags based on node config. Note this is normally run automatically.'
|
64
|
+
long_desc <<-DESC
|
65
|
+
Normally, you should not need to run `refresh`. It is run automatically before every `push` or
|
66
|
+
`bootstrap`. This command prepares data bags for the node. Presently, this is only used for
|
67
|
+
SSH keys. Thus, `refresh` will go through all the `authorized_keys` folders and generate
|
68
|
+
data_bags for each user. These data_bags are then used by the Chef Cookbooks during pushes.
|
69
|
+
DESC
|
70
|
+
def refresh
|
71
|
+
logger.info "Starting `refresh`!"
|
72
|
+
inside_kitchen do
|
73
|
+
# Handle authorized_keys
|
74
|
+
logger.debug '# Refreshing authorized_keys'
|
75
|
+
Dir.glob('authorized_keys/*') do |user_dir|
|
76
|
+
if File.directory? user_dir
|
77
|
+
user_name = File.basename(user_dir)
|
78
|
+
logger.info "Processing SSH keys for user #{user_name}."
|
79
|
+
user_bag = data_bag(:authorized_keys)[user_name]
|
80
|
+
user_bag[:keys] = []
|
81
|
+
|
82
|
+
Dir.glob(File.join(user_dir, '*')) do |key_file|
|
83
|
+
logger.debug "Reading key file: #{key_file}"
|
84
|
+
user_bag[:keys] << File.read(key_file).strip
|
85
|
+
end
|
86
|
+
|
87
|
+
logger.info "Writing #{user_bag.options[:filename]}"
|
88
|
+
user_bag.save
|
89
|
+
else
|
90
|
+
logger.info "Ignoring file in authorized_keys (must be inside a directory): #{f}"
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
logger.debug '# Ensuring bundle is up to date'
|
95
|
+
# Handle bundler, ensure it's up to date
|
96
|
+
unless system('bundle check > /dev/null 2>&1')
|
97
|
+
logger.info "Bundle out of date! Running bundle update..."
|
98
|
+
Cocaine::CommandLine.new('bundle', 'install').run
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
desc 'exec "CMD"', 'Execute given command (via SSH) on node, as deploy user (can sudo)'
|
104
|
+
def exec(cmd)
|
105
|
+
ssh_run(cmd, interactive: true)
|
106
|
+
end
|
107
|
+
|
108
|
+
desc 'ssh', 'SSHs to the node as the deploy user (can sudo)'
|
109
|
+
def ssh
|
110
|
+
exec nil
|
111
|
+
end
|
112
|
+
|
113
|
+
desc 'sudo "CMD"', 'Executes the given command as root on the node'
|
114
|
+
def sudo(cmd)
|
115
|
+
# pull args off of cmd
|
116
|
+
exec("sudo #{cmd}")
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module AmoebaDeployTools
|
2
|
+
class Ssl < Command
|
3
|
+
# Needed for knife solo stuff
|
4
|
+
include AmoebaDeployTools::Concerns::SSH
|
5
|
+
|
6
|
+
desc 'import', 'Import an SSL certificate and add to encrypted data bag (encryption key `default` used unless specified)'
|
7
|
+
option :privateKey, desc: 'Name of the private key to use. Will create if missing', default: 'default'
|
8
|
+
option :key, desc: 'SSL private key file name / path', required: true
|
9
|
+
option :cert, desc: 'SSL public certificate file name / path', required: true
|
10
|
+
option :ca, desc: 'SSL intermediary CA certificate name / path'
|
11
|
+
def import(cert_name=nil)
|
12
|
+
logger.debug "Starting SSL import!"
|
13
|
+
validate_chef_id!(cert_name)
|
14
|
+
|
15
|
+
private_key = options[:privateKey]
|
16
|
+
|
17
|
+
json_data = { id: cert_name }
|
18
|
+
[:key, :cert, :ca].each do |c|
|
19
|
+
# read certificates before we get in the kitchen
|
20
|
+
if File.exist? options[c]
|
21
|
+
json_data[c] = File.read(options[c])
|
22
|
+
else
|
23
|
+
logger.error "Cannot find certificate file to import (ignoring): #{options[c]}"
|
24
|
+
options[c] = nil
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Ensure key exists
|
29
|
+
unless config.private_keys_[private_key]
|
30
|
+
logger.warn "Private key missing: #{options[:privateKey]}, running `amoeba key create #{options[:privateKey]}`"
|
31
|
+
AmoebaDeployTools::Key.new.create(options[:privateKey])
|
32
|
+
end
|
33
|
+
|
34
|
+
inside_kitchen do
|
35
|
+
# Import to certs databag
|
36
|
+
with_tmpfile( json_data.to_json, name: [cert_name, '.json'] ) do |file_name|
|
37
|
+
knife_solo "data bag create certs #{cert_name}",
|
38
|
+
'json-file' => file_name,
|
39
|
+
'secret' => "'#{config.reload!.private_keys[private_key]}'"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'json'
|
3
|
+
require 'fileutils'
|
4
|
+
require 'hashie/mash'
|
5
|
+
|
6
|
+
module AmoebaDeployTools
|
7
|
+
class Config < Hashie::Mash
|
8
|
+
def self.load(filename, opts={})
|
9
|
+
opts[:filename] = File.expand_path filename
|
10
|
+
Config.new.tap do |c|
|
11
|
+
c.restore(opts)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.create(filename, opts={})
|
16
|
+
opts.merge!({
|
17
|
+
filename: File.expand_path(filename),
|
18
|
+
create: true
|
19
|
+
})
|
20
|
+
|
21
|
+
Config.new.tap do |c|
|
22
|
+
c.options(opts)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def options(opts=nil)
|
27
|
+
@opts ||= { format: :yaml }
|
28
|
+
@opts.merge! opts if opts
|
29
|
+
@opts
|
30
|
+
end
|
31
|
+
|
32
|
+
def restore(opts=nil)
|
33
|
+
options(opts)
|
34
|
+
|
35
|
+
return unless filename = options[:filename]
|
36
|
+
|
37
|
+
self.clear.deep_merge! deserialize(File.read(filename))
|
38
|
+
|
39
|
+
self
|
40
|
+
rescue Errno::ENOENT
|
41
|
+
@new_file = true
|
42
|
+
FileUtils.touch(filename) and retry if options[:create]
|
43
|
+
end
|
44
|
+
|
45
|
+
def reload!
|
46
|
+
restore(filename: options[:filename])
|
47
|
+
end
|
48
|
+
|
49
|
+
def new_file?
|
50
|
+
!!@new_file
|
51
|
+
end
|
52
|
+
|
53
|
+
def save(opts=nil)
|
54
|
+
options(opts)
|
55
|
+
|
56
|
+
return unless filename = options[:filename]
|
57
|
+
|
58
|
+
File.open(filename, 'w') do |fh|
|
59
|
+
fh.write(serialize(self.to_hash))
|
60
|
+
end
|
61
|
+
|
62
|
+
self
|
63
|
+
rescue Errno::ENOENT
|
64
|
+
FileUtils.touch(filename) and retry if options[:create]
|
65
|
+
end
|
66
|
+
|
67
|
+
def [](k)
|
68
|
+
chain = k.to_s.split('.')
|
69
|
+
cur = self
|
70
|
+
|
71
|
+
return super if chain.count <= 1
|
72
|
+
|
73
|
+
for c in chain[0..-2]
|
74
|
+
if cur and cur.key? c
|
75
|
+
cur = cur.regular_reader(c)
|
76
|
+
else
|
77
|
+
return
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
cur[chain[-1]]
|
82
|
+
end
|
83
|
+
|
84
|
+
def []=(k, v)
|
85
|
+
chain = k.to_s.split('.')
|
86
|
+
cur = self
|
87
|
+
|
88
|
+
return super if chain.count <= 1
|
89
|
+
|
90
|
+
for c in chain[0..-2]
|
91
|
+
cur = cur.initializing_reader(c)
|
92
|
+
end
|
93
|
+
|
94
|
+
cur[chain[-1]] = v
|
95
|
+
end
|
96
|
+
|
97
|
+
def flatten
|
98
|
+
flat = {}
|
99
|
+
|
100
|
+
each do |k1, v1|
|
101
|
+
if v1.class == self.class
|
102
|
+
v1.flatten.each do |k2, v2|
|
103
|
+
flat["#{k1}.#{k2}"] = v2
|
104
|
+
end
|
105
|
+
else
|
106
|
+
flat[k1] = v1
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
flat
|
111
|
+
end
|
112
|
+
|
113
|
+
|
114
|
+
def to_s
|
115
|
+
to_hash.to_s
|
116
|
+
end
|
117
|
+
|
118
|
+
protected
|
119
|
+
|
120
|
+
@@formats = {
|
121
|
+
json: JSON,
|
122
|
+
yaml: YAML
|
123
|
+
}
|
124
|
+
|
125
|
+
def serialize(d)
|
126
|
+
@@formats[options[:format]].dump(d)
|
127
|
+
end
|
128
|
+
|
129
|
+
def deserialize(d)
|
130
|
+
@@formats[options[:format]].load(d) || {}
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module AmoebaDeployTools
|
4
|
+
class DataBag
|
5
|
+
def initialize(bag, kitchen)
|
6
|
+
@bag_dir = File.join(kitchen, 'data_bags', bag.to_s)
|
7
|
+
Dir.mkdir @bag_dir unless Dir.exists? @bag_dir
|
8
|
+
end
|
9
|
+
|
10
|
+
def []=(id, item)
|
11
|
+
bag_item = DataBagItem.create(item_filename(id), format: :json)
|
12
|
+
bag_item.clear.deep_merge!(item.to_hash)
|
13
|
+
bag_item.id = id
|
14
|
+
bag_item.save
|
15
|
+
end
|
16
|
+
|
17
|
+
def [](id)
|
18
|
+
DataBagItem.load(item_filename(id), format: :json, create: true).tap do |i|
|
19
|
+
i.id = id
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def item_filename(id)
|
24
|
+
File.join(@bag_dir, "#{id}.json")
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class DataBagItem < Config
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'tempfile'
|
2
|
+
require 'pathname'
|
3
|
+
require 'tmpdir'
|
4
|
+
|
5
|
+
def with_tmpfile(content=nil, options={} )
|
6
|
+
name = options[:name] || 'amoebatmp'
|
7
|
+
|
8
|
+
tmpf = Tempfile.new name
|
9
|
+
tmpf.write content
|
10
|
+
tmpf.close
|
11
|
+
|
12
|
+
results = yield tmpf.path, tmpf
|
13
|
+
|
14
|
+
tmpf.unlink
|
15
|
+
results
|
16
|
+
end
|
17
|
+
|
18
|
+
def in_tmpdir
|
19
|
+
Dir.mktmpdir do |tmpd|
|
20
|
+
Dir.chdir tmpd do
|
21
|
+
yield
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def dedent(s)
|
27
|
+
indent = s.split("\n").reject {|l| l =~ /^\s*$/}.map {|l| l.index /\S/ }.min
|
28
|
+
s.sub(/^\n/, '').gsub(/ +$/, '').gsub(/^ {#{indent}}/, '')
|
29
|
+
end
|
30
|
+
|
31
|
+
def indent(s, indent=4)
|
32
|
+
s.gsub(/^/, ' ' * indent)
|
33
|
+
end
|
34
|
+
|
35
|
+
def say_bold(text)
|
36
|
+
say text, :bold
|
37
|
+
end
|
38
|
+
|
39
|
+
def say_fatal(text)
|
40
|
+
say text, :red
|
41
|
+
exit 1
|
42
|
+
end
|
43
|
+
|
44
|
+
class Exception
|
45
|
+
def bt
|
46
|
+
backtrace.map do |l|
|
47
|
+
cwd = Dir.pwd
|
48
|
+
l.start_with?(cwd) ? l.sub(cwd, '.') : l
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# coding: UTF-8
|
2
|
+
require 'pty'
|
3
|
+
|
4
|
+
module AmoebaDeployTools
|
5
|
+
class InteractiveCocaineRunner
|
6
|
+
def self.supported?
|
7
|
+
true
|
8
|
+
end
|
9
|
+
|
10
|
+
def supported?
|
11
|
+
self.class.supported?
|
12
|
+
end
|
13
|
+
|
14
|
+
def call(command, env = {})
|
15
|
+
with_modified_environment(env) do
|
16
|
+
system(command)
|
17
|
+
end
|
18
|
+
''
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def with_modified_environment(env, &block)
|
24
|
+
ClimateControl.modify(env, &block)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module AmoebaDeployTools
|
4
|
+
class Logger < ::Logger
|
5
|
+
module Colors
|
6
|
+
VERSION = '1.0.0'
|
7
|
+
|
8
|
+
NOTHING = '0;0'
|
9
|
+
BLACK = '0;30'
|
10
|
+
RED = '0;31'
|
11
|
+
GREEN = '0;32'
|
12
|
+
BROWN = '0;33'
|
13
|
+
BLUE = '0;34'
|
14
|
+
PURPLE = '0;35'
|
15
|
+
CYAN = '0;36'
|
16
|
+
LIGHT_GRAY = '0;37'
|
17
|
+
DARK_GRAY = '1;30'
|
18
|
+
LIGHT_RED = '1;31'
|
19
|
+
LIGHT_GREEN = '1;32'
|
20
|
+
YELLOW = '1;33'
|
21
|
+
LIGHT_BLUE = '1;34'
|
22
|
+
LIGHT_PURPLE = '1;35'
|
23
|
+
LIGHT_CYAN = '1;36'
|
24
|
+
WHITE = '1;37'
|
25
|
+
|
26
|
+
SCHEMA = {
|
27
|
+
STDOUT => %w[nothing green brown red purple cyan],
|
28
|
+
STDERR => %w[nothing green yellow light_red light_purple light_cyan],
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
class SimpleFormatter < ::Logger::Formatter
|
33
|
+
def call(severity, time, progname, msg)
|
34
|
+
"%s\n" % msg2str(msg)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def format_message(level, *args)
|
39
|
+
if Logger::Colors::SCHEMA[@logdev.dev]
|
40
|
+
color = begin
|
41
|
+
Logger::Colors.const_get \
|
42
|
+
Logger::Colors::SCHEMA[@logdev.dev][Logger.const_get(level.sub "ANY","UNKNOWN")].to_s.upcase
|
43
|
+
rescue NameError
|
44
|
+
"0;0"
|
45
|
+
end
|
46
|
+
"\e[#{ color }m#{ super(level, *args) }\e[0;0m"
|
47
|
+
else
|
48
|
+
super(level, *args)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def initialize(logdev)
|
53
|
+
super
|
54
|
+
|
55
|
+
@formatter = SimpleFormatter.new
|
56
|
+
end
|
57
|
+
|
58
|
+
@@instance = self.new(STDOUT)
|
59
|
+
|
60
|
+
def self.instance
|
61
|
+
return @@instance
|
62
|
+
end
|
63
|
+
|
64
|
+
# Prevent folks from instantiating one
|
65
|
+
private_class_method :new
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# coding: UTF-8
|
2
|
+
require 'pty'
|
3
|
+
|
4
|
+
module AmoebaDeployTools
|
5
|
+
class NoiseyCocaineRunner
|
6
|
+
def self.supported?
|
7
|
+
true
|
8
|
+
end
|
9
|
+
|
10
|
+
def supported?
|
11
|
+
self.class.supported?
|
12
|
+
end
|
13
|
+
|
14
|
+
def call(command, env = {})
|
15
|
+
buffer = ''
|
16
|
+
with_modified_environment(env) do
|
17
|
+
begin
|
18
|
+
# Use PTY.spawn so we don't buffer anything. r will contain the output (stdout & stderr)
|
19
|
+
PTY.spawn(command) do |r,w,pid|
|
20
|
+
begin
|
21
|
+
r.each { |line| Cocaine::CommandLine.logger.debug line.strip; buffer << line }
|
22
|
+
rescue Errno::EIO
|
23
|
+
# Output is done
|
24
|
+
end
|
25
|
+
# Note: This requires ruby 1.9.2!
|
26
|
+
Process.wait(pid) # Wait for the process to die (so $? is set)
|
27
|
+
end
|
28
|
+
rescue PTY::ChildExited
|
29
|
+
# The command is done
|
30
|
+
# $!.status.exitstatus would likely contain the exit code
|
31
|
+
end
|
32
|
+
end
|
33
|
+
buffer
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def with_modified_environment(env, &block)
|
39
|
+
ClimateControl.modify(env, &block)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'stringio'
|
3
|
+
require 'fileutils'
|
4
|
+
|
5
|
+
class AmoebaDeployTools
|
6
|
+
describe 'Amoeba command' do
|
7
|
+
def run_cmd(argv)
|
8
|
+
$stdout = StringIO.new
|
9
|
+
$stderr = StringIO.new
|
10
|
+
|
11
|
+
object = Amoeba.new(*argv).tap {|c| c.run(false) }
|
12
|
+
|
13
|
+
@stdout = $stdout.tap {|f| f.seek(0) }.read
|
14
|
+
@stderr = $stderr.tap {|f| f.seek(0) }.read
|
15
|
+
ensure
|
16
|
+
$stdout = STDOUT
|
17
|
+
$stderr = STDERR
|
18
|
+
|
19
|
+
return object
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.context(*a, **kw)
|
23
|
+
if a.first.is_a? Array
|
24
|
+
kw[:argv] = a[0]
|
25
|
+
a[0] = "[#{a[0].join(' ')}]"
|
26
|
+
end
|
27
|
+
|
28
|
+
super
|
29
|
+
end
|
30
|
+
|
31
|
+
@@subject = nil
|
32
|
+
def rerun_cmd
|
33
|
+
@@subject = run_cmd(example.metadata[:argv])
|
34
|
+
end
|
35
|
+
|
36
|
+
subject { rerun_cmd }
|
37
|
+
before { rerun_cmd }
|
38
|
+
|
39
|
+
shared_context require_kitchen: true do
|
40
|
+
around do |example|
|
41
|
+
in_tmpdir do
|
42
|
+
%w(
|
43
|
+
.amoeba
|
44
|
+
.amoeba/nodes
|
45
|
+
.amoeba/roles
|
46
|
+
).map {|p| Dir.mkdir p}
|
47
|
+
|
48
|
+
example.metadata[:nodes].each do |d|
|
49
|
+
FileUtils.touch ".amoeba/nodes/#{d}.json"
|
50
|
+
end if example.metadata[:nodes].is_a? Enumerable
|
51
|
+
|
52
|
+
example.run
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
context 'when missing kitchen' do
|
57
|
+
before { FileUtils.rm_rf '.amoeba' }
|
58
|
+
|
59
|
+
its (:status) { should eq(1) }
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
context %w(help) do
|
64
|
+
its (:cmd) { should eq(:amoeba) }
|
65
|
+
its (:subcmd) { should eq(:help) }
|
66
|
+
|
67
|
+
its (:status) { should eq(1) }
|
68
|
+
end
|
69
|
+
|
70
|
+
context %w(node list), :require_kitchen do
|
71
|
+
its (:cmd) { should eq(:node) }
|
72
|
+
its (:subcmd) { should eq(:list) }
|
73
|
+
|
74
|
+
context 'without nodes' do
|
75
|
+
it 'returns empty' do
|
76
|
+
expect(@stdout).to eq('')
|
77
|
+
end
|
78
|
+
|
79
|
+
its (:status) { should eq(0) }
|
80
|
+
end
|
81
|
+
|
82
|
+
context 'with nodes', nodes: %w( a.example.com b.example.com ) do
|
83
|
+
it 'lists nodes' do
|
84
|
+
expect(@stdout).to eq(example.metadata[:nodes].map {|n| n + "\n"}.join)
|
85
|
+
end
|
86
|
+
|
87
|
+
its (:status) { should eq(0) }
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
describe AmoebaDeployTools::Config do
|
5
|
+
let (:config) { subject }
|
6
|
+
|
7
|
+
let :given do
|
8
|
+
{'foo' => 'bar'}
|
9
|
+
end
|
10
|
+
|
11
|
+
before merge: true do
|
12
|
+
config.merge! given
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'saves config files', :merge do
|
16
|
+
with_tmpfile do |f|
|
17
|
+
config.save filename: f
|
18
|
+
end
|
19
|
+
|
20
|
+
expect(config).to eq(given)
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'loads config files' do
|
24
|
+
with_tmpfile YAML.dump(given) do |f|
|
25
|
+
config.restore filename: f
|
26
|
+
end
|
27
|
+
|
28
|
+
expect(config).to eq(given)
|
29
|
+
end
|
30
|
+
|
31
|
+
context 'with a nested structure' do
|
32
|
+
let :given do
|
33
|
+
{'foo' => {'bar' => {'baz' => 'garply'}}}
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'expands dotted keys' do
|
37
|
+
config['foo.bar.baz'] = 'garply'
|
38
|
+
|
39
|
+
expect(config).to eq(given)
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'flattens into dotted keys', :merge do
|
43
|
+
expect(config.flatten).to eq({
|
44
|
+
'foo.bar.baz' => 'garply'
|
45
|
+
})
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe :dedent do
|
4
|
+
it 'correctly removes indentation' do
|
5
|
+
expect(dedent %{
|
6
|
+
foo
|
7
|
+
bar
|
8
|
+
baz
|
9
|
+
garply
|
10
|
+
}).to eq("foo\n bar\nbaz\n garply\n")
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
describe :indent do
|
15
|
+
it 'indents each line by the specified amount' do
|
16
|
+
expect(indent("foo\n bar\nbaz\n garply\n", 3)).to eq(" foo\n bar\n baz\n garply\n")
|
17
|
+
end
|
18
|
+
end
|