soloist 1.0.0.pre → 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.
data/soloist.gemspec CHANGED
@@ -23,10 +23,13 @@ Gem::Specification.new do |s|
23
23
  s.add_dependency "librarian"
24
24
  s.add_dependency "thor"
25
25
  s.add_dependency "hashie"
26
+ s.add_dependency "net-ssh"
27
+ s.add_dependency "awesome_print"
26
28
 
27
29
  s.add_development_dependency "rspec"
28
30
  s.add_development_dependency "guard-rspec"
29
31
  s.add_development_dependency "guard-bundler"
32
+ s.add_development_dependency "guard-shell"
30
33
  s.add_development_dependency "rb-fsevent"
31
34
  s.add_development_dependency "terminal-notifier-guard"
32
35
  s.add_development_dependency "gem-release"
@@ -0,0 +1,8 @@
1
+ def make_story_channel(&block)
2
+ story do |session|
3
+ channel = session.opens_channel
4
+ block.call(channel)
5
+ channel.gets_close
6
+ channel.sends_close
7
+ end
8
+ end
@@ -0,0 +1,38 @@
1
+ require "net/ssh/test"
2
+
3
+ class Net::SSH::Test::Packet
4
+ alias :original_types :types
5
+ def types
6
+ @types ||= case @type
7
+ when CHANNEL_EXTENDED_DATA then [:long, :long, :string]
8
+ else original_types
9
+ end
10
+ end
11
+ end
12
+
13
+ class Net::SSH::Test::Script
14
+ def gets_channel_extended_data(channel, data)
15
+ events << Net::SSH::Test::RemotePacket.new(:channel_extended_data, channel.local_id, 1, data)
16
+ end
17
+ end
18
+
19
+ class Net::SSH::Test::Channel
20
+ def gets_extended_data(data)
21
+ script.gets_channel_extended_data(self, data)
22
+ end
23
+ end
24
+
25
+ class Net::SSH::Test::Kex
26
+ def exchange_keys
27
+ result = Net::SSH::Buffer.from(:byte, NEWKEYS)
28
+ @connection.send_message(result)
29
+
30
+ buffer = @connection.next_message
31
+ raise Net::SSH::Exception, "expected NEWKEYS" unless buffer.type == NEWKEYS
32
+
33
+ { :session_id => "abc-xyz",
34
+ :server_key => OpenSSL::PKey::RSA.new(512),
35
+ :shared_secret => OpenSSL::BN.new("1234567890", 10),
36
+ :hashing_algorithm => OpenSSL::Digest::SHA1 }
37
+ end
38
+ end
@@ -2,12 +2,27 @@ require "spec_helper"
2
2
 
3
3
  describe Soloist::CLI do
4
4
  let(:cli) { Soloist::CLI.new }
5
- let(:base_path) { Dir.mktmpdir }
5
+ let(:base_path) { RSpec.configuration.tempdir }
6
6
  let(:soloistrc_path) { File.expand_path("soloistrc", base_path) }
7
7
 
8
- before { FileUtils.mkdir_p(base_path) }
8
+ before do
9
+ FileUtils.mkdir_p(base_path)
10
+ Soloist::Config.any_instance.stub(:exec)
11
+ end
9
12
 
10
13
  describe "#chef" do
14
+ it "receives the outside environment" do
15
+ FileUtils.touch(soloistrc_path)
16
+ Dir.chdir(base_path) do
17
+ ENV["AUTREYISM"] = "pathological-yodeling"
18
+ cli.soloist_config.should_receive(:exec) do |chef_solo|
19
+ `#{chef_solo}`.chomp.should == "pathological-yodeling"
20
+ end
21
+ cli.soloist_config.stub(:chef_solo).and_return('echo $AUTREYISM')
22
+ cli.chef
23
+ end
24
+ end
25
+
11
26
  context "when the soloistrc file does not exist" do
12
27
  it "raises an error" do
13
28
  expect do
@@ -26,55 +41,84 @@ describe Soloist::CLI do
26
41
  File.open(soloistrc_path, "w") do |file|
27
42
  file.write(YAML.dump("recipes" => ["stinky::feet"]))
28
43
  end
44
+ cli.soloist_config = nil
45
+ Dir.chdir(base_path) { cli.soloist_config.stub(:exec) }
29
46
  end
30
47
 
31
48
  it "runs the proper recipes" do
32
- cli.stub(:exec)
33
- Dir.chdir(base_path) { cli.chef }
34
- cli.config.royal_crown.recipes.should =~ ["stinky::feet"]
49
+ cli.chef
50
+ cli.soloist_config.royal_crown.recipes.should =~ ["stinky::feet"]
51
+ end
52
+
53
+ context "when a soloistrc_local file exists" do
54
+ let(:soloistrc_local_path) { File.expand_path("soloistrc_local", base_path) }
55
+
56
+ before do
57
+ File.open(soloistrc_local_path, "w") do |file|
58
+ file.write(YAML.dump("recipes" => ["stinky::socks"]))
59
+ end
60
+ cli.soloist_config = nil
61
+ Dir.chdir(base_path) { cli.soloist_config.stub(:exec) }
62
+ end
63
+
64
+ it "installs the proper recipes" do
65
+ cli.chef
66
+ cli.soloist_config.royal_crown.recipes.should =~ ["stinky::feet", "stinky::socks"]
67
+ end
35
68
  end
36
69
 
37
70
  context "when the Cheffile does not exist" do
38
71
  it "runs chef" do
39
- cli.should_receive(:exec)
40
- Dir.chdir(base_path) { cli.chef }
72
+ cli.soloist_config.should_receive(:exec)
73
+ cli.chef
41
74
  end
42
75
 
43
76
  it "does not run librarian" do
44
- cli.stub(:exec)
45
77
  Librarian::Chef::Cli.should_not_receive(:with_environment)
46
- Dir.chdir(base_path) { cli.chef }
78
+ cli.chef
47
79
  end
48
80
  end
49
81
 
50
82
  context "when the Cheffile exists" do
51
83
  let(:cli_instance) { double(:cli_instance) }
52
84
 
53
- before do
54
- FileUtils.touch(File.expand_path("Cheffile", base_path))
55
- cli.stub(:exec)
56
- end
85
+ before { FileUtils.touch(File.expand_path("Cheffile", base_path)) }
57
86
 
58
87
  it "runs librarian" do
59
88
  Librarian::Chef::Cli.should_receive(:with_environment).and_yield
60
89
  Librarian::Chef::Cli.should_receive(:new).and_return(cli_instance)
61
90
  cli_instance.should_receive(:install)
62
- Dir.chdir(base_path) { cli.chef }
91
+ cli.chef
63
92
  end
64
93
 
65
- it "runs chef" do
66
- cli.should_receive(:exec)
67
- Dir.chdir(base_path) { cli.chef }
94
+ context "when the user is not root" do
95
+ it "creates the cache path using sudo" do
96
+ cli.soloist_config.should_receive(:exec) do |command|
97
+ command.should =~ /^sudo -E/
98
+ end
99
+ cli.chef
100
+ end
101
+ end
102
+
103
+ context "when the user is root" do
104
+ before { Process.stub(:uid => 0) }
105
+
106
+ it "creates the cache path" do
107
+ cli.soloist_config.should_receive(:exec) do |command|
108
+ command.should_not =~ /^sudo -E/
109
+ end
110
+ cli.chef
111
+ end
68
112
  end
69
113
  end
70
114
  end
71
115
  end
72
116
 
73
- describe "#DO_IT_LIVE" do
117
+ describe "#run_recipe" do
74
118
  context "when the soloistrc does not exist" do
75
119
  it "raises an error" do
76
120
  expect do
77
- Dir.chdir(base_path) { cli.DO_IT_LIVE("pineapple::wut") }
121
+ Dir.chdir(base_path) { cli.run_recipe("pineapple::wut") }
78
122
  end.to raise_error(Soloist::NotFound)
79
123
  end
80
124
  end
@@ -89,45 +133,22 @@ describe Soloist::CLI do
89
133
  it "sets a recipe to run" do
90
134
  Dir.chdir(base_path) do
91
135
  cli.should_receive(:chef)
92
- cli.DO_IT_LIVE("angst::teenage", "ennui::default")
93
- cli.config.royal_crown.recipes.should =~ ["angst::teenage", "ennui::default"]
136
+ cli.run_recipe("angst::teenage", "ennui::default")
137
+ cli.soloist_config.royal_crown.recipes.should =~ ["angst::teenage", "ennui::default"]
94
138
  end
95
139
  end
96
140
  end
97
141
  end
98
142
 
99
- describe "#ensure_chef_cache_path" do
100
- context "when the cache path does not exist" do
101
- before { File.stub(:directory? => false) }
102
-
103
- it "creates the cache path" do
104
- cli.should_receive(:system).with("sudo mkdir -p /var/chef/cache")
105
- cli.ensure_chef_cache_path
106
- end
107
- end
108
-
109
- context "when the cache path exists" do
110
- before { File.stub(:directory? => true) }
111
-
112
- it "does not create the cache path" do
113
- cli.should_not_receive(:system)
114
- cli.ensure_chef_cache_path
115
- end
116
- end
117
- end
143
+ describe "#config" do
144
+ let(:royal_crown) { Soloist::RoyalCrown.new(:node_attributes => {"a" => "b"}) }
145
+ let(:config) { Soloist::Config.new(royal_crown) }
118
146
 
119
- describe "#chef_solo" do
120
- before do
121
- ENV["AUTREYISM"] = "pathological-yodeling"
122
- FileUtils.touch(File.expand_path("soloistrc", base_path))
123
- end
147
+ before { cli.stub(:soloist_config => config) }
124
148
 
125
- it "receives the outside environment" do
126
- cli.stub(:chef_solo).and_return('echo $AUTREYISM')
127
- cli.should_receive(:exec) do |chef_solo|
128
- `#{chef_solo}`.chomp.should == "pathological-yodeling"
129
- end
130
- Dir.chdir(base_path) { cli.chef }
149
+ it "prints the hash render of the RoyalCrown" do
150
+ Kernel.should_receive(:ap).with({"recipes"=>[], "a" => "b"})
151
+ cli.config
131
152
  end
132
153
  end
133
154
  end
@@ -1,107 +1,196 @@
1
1
  require "spec_helper"
2
2
 
3
3
  describe Soloist::Config do
4
- let(:soloist_rc) { Soloist::RoyalCrown.new(:path => "/tmp/soloist/soloistrc") }
4
+ let(:soloist_rc_path) { File.expand_path("soloistrc", RSpec.configuration.tempdir) }
5
+ let(:soloist_rc) { Soloist::RoyalCrown.new(:path => soloist_rc_path) }
5
6
  let(:config) { Soloist::Config.new(soloist_rc) }
7
+ let(:cookbook_path) { File.expand_path("cookbooks", RSpec.configuration.tempdir) }
8
+ let(:nested_cookbook_path) { File.expand_path("whoa/cookbooks", RSpec.configuration.tempdir) }
6
9
 
7
10
  describe "#as_solo_rb" do
8
- context "without extra cookbook paths" do
9
- it "can generate solo.rb" do
10
- config.as_solo_rb.should == 'cookbook_path ["/tmp/soloist/cookbooks"]'
11
- end
11
+ subject { config.as_solo_rb }
12
+
13
+ it { should include 'file_cache_path "/var/chef/cache"' }
14
+ it { should include %(json_attribs "#{config.node_json_path}") }
15
+ end
16
+
17
+ describe "#cookbook_paths" do
18
+ subject { config.cookbook_paths }
19
+
20
+ context "when the default cookbook path does not exist" do
21
+ it { should have(0).paths }
12
22
  end
13
23
 
14
- context "with a cookbook path" do
15
- before { soloist_rc.cookbook_paths = ["/opt/holla/at/yo/soloist"] }
24
+ context "when the default cookbook path exists" do
25
+ before { FileUtils.mkdir_p(cookbook_path) }
26
+
27
+ it { should have(1).path }
28
+ it { should =~ [cookbook_path] }
29
+
30
+ context "when the default cookbook path is specified" do
31
+ before { soloist_rc.cookbook_paths = [cookbook_path] }
16
32
 
17
- it "can have multiple cookbook paths" do
18
- config.as_solo_rb.should == 'cookbook_path ["/tmp/soloist/cookbooks", "/opt/holla/at/yo/soloist"]'
33
+ it { should have(1).path }
34
+ it { should =~ [cookbook_path] }
19
35
  end
20
36
 
21
- it "removes duplicate cookbook paths" do
22
- expect do
23
- soloist_rc.cookbook_paths << "/opt/holla/at/yo/soloist"
24
- end.not_to change { config.as_solo_rb }
37
+ context "with a specified cookbook path" do
38
+ before { soloist_rc.cookbook_paths = [nested_cookbook_path] }
39
+
40
+ context "when the specified path exists" do
41
+ before { FileUtils.mkdir_p(nested_cookbook_path) }
42
+
43
+ it { should have(2).paths }
44
+ it { should =~ [cookbook_path, nested_cookbook_path] }
45
+
46
+ context "with duplicate cookbook paths" do
47
+ before { soloist_rc.cookbook_paths = [nested_cookbook_path, nested_cookbook_path] }
48
+
49
+ it { should have(2).paths }
50
+ it { should =~ [cookbook_path, nested_cookbook_path] }
51
+ end
52
+ end
53
+
54
+ context "when the specified path does not exist" do
55
+ it { should have(1).path }
56
+ it { should =~ [cookbook_path] }
57
+ end
25
58
  end
26
59
  end
27
60
 
28
61
  context "with relative paths" do
29
- let(:pwd) { File.expand_path(".") }
30
-
31
- before { soloist_rc.cookbook_paths << "./meth/cookbooks" }
32
-
33
- it "can have multiple cookbook paths" do
34
- config.as_solo_rb.should == 'cookbook_path ["/tmp/soloist/cookbooks", "/tmp/soloist/meth/cookbooks"]'
62
+ before do
63
+ soloist_rc.cookbook_paths = ["./whoa/cookbooks"]
64
+ FileUtils.mkdir_p(nested_cookbook_path)
35
65
  end
66
+
67
+ it { should have(1).path }
68
+ it { should =~ [nested_cookbook_path] }
36
69
  end
37
70
 
38
71
  context "with unixisms in the cookbook path" do
39
72
  let(:home) { File.expand_path("~") }
40
73
 
41
- before { soloist_rc.cookbook_paths << "~/yo/homes" }
74
+ before { soloist_rc.cookbook_paths = ["~"] }
42
75
 
43
- it "expands paths" do
44
- config.as_solo_rb.should include "#{home}/yo/homes"
45
- end
76
+ it { should have(1).path }
77
+ it { should =~ [home] }
46
78
  end
47
79
  end
48
80
 
49
- describe "#as_json" do
50
- context "with recipes" do
51
- before { soloist_rc.recipes = ["waffles"] }
52
-
53
- it "can generate json" do
54
- config.as_json["recipes"].should include "waffles"
55
- end
81
+ describe "#as_node_json" do
82
+ let(:soloist_rc) do
83
+ Soloist::RoyalCrown.new(
84
+ :path => soloist_rc_path,
85
+ :recipes => ["waffles"],
86
+ :node_attributes => { "gargling" => "cool", "birds" => {"nested" => "cheep"} }
87
+ )
56
88
  end
57
- end
58
89
 
59
- describe "#compiled_rc" do
60
- let(:switch) { {"OX_TONGUES" => {"FINE" => {"recipes" => ["hobo_fist"]}}} }
90
+ describe "node_attributes" do
91
+ subject { config.as_node_json }
61
92
 
62
- before do
63
- soloist_rc.env_variable_switches = switch
93
+ it { should include "gargling" => "cool" }
94
+ it { should include "birds" => { "nested" => "cheep" } }
64
95
  end
65
96
 
66
- context "when a switch is active" do
67
- before { ENV.stub(:[]).and_return("FINE") }
97
+ describe "recipes" do
98
+ subject { config.as_node_json["recipes"] }
68
99
 
69
- it "merges the environment variable switch" do
70
- config.compiled_rc.recipes.should include "hobo_fist"
71
- end
100
+ it { should have(1).recipe }
101
+ it { should =~ ["waffles"] }
72
102
  end
103
+ end
73
104
 
74
- context "when a switch is inactive" do
75
- before { ENV.stub(:[]).and_return("WHAT_NO_EW") }
105
+ describe "#compiled" do
106
+ let(:nested) { {} }
107
+ let(:switch) do
108
+ {
109
+ "TONGUES" => {
110
+ "FINE" => {
111
+ "recipes" => ["hobo_fist"],
112
+ "env_variable_switches" => nested
113
+ }
114
+ }
115
+ }
116
+ end
117
+
118
+ before { config.royal_crown.env_variable_switches = switch }
119
+
120
+ context "when the switch is inactive" do
121
+ before { ENV.stub(:[]).and_return("LOLWUT") }
76
122
 
77
- it "outputs an empty list" do
78
- config.compiled_rc.recipes.should be_empty
123
+ it "does not merge the attribute" do
124
+ config.compiled["recipes"].should be_empty
79
125
  end
80
126
  end
81
127
 
82
- context "when switches are nested" do
83
- let(:inner) { {"GOAT" => {"TASTY" => {"node_attributes" => {"bbq" => "satan"}}}} }
84
- let(:outer) { {"recipes" => ["stinkditch"], "env_variable_switches" => inner} }
128
+ context "when a switch is active" do
129
+ before { ENV.stub(:[]).and_return("FINE") }
85
130
 
86
- before { ENV.stub(:[]).and_return("TASTY") }
131
+ it "merges the attributes" do
132
+ config.compiled.recipes.should =~ ["hobo_fist"]
133
+ end
87
134
 
88
- context "when the inner switch is active" do
89
- let(:switch) { {"HORSE" => {"TASTY" => outer }} }
135
+ context "when an inactive switch is nested" do
136
+ let(:nested) { {"BEANS" => {"EW" => {"recipes" => ["slammin"]}}} }
90
137
 
91
- it "evalutes all switches" do
92
- config.compiled_rc.node_attributes.bbq.should == "satan"
93
- config.compiled_rc.recipes.should == ["stinkditch"]
138
+ it "does not merge the attributes" do
139
+ config.compiled.recipes.should =~ ["hobo_fist"]
94
140
  end
95
141
  end
96
142
 
97
- context "when the outer switch is inactive" do
98
- let(:switch) { {"HORSE" => {"GROSS" => outer }} }
143
+ context "when an active switch is nested" do
144
+ let(:nested) { {"BEANS" => {"FINE" => {"recipes" => ["slammin"]}}} }
99
145
 
100
- it "does not evaluate deeper" do
101
- config.compiled_rc.recipes.should be_empty
102
- config.compiled_rc.node_attributes.should be_empty
146
+ it "merges the attributes" do
147
+ config.compiled.recipes.should =~ ["slammin"]
103
148
  end
104
149
  end
105
150
  end
106
151
  end
152
+
153
+ describe "#merge!" do
154
+ let(:soloist_rc) { Soloist::RoyalCrown.new(:recipes => ["guts"], :node_attributes => {:reliable => "maybe"}) }
155
+ let(:other_rc) { Soloist::RoyalCrown.new(:recipes => ["chum"], :node_attributes => {:tasty => "maybe"}) }
156
+ let(:other_config) { Soloist::Config.new(other_rc) }
157
+
158
+ it "merges another config into the current one" do
159
+ config.merge!(other_config)
160
+ config.royal_crown.recipes.should =~ ["guts", "chum"]
161
+ config.royal_crown.node_attributes.keys.should =~ [:reliable, :tasty]
162
+ end
163
+
164
+ it "does not trample the other config" do
165
+ config.merge!(other_config)
166
+ other_config.royal_crown.recipes.should =~ ["chum"]
167
+ other_config.royal_crown.node_attributes.should == {:tasty => "maybe"}
168
+ end
169
+ end
170
+
171
+ describe "#log_level" do
172
+ subject { config.log_level }
173
+
174
+ context "when LOG_LEVEL is not set" do
175
+ it { should == "info" }
176
+ end
177
+
178
+ context "when LOG_LEVEL is set" do
179
+ before { ENV.stub(:[] => "BEANS") }
180
+ it { should == "BEANS" }
181
+ end
182
+ end
183
+
184
+ describe "#debug?" do
185
+ subject { config.debug? }
186
+
187
+ context "when log_level is not debug" do
188
+ it { should_not be }
189
+ end
190
+
191
+ context "when log_level is debug" do
192
+ before { config.stub(:log_level => "debug") }
193
+ it { should be }
194
+ end
195
+ end
107
196
  end
@@ -0,0 +1,84 @@
1
+ require "spec_helper"
2
+
3
+ describe Soloist::RemoteConfig do
4
+ let(:royal_crown_path) { File.expand_path("soloistrc", RSpec.configuration.tempdir) }
5
+ let(:royal_crown) { Soloist::RoyalCrown.new(:path => royal_crown_path) }
6
+ let(:remote) { Soloist::Remote.new("user", "host", "key") }
7
+ let(:remote_config) { Soloist::RemoteConfig.new(royal_crown, remote) }
8
+
9
+ before { remote.stub(:backtick => "", :system => 0) }
10
+
11
+ def commands_for(method)
12
+ [].tap do |commands|
13
+ remote.stub(:system) { |c| commands << c; 0 }
14
+ remote.stub(:backtick) { |c| commands << c; "" }
15
+ remote_config.send(method)
16
+ end
17
+ end
18
+
19
+ describe "#run_chef" do
20
+ it "runs chef" do
21
+ commands_for(:run_chef).last.should include "chef-solo"
22
+ end
23
+ end
24
+
25
+ describe "#solo_rb_path" do
26
+ it "sets the path to /etc/chef/solo.rb" do
27
+ remote_config.solo_rb_path.should == "/etc/chef/solo.rb"
28
+ end
29
+
30
+ it "sets up solo.rb remotely" do
31
+ commands_for(:solo_rb_path).last.should =~ /sudo -E tee \/etc\/chef\/solo\.rb$/
32
+ end
33
+ end
34
+
35
+ describe "#node_json_path" do
36
+ it "sets the path" do
37
+ remote_config.node_json_path.should == "/etc/chef/node.json"
38
+ end
39
+
40
+ it "sets up node.json remotely" do
41
+ commands_for(:node_json_path).last.should =~ /sudo -E tee \/etc\/chef\/node\.json$/
42
+ end
43
+ end
44
+
45
+ describe "#chef_config_path" do
46
+ it "sets the path" do
47
+ remote_config.chef_config_path.should == "/etc/chef"
48
+ end
49
+
50
+ it "creates the path remotely" do
51
+ commands_for(:chef_config_path).tap do |commands|
52
+ commands.should have(1).command
53
+ commands.first.should =~ /mkdir .*? -p \/etc\/chef$/
54
+ end
55
+ end
56
+ end
57
+
58
+ describe "#chef_cache_path" do
59
+ it "sets the path" do
60
+ remote_config.chef_cache_path.should == "/var/chef/cache"
61
+ end
62
+
63
+ it "creates the path remotely" do
64
+ commands_for(:chef_cache_path).tap do |commands|
65
+ commands.should have(1).command
66
+ commands.first.should =~ /mkdir .*? -p \/var\/chef\/cache$/
67
+ end
68
+ end
69
+ end
70
+
71
+ describe "#cookbook_paths" do
72
+ it "sets the path" do
73
+ remote_config.cookbook_paths.should have(1).path
74
+ remote_config.cookbook_paths.should =~ ["/var/chef/cookbooks"]
75
+ end
76
+
77
+ it "creates the path remotely" do
78
+ commands_for(:cookbook_paths).tap do |commands|
79
+ commands.should have(1).command
80
+ commands.first.should =~ /mkdir .*? -p \/var\/chef\/cookbooks$/
81
+ end
82
+ end
83
+ end
84
+ end