manic_baker 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: d67391367a32c3bdfb8243523236a30b1c4f8ea6
4
+ data.tar.gz: 04c45791867835639f37d6b9b623e99438da11a9
5
+ SHA512:
6
+ metadata.gz: 9f3ccd681d950e38144143fd6423773d194072fb98c944faa255fc1602f59c2db61ee59034391960d3aeec0ba1d10ce1b85382c125e4ad1d0f1ca44e40cb8942
7
+ data.tar.gz: daad125b612a6df9eed43fda309f5c6d8fee4ae50a8f6214d1fb90649bd6fd22d09ca5ab58c80a9fc8bc7f015790c7bce3f6819ed72dcaa2d536fb777b6225d7
data/.gitignore ADDED
@@ -0,0 +1,21 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ .DS_Store
19
+ .baker.yml
20
+ Cheffile*
21
+ cookbooks
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format documentation
data/.ruby-gemset ADDED
@@ -0,0 +1 @@
1
+ manic_baker
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.0.0-p195
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ rvm:
2
+ - 1.9.3
3
+ - 1.9.2
4
+ - 2.0.0
5
+ script: script/ci.sh
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in manic_baker.gemspec
4
+ gemspec
data/Guardfile ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ guard :rspec, {:env => {"JOYENT_USERNAME" => "docteats"} } do
4
+ watch(%r{^spec/.+_spec\.rb$})
5
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
6
+ watch('spec/spec_helper.rb') { "spec" }
7
+ end
8
+
9
+ guard 'bundler' do
10
+ watch('Gemfile')
11
+ watch(/^.+\.gemspec/)
12
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Doc Ritezel
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
@@ -0,0 +1,89 @@
1
+ # Manic Baker
2
+
3
+ [![Build Status](https://travis-ci.org/minifast/manic_baker.png)](https://travis-ci.org/minifast/manic_baker) [![Code Climate](https://codeclimate.com/github/minifast/manic_baker.png)](https://codeclimate.com/github/minifast/manic_baker)
4
+
5
+ > Note: Most of the functionality in this repository is
6
+ > better implemented in Vagrant or Knife.
7
+
8
+ Manic Baker lets you do all the stuff you want for a full
9
+ continuous deployment cycle on Joyent:
10
+
11
+ 1. Spin up a new Joyent base image
12
+ 1. Bootstrap the new node into Chefable condition
13
+ 1. Parbake your application code
14
+ 1. Run your runlist using soloist
15
+ 1. Snapshot the Joyent instance
16
+ 1. Scale instances up or down using a snapshot id
17
+
18
+ ## Installation
19
+
20
+ Add this line to your application's Gemfile:
21
+
22
+ gem 'manic_baker'
23
+
24
+ And then execute:
25
+
26
+ $ bundle
27
+
28
+ Or install it yourself as:
29
+
30
+ $ gem install manic_baker
31
+
32
+ ## Usage
33
+
34
+ Manic Baker has a totally baller command line interface.
35
+
36
+ $ manic parbake 1d6af-is-that-a-sha
37
+
38
+ start base image '0000-2bad-54e5-a-ba7'
39
+ run bootstrap.sh
40
+ rsync copying this directory to /opt/app
41
+ chef not_sql::default
42
+ chef your_awesome_application::default
43
+ chef runit::your_awesome_application
44
+ snapshot created with id '1d6af-is-that-a-sha'
45
+ stop build complete
46
+
47
+ $ manic parbake 1d6af-is-that-a-sha
48
+
49
+ oop snapshot '1d6af-is-that-a-sha' already exists
50
+
51
+ $ manic boot no-snapshot-called-this
52
+
53
+ oop snapshot 'no-snapshot-called-this' not found
54
+
55
+ $ manic boot 1d6af-is-that-a-sha
56
+
57
+ start snapshot '1d6af-is-that-a-sha' booting
58
+ waiting ...
59
+ bound instance is now available at 192.168.0.1
60
+
61
+ $ manic boot 1d6af-is-that-a-sha
62
+
63
+ nope an instance of '1d6af-is-that-a-sha' is running
64
+
65
+ $ manic boot --plz --scale=2 1d6af-is-that-a-sha
66
+
67
+ fine whatever
68
+ panic running over instance at 192.168.0.1
69
+ waiting ...
70
+ done it was looking at me funny
71
+ start snapshot '1d6af-is-that-a-sha' booting
72
+ waiting ...
73
+ scale.1 instance is now available at 192.168.0.2
74
+ scale.2 instance is now available at 192.168.0.3
75
+
76
+ $ manic panic 1d6af-is-that-a-sha
77
+
78
+ panic slandering instance at 192.168.0.2
79
+ panic posting pictures of instance at 192.168.0.3
80
+ done their lives are ruined for a reason
81
+
82
+ ## Contributing
83
+
84
+ 1. Fork it
85
+ 1. Create your feature branch (`git checkout -b my-new-feature`)
86
+ 1. If you are making changes in the lib/ directory, add tests.
87
+ 1. Commit your changes (`git commit -am 'Add some feature'`)
88
+ 1. Push to the branch (`git push origin my-new-feature`)
89
+ 1. Create new Pull Request
data/bin/manic ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ $:.push File.expand_path("../../lib", __FILE__)
3
+ require "manic_baker"
4
+ ManicBaker::Cli.start
@@ -0,0 +1,52 @@
1
+ boring:
2
+ - ugh
3
+ - hurry uuuuuup
4
+ - i'm getting old
5
+ - this is seriously slower than my housekeeper?
6
+ - really?
7
+ - guys this is not my jam
8
+ - have you seen my phone.
9
+ - i don't really have a tumblr anymore
10
+ success:
11
+ start:
12
+ - making friends with %s
13
+ - going to music festival with %s
14
+ - drinking sangria and watching boys with %s
15
+ - going to the club with %s
16
+ bootstrap:
17
+ - getting french manicure with %s
18
+ - watching project runway with %s
19
+ - writing gilmore girls fanfics starring %s
20
+ chef:
21
+ - making quesadillas with %s
22
+ stop:
23
+ - slandering %s
24
+ - running over %s
25
+ - snapchatting naked pictures of %s
26
+ - stealing boyfriend of %s
27
+ - stealing girlfriend of %s
28
+ terminate:
29
+ - its life is over for a reason
30
+ - it just couldn't hang
31
+ - i feel sorry for it but not really?
32
+ snapshot:
33
+ - omg totally tagging %s cuz we were in %s
34
+ ssh:
35
+ - just so happy to hang with %s
36
+ - going to a movie with %s tmz
37
+ failure:
38
+ start:
39
+ - can't even stand the sight of %s at lunch
40
+ - ignoring texts from %s
41
+ - deleting %s from facebook
42
+ bootstrap:
43
+ - cannot change anything about %s
44
+ chef:
45
+ - totally got food poisoning from %s
46
+ stop:
47
+ - cannot even stand to be around %s
48
+ terminate:
49
+ - ugh why are some people just bad
50
+ ssh:
51
+ - so worried about %s
52
+ - does anyone know why %s isn't answering?
@@ -0,0 +1,2 @@
1
+ require "manic_baker/version"
2
+ require "manic_baker/cli"
@@ -0,0 +1,177 @@
1
+ require "librarian/chef/cli"
2
+ require "thor"
3
+ require "thor/shell/mean"
4
+ require "fog"
5
+ require "manic_baker/config"
6
+ require "manic_baker/remote_config"
7
+ require "soloist/spotlight"
8
+ require "soloist/remote"
9
+
10
+ module ManicBaker
11
+ class Cli < Thor
12
+ include Thor::Shell::Mean
13
+
14
+ desc "launch", "Launch a new instance, Alana"
15
+ def launch(dataset = nil)
16
+ unless dataset.nil?
17
+ config.dataset = dataset
18
+ config.save
19
+ end
20
+
21
+ check_dataset
22
+ check_no_servers
23
+
24
+ say_message_for(:start, :success, config.dataset)
25
+ joyent.servers.create(config.to_hash)
26
+ say_waiting_until { dataset_servers.first.state == "running" }
27
+ say_message_for(:start, :success, dataset_servers.first.name)
28
+ end
29
+
30
+ desc "panic", "Destroy some instances or whatever? Who cares."
31
+ def panic
32
+ check_dataset
33
+ check_servers
34
+
35
+ dataset_servers.each { |s| say_message_for(:stop, :success, s.name) }
36
+ dataset_servers.each(&:destroy)
37
+ say_waiting_until { !any_running? }
38
+ say_message_for(:terminate, :success)
39
+ end
40
+
41
+ desc "ssh", "I'm just gonna take a moment okay?"
42
+ def ssh
43
+ check_dataset
44
+ check_servers
45
+
46
+ exec("ssh -i #{config.private_key_path} root@#{dataset_servers.first.public_ip_address}")
47
+ end
48
+
49
+ desc "bootstrap", "You know those boots are like 90s style right"
50
+ def bootstrap
51
+ check_dataset
52
+ check_servers
53
+
54
+ remote.upload("#{script_path}/", "script/")
55
+ remote.system!("script/bootstrap.sh")
56
+ say_message_for(:bootstrap, :success, dataset_servers.first.name)
57
+ end
58
+
59
+ desc "chef", "Can you like make some spaghetti? I'm so high."
60
+ def chef
61
+ check_dataset
62
+ check_servers
63
+
64
+ install_cookbooks if cheffile_exists?
65
+ remote_config.run_chef
66
+ say_message_for(:chef, :success, dataset_servers.first.name)
67
+ end
68
+
69
+ desc "snapshot", "OMGGGGGG this jon lenin shirt is so ammmmazingggg"
70
+ def snapshot(name)
71
+ check_dataset
72
+ check_servers
73
+ check_no_snapshot_named(name)
74
+
75
+ joyent.snapshots.create(dataset_servers.first.id, name)
76
+ say_message_for(:snapshot, :success, dataset_servers.first.name, name)
77
+ end
78
+
79
+ private
80
+
81
+ def cheffile_exists?
82
+ File.exists?(File.expand_path("../Cheffile", config_path))
83
+ end
84
+
85
+ def install_cookbooks
86
+ Dir.chdir(File.dirname(config_path)) do
87
+ Librarian::Chef::Cli.with_environment do
88
+ Librarian::Chef::Cli.new.install
89
+ end
90
+ end
91
+ end
92
+
93
+ def any_running?
94
+ dataset_servers.any? do |server|
95
+ begin
96
+ server.reload
97
+ server.state == "running"
98
+ rescue Excon::Errors::Gone
99
+ false
100
+ end
101
+ end
102
+ end
103
+
104
+ def check_dataset
105
+ if config.dataset.nil?
106
+ raise Thor::Error.new("requires a dataset the first time out")
107
+ end
108
+ end
109
+
110
+ def check_servers
111
+ if dataset_servers.empty?
112
+ raise Thor::Error.new("found zero servers with dataset #{config.dataset}")
113
+ end
114
+ end
115
+
116
+ def check_no_servers
117
+ unless dataset_servers.empty?
118
+ raise Thor::Error.new("cannot clobber an existing instance")
119
+ end
120
+ end
121
+
122
+ def check_no_snapshot_named(name)
123
+ if dataset_servers.first.snapshots.any? { |s| s.name == name }
124
+ raise Thor::Error.new("there is already a snapshot called #{name}")
125
+ end
126
+ end
127
+
128
+ def script_path
129
+ File.expand_path("../../../script", __FILE__)
130
+ end
131
+
132
+ def dataset_servers
133
+ joyent.servers.reload.select do |server|
134
+ server.dataset == config.dataset
135
+ end
136
+ end
137
+
138
+ def say_waiting_until
139
+ say_waiting
140
+ say_until(".", nil, false) do
141
+ yield.tap do |all_done|
142
+ unless all_done
143
+ say_boring if rand < 0.1
144
+ sleep 1
145
+ end
146
+ end
147
+ end
148
+ end
149
+
150
+ def remote_config
151
+ @remote_config ||= ManicBaker::RemoteConfig.new(config, remote)
152
+ end
153
+
154
+ def remote
155
+ @remote ||= Soloist::Remote.new(
156
+ "root",
157
+ dataset_servers.first.public_ip_address,
158
+ config.private_key_path
159
+ )
160
+ end
161
+
162
+ def joyent
163
+ @joyent ||= Fog::Compute.new(
164
+ provider: "Joyent",
165
+ joyent_url: config.joyent_uri
166
+ )
167
+ end
168
+
169
+ def config
170
+ @config ||= ManicBaker::Config.from_file(config_path)
171
+ end
172
+
173
+ def config_path
174
+ @config_path ||= Soloist::Spotlight.find!(".baker.yml")
175
+ end
176
+ end
177
+ end
@@ -0,0 +1,21 @@
1
+ require "soloist/royal_crown"
2
+
3
+ module ManicBaker
4
+ class Config < Soloist::RoyalCrown
5
+ property :dataset
6
+ property :joyent_uri, :default => "https://us-east-1.api.joyentcloud.com"
7
+ property :flavor, :default => "Small 1GB"
8
+ property :ssh_key_name, :default => "id_rsa"
9
+ property :private_key_path, :default => File.expand_path("~/.ssh/id_rsa")
10
+
11
+ def public_key_path
12
+ "#{self.private_key_path}.pub"
13
+ end
14
+
15
+ private
16
+
17
+ def self.nilable_properties
18
+ (properties - [:path, :dataset]).map(&:to_s)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,57 @@
1
+ require "soloist/config"
2
+ require "soloist/remote"
3
+
4
+ module ManicBaker
5
+ class RemoteConfig < Soloist::Config
6
+ attr_reader :remote
7
+
8
+ def initialize(royal_crown, remote)
9
+ @royal_crown = royal_crown
10
+ @remote = remote
11
+ end
12
+
13
+ def run_chef
14
+ remote.system!(conditional_sudo(%(/usr/bin/bash -lc "#{chef_solo}")))
15
+ end
16
+
17
+ def node_json_path
18
+ @node_json_path ||= File.expand_path("node.json", chef_config_path).tap do |path|
19
+ remote.system!(%(echo '#{JSON.dump(as_node_json)}' | #{conditional_sudo("tee #{path}")}))
20
+ end
21
+ end
22
+
23
+ def solo_rb_path
24
+ @solo_rb_path ||= File.expand_path("solo.rb", chef_config_path).tap do |path|
25
+ remote.system!(%(echo '#{as_solo_rb}' | #{conditional_sudo("tee #{path}")}))
26
+ end
27
+ end
28
+
29
+ def chef_cache_path
30
+ @chef_cache_path ||= "/var/chef/cache".tap do |cache_path|
31
+ remote.system!(conditional_sudo("/opt/local/bin/mkdir -m 777 -p #{cache_path}"))
32
+ end
33
+ end
34
+
35
+ def chef_config_path
36
+ @chef_config_path ||= "/etc/chef".tap do |path|
37
+ remote.system!(conditional_sudo("/opt/local/bin/mkdir -m 777 -p #{path}"))
38
+ end
39
+ end
40
+
41
+ def cookbook_paths
42
+ @cookbook_paths ||= ["/var/chef/cookbooks".tap do |remote_path|
43
+ remote.system!(conditional_sudo("/opt/local/bin/mkdir -m 777 -p #{remote_path}"))
44
+ super.each { |path| remote.upload("#{path}/", remote_path) }
45
+ end]
46
+ end
47
+
48
+ protected
49
+ def conditional_sudo(command)
50
+ root? ? command : "/usr/bin/pfexec -E #{command}"
51
+ end
52
+
53
+ def root?
54
+ remote.user == "root"
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,3 @@
1
+ module ManicBaker
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,34 @@
1
+ class Thor
2
+ module Shell
3
+ module Mean
4
+ def messages
5
+ path = File.expand_path("../../../../config/locales/en.yml", __FILE__)
6
+ @messages ||= YAML.load_file(path)
7
+ end
8
+
9
+ def message_for(topic, state, *parameters)
10
+ messages[state.to_s][topic.to_s].sample % parameters
11
+ end
12
+
13
+ def say_message_for(topic, state, *parameters)
14
+ say_status(topic, message_for(topic, state, *parameters))
15
+ end
16
+
17
+ def say_until(*args)
18
+ say(*args) until yield
19
+ say
20
+ end
21
+
22
+ def say_waiting
23
+ status = set_color("waiting".rjust(12), :green, true)
24
+ say("#{status} ", nil, false)
25
+ end
26
+
27
+ def say_boring
28
+ puts
29
+ say_status(nil, messages["boring"].sample)
30
+ say_waiting
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'manic_baker/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "manic_baker"
8
+ spec.version = ManicBaker::VERSION
9
+ spec.authors = ["Doc Ritezel"]
10
+ spec.email = ["doc@minifast.co"]
11
+ spec.description = %q{Makes Joyent less manic}
12
+ spec.summary = %q{Stop calling the Ghostbusters just because you don't like my music, mom}
13
+ spec.homepage = "https://github.com/minifast/manic_baker"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split("\n")
17
+ spec.executables = `git ls-files -- bin`.split("\n").map { |f| File.basename(f) }
18
+ spec.test_files = `git ls-files -- spec`.split("\n")
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "fog"
22
+ spec.add_dependency "soloist"
23
+
24
+ spec.add_development_dependency "rspec"
25
+ spec.add_development_dependency "guard-rspec"
26
+ spec.add_development_dependency "guard-bundler"
27
+ spec.add_development_dependency "gem-release"
28
+ end
@@ -0,0 +1,31 @@
1
+ #!/bin/sh
2
+
3
+ pkgin list | grep ^ruby193-[0-9] ;
4
+ if [ $? -gt 0 ]; then
5
+ pkgin -y install ruby193 ;
6
+ fi ;
7
+
8
+ pkgin list | grep ^gmake-[0-9] ;
9
+ if [ $? -gt 0 ]; then
10
+ pkgin -y install gmake ;
11
+ fi ;
12
+
13
+ pkgin list | grep ^gcc47-[0-9] ;
14
+ if [ $? -gt 0 ]; then
15
+ pkgin -y install gcc47 ;
16
+ fi ;
17
+
18
+ gem list | grep ^chef
19
+ if [ $? -gt 0 ]; then
20
+ gem install chef --no-ri -no-rdoc ;
21
+ fi
22
+
23
+ test -h /opt/local/bin/chef-client ;
24
+ if [ $? -gt 0 ]; then
25
+ ln -s /opt/local/lib/ruby/gems/1.9.3/gems/*/bin/chef-client /opt/local/bin/chef-client ;
26
+ fi
27
+
28
+ test -h /opt/local/bin/chef-solo ;
29
+ if [ $? -gt 0 ]; then
30
+ ln -s /opt/local/lib/ruby/gems/1.9.3/gems/*/bin/chef-solo /opt/local/bin/chef-solo ;
31
+ fi
data/script/ci.sh ADDED
@@ -0,0 +1 @@
1
+ bundle exec rspec
@@ -0,0 +1,255 @@
1
+ require "spec_helper"
2
+
3
+ describe ManicBaker::Cli do
4
+ let(:config) { ManicBaker::Config.new }
5
+ let(:config_hash) { config.to_hash }
6
+
7
+ let(:dataset) { "some-image-or-other" }
8
+ let(:server_dataset) { dataset }
9
+
10
+ let(:fake_server) { double(:server, dataset: server_dataset, name: "this server is so great") }
11
+ let(:fake_server_collection) { [fake_server] }
12
+ let(:fake_joyent) { double(:joyent, servers: fake_server_collection) }
13
+
14
+ subject(:cli) { ManicBaker::Cli.new }
15
+
16
+ before do
17
+ fake_server.stub(reload: fake_server)
18
+ fake_server_collection.stub(reload: fake_server_collection)
19
+ cli.stub(config: config, joyent: fake_joyent, say: nil, say_status: nil)
20
+ end
21
+
22
+ describe "#launch" do
23
+ let(:new_fake_server) { double(:server, dataset: dataset, state: "running", name: "some new server") }
24
+
25
+ before do
26
+ fake_server_collection.stub(:create) do
27
+ fake_server_collection << new_fake_server
28
+ new_fake_server
29
+ end
30
+ end
31
+
32
+ context "with a dataset" do
33
+ let(:config_hash) { config.to_hash.merge("dataset" => dataset) }
34
+
35
+ context "when no instances exist with the dataset" do
36
+ let(:server_dataset) { "i-am-another-data-set" }
37
+
38
+ it "creates a new instance on joyent" do
39
+ fake_server_collection.should_receive(:create).with(config_hash)
40
+ cli.launch(dataset)
41
+ end
42
+
43
+ it "writes the dataset to the config file" do
44
+ expect do
45
+ cli.launch(dataset)
46
+ end.to change { config.dataset }.from(nil).to(dataset)
47
+ end
48
+ end
49
+
50
+ context "when an instance exists with the dataset" do
51
+ it "raises an error" do
52
+ expect { cli.launch(dataset) }.to raise_error(Thor::Error)
53
+ end
54
+ end
55
+ end
56
+
57
+ context "without a dataset" do
58
+ context "when the config has a dataset" do
59
+ let(:fake_server_collection) { [] }
60
+ let(:dataset) { "fancy-dataset" }
61
+
62
+ before { config.dataset = dataset }
63
+
64
+ it "does not change the dataset in the config file" do
65
+ expect { cli.launch }.to_not change { config.dataset }
66
+ end
67
+ end
68
+
69
+ context "when the config does not have a dataset" do
70
+ it "raises an error" do
71
+ expect { cli.launch }.to raise_error(Thor::Error)
72
+ end
73
+ end
74
+ end
75
+ end
76
+
77
+ describe "#panic" do
78
+ before do
79
+ fake_server.stub(destroy: nil)
80
+ end
81
+
82
+ context "with a dataset in the config" do
83
+ before { config.dataset = dataset }
84
+
85
+ context "when there is a server with the dataset" do
86
+ before { fake_server.stub(state: "stopped") }
87
+
88
+ it "destroys the server" do
89
+ fake_server.should_receive(:destroy)
90
+ cli.panic
91
+ end
92
+
93
+ context "when reloading the server raises 410 Gone" do
94
+ it "does not raise an error" do
95
+ fake_server.stub(:reload).and_raise(Excon::Errors::Gone.new("no"))
96
+ expect { cli.panic }.to_not raise_error
97
+ end
98
+ end
99
+ end
100
+
101
+ context "when the server has a different dataset" do
102
+ let(:server_dataset) { "guess-who" }
103
+
104
+ it "does not destroy the server" do
105
+ expect { cli.panic }.to raise_error(Thor::Error)
106
+ end
107
+ end
108
+ end
109
+
110
+ context "with no dataset in the config" do
111
+ before { config.dataset = nil }
112
+
113
+ it "raises an exception" do
114
+ expect { cli.panic }.to raise_error(Thor::Error)
115
+ end
116
+ end
117
+ end
118
+
119
+ describe "#ssh" do
120
+ before do
121
+ fake_server.stub(public_ip_address: "some-host")
122
+ cli.stub(exec: nil)
123
+ end
124
+
125
+ context "with a dataset in the config" do
126
+ before { config.dataset = dataset }
127
+
128
+ context "when there is a server with the dataset" do
129
+ it "starts an ssh session to the host" do
130
+ cli.should_receive(:exec).with("ssh -i #{config.private_key_path} root@some-host")
131
+ cli.ssh
132
+ end
133
+ end
134
+
135
+ context "when the server has a different dataset" do
136
+ let(:server_dataset) { "guess-who" }
137
+
138
+ it "does not destroy the server" do
139
+ expect { cli.ssh }.to raise_error(Thor::Error)
140
+ end
141
+ end
142
+ end
143
+
144
+ context "with no dataset in the config" do
145
+ before { config.dataset = nil }
146
+
147
+ it "raises an exception" do
148
+ expect { cli.ssh }.to raise_error(Thor::Error)
149
+ end
150
+ end
151
+ end
152
+
153
+ describe "#bootstrap" do
154
+ let(:fake_remote) { double(:remote, system!: nil, upload: nil) }
155
+
156
+ before { cli.stub(remote: fake_remote) }
157
+
158
+ context "with a dataset in the config" do
159
+ let(:script_path) { File.expand_path("../../../../script", __FILE__) }
160
+
161
+ before { config.dataset = dataset }
162
+
163
+ context "when the server is in the same dataset" do
164
+ it "uploads the script directory" do
165
+ fake_remote.should_receive(:upload).with("#{script_path}/", "script/")
166
+ cli.bootstrap
167
+ end
168
+
169
+ it "runs the bootstrap script" do
170
+ fake_remote.should_receive(:system!).with("script/bootstrap.sh")
171
+ cli.bootstrap
172
+ end
173
+ end
174
+
175
+ context "when the server has a different dataset" do
176
+ let(:server_dataset) { "i-am-so-tired-of-guessing" }
177
+
178
+ it "does not destroy the server" do
179
+ expect { cli.bootstrap }.to raise_error(Thor::Error)
180
+ end
181
+ end
182
+ end
183
+
184
+ context "with no dataset in the config" do
185
+ before { config.dataset = nil }
186
+
187
+ it "raises an exception" do
188
+ expect { cli.bootstrap }.to raise_error(Thor::Error)
189
+ end
190
+ end
191
+ end
192
+
193
+ describe "#chef" do
194
+ let(:fake_remote_config) { double(:remote_config) }
195
+
196
+ before { cli.stub(remote_config: fake_remote_config, install_cookbooks: nil) }
197
+
198
+ context "with a dataset in the config" do
199
+ before { config.dataset = dataset }
200
+
201
+ context "when there is a server with the dataset" do
202
+ before { Soloist::Spotlight.stub(find!: ".") }
203
+
204
+ it "runs chef using the remote config" do
205
+ fake_remote_config.should_receive(:run_chef)
206
+ cli.chef
207
+ end
208
+ end
209
+
210
+ context "when the server has a different dataset" do
211
+ let(:server_dataset) { "wait, there's more than one of these?" }
212
+
213
+ it "does not destroy the server" do
214
+ expect { cli.chef }.to raise_error(Thor::Error)
215
+ end
216
+ end
217
+ end
218
+
219
+ context "with no dataset in the config" do
220
+ before { config.dataset = nil }
221
+
222
+ it "raises an exception" do
223
+ expect { cli.chef }.to raise_error(Thor::Error)
224
+ end
225
+ end
226
+ end
227
+
228
+ describe "#snapshot" do
229
+ context "with a dataset in the config" do
230
+ before { config.dataset = dataset }
231
+
232
+ context "when there is a server with the dataset" do
233
+ it "starts an ssh session to the host" do
234
+ cli.snapshot("teeth")
235
+ end
236
+ end
237
+
238
+ context "when the server has a different dataset" do
239
+ let(:server_dataset) { "wait, there's more than one of these?" }
240
+
241
+ it "does not destroy the server" do
242
+ expect { cli.snapshot("toes") }.to raise_error(Thor::Error)
243
+ end
244
+ end
245
+ end
246
+
247
+ context "with no dataset in the config" do
248
+ before { config.dataset = nil }
249
+
250
+ it "raises an exception" do
251
+ expect { cli.snapshot("omg braces?") }.to raise_error(Thor::Error)
252
+ end
253
+ end
254
+ end
255
+ end
@@ -0,0 +1,84 @@
1
+ require "spec_helper"
2
+
3
+ describe ManicBaker::Config do
4
+ let(:contents) { { "recipes" => ["broken_vim"] } }
5
+ let(:tempfile) do
6
+ Tempfile.new("manic-baker-config").tap do |file|
7
+ file.write(YAML.dump(contents))
8
+ file.close
9
+ end
10
+ end
11
+
12
+ let(:royal_crown) { ManicBaker::Config.from_file(tempfile.path) }
13
+
14
+ describe "defaults" do
15
+ its(:dataset) { should be_nil }
16
+ its(:flavor) { should == "Small 1GB" }
17
+ its(:joyent_uri) { should == "https://us-east-1.api.joyentcloud.com" }
18
+ its(:private_key_path) { should == File.expand_path("~/.ssh/id_rsa") }
19
+ its(:public_key_path) { should == File.expand_path("~/.ssh/id_rsa.pub") }
20
+ end
21
+
22
+ describe ".from_file" do
23
+ context "when the rc file is empty" do
24
+ let(:tempfile) do
25
+ Tempfile.new("manic-baker-config").tap do |file|
26
+ file.close
27
+ end
28
+ end
29
+
30
+ it "loads an empty file" do
31
+ expect { royal_crown }.not_to raise_error
32
+ end
33
+ end
34
+
35
+ it "loads from a yaml file" do
36
+ royal_crown.recipes.should =~ ["broken_vim"]
37
+ end
38
+
39
+ it "defaults nil fields to an empty primitive" do
40
+ royal_crown.node_attributes.should == {}
41
+ end
42
+
43
+ context "when the rc file has ERB tags" do
44
+ let(:tempfile) do
45
+ Tempfile.new("manic-baker-config").tap do |file|
46
+ file.write(<<-YAML
47
+ recipes:
48
+ - broken_vim
49
+ node_attributes:
50
+ evaluated: <%= "From ERB" %>
51
+ YAML
52
+ )
53
+ file.close
54
+ end
55
+ end
56
+
57
+ it "evaluates the ERB and parses the resulting YAML" do
58
+ royal_crown.node_attributes.should == {
59
+ "evaluated" => "From ERB"
60
+ }
61
+ royal_crown.recipes.should =~ ["broken_vim"]
62
+ end
63
+ end
64
+ end
65
+
66
+ describe "#save" do
67
+ it "writes the values to a file" do
68
+ royal_crown.recipes = ["hot_rats", "tissue_paper"]
69
+ royal_crown.save
70
+ royal_crown = ManicBaker::Config.from_file(tempfile.path)
71
+ royal_crown.recipes.should =~ ["hot_rats", "tissue_paper"]
72
+ end
73
+ end
74
+
75
+ describe "#to_yaml" do
76
+ it "skips the path attribute" do
77
+ royal_crown.to_yaml.keys.should_not include "path"
78
+ end
79
+
80
+ it "nils out fields that have not been set" do
81
+ royal_crown.to_yaml["node_attributes"].should be_nil
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,64 @@
1
+ require "spec_helper"
2
+
3
+ describe ManicBaker::RemoteConfig do
4
+ let(:contents) { {} }
5
+ let(:tempfile) do
6
+ Tempfile.new("manic-baker-config").tap do |file|
7
+ file.write(YAML.dump(contents))
8
+ file.close
9
+ end
10
+ end
11
+ let(:config) { ManicBaker::Config.new(:path => tempfile.path) }
12
+ let(:remote) { Soloist::Remote.new("user", "host", "key") }
13
+ let(:remote_config) { ManicBaker::RemoteConfig.new(config, remote) }
14
+
15
+ before { remote.stub(:backtick => "", :system => 0) }
16
+
17
+ def commands_for(method)
18
+ [].tap do |commands|
19
+ remote.stub(:system) { |c| commands << c; 0 }
20
+ remote.stub(:backtick) { |c| commands << c; "" }
21
+ remote_config.send(method)
22
+ end
23
+ end
24
+
25
+ describe "#chef_config_path" do
26
+ it "sets the path" do
27
+ remote_config.chef_config_path.should == "/etc/chef"
28
+ end
29
+
30
+ it "creates the path remotely" do
31
+ commands_for(:chef_config_path).tap do |commands|
32
+ commands.should have(1).command
33
+ commands.first.should =~ /mkdir .*? -p \/etc\/chef$/
34
+ end
35
+ end
36
+ end
37
+
38
+ describe "#chef_cache_path" do
39
+ it "sets the path" do
40
+ remote_config.chef_cache_path.should == "/var/chef/cache"
41
+ end
42
+
43
+ it "creates the path remotely" do
44
+ commands_for(:chef_cache_path).tap do |commands|
45
+ commands.should have(1).command
46
+ commands.first.should =~ /mkdir .*? -p \/var\/chef\/cache$/
47
+ end
48
+ end
49
+ end
50
+
51
+ describe "#cookbook_paths" do
52
+ it "sets the path" do
53
+ remote_config.cookbook_paths.should have(1).path
54
+ remote_config.cookbook_paths.should =~ ["/var/chef/cookbooks"]
55
+ end
56
+
57
+ it "creates the path remotely" do
58
+ commands_for(:cookbook_paths).tap do |commands|
59
+ commands.should have(1).command
60
+ commands.first.should =~ /mkdir .*? -p \/var\/chef\/cookbooks$/
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,103 @@
1
+ require "spec_helper"
2
+
3
+ describe Thor::Shell::Mean do
4
+ let(:contents) { { "failure" => { "binge_drinking" => ["your %s is over"] } } }
5
+
6
+ class MeanHarness
7
+ include Thor::Shell::Mean
8
+
9
+ def say(string, _ = nil, newline = true)
10
+ newline ? puts(string) : print(string)
11
+ end
12
+ def say_status(*args)
13
+ say(args)
14
+ end
15
+ def set_color(string, *args)
16
+ string
17
+ end
18
+ end
19
+
20
+ subject(:mean) { MeanHarness.new }
21
+
22
+ before { mean.stub(messages: contents) }
23
+
24
+ describe "attributes" do
25
+ context "when the messages path has been set" do
26
+ its(:messages) { should have_key("failure") }
27
+ its(:'messages.values.first') { should have_key("binge_drinking") }
28
+ end
29
+ end
30
+
31
+ describe "#message_for" do
32
+ let(:parameters) { ["life"] }
33
+ let(:topic) { :binge_drinking }
34
+ let(:state) { :failure }
35
+
36
+ context "when the topic exists" do
37
+ context "when the state exists" do
38
+ specify { mean.message_for(topic, state, *parameters).should == "your life is over" }
39
+ end
40
+
41
+ context "when the state does not exist" do
42
+ let(:state) { :success }
43
+
44
+ specify { expect { mean.message_for(topic, state, *parameters) }.to raise_error(NoMethodError) }
45
+ end
46
+ end
47
+
48
+ context "when the topic does not exist" do
49
+ let(:topic) { :empathy }
50
+
51
+ specify { expect { mean.message_for(topic, state, *parameters) }.to raise_error(NoMethodError) }
52
+ end
53
+
54
+ context "when there are not enough parameters" do
55
+ specify { expect { mean.message_for(topic, state) }.to raise_error(ArgumentError) }
56
+ end
57
+ end
58
+
59
+ describe "#say_message_for" do
60
+ let(:output) do
61
+ capture(:stdout) do
62
+ mean.say_message_for(:binge_drinking, :failure, "relationship")
63
+ end
64
+ end
65
+
66
+ specify { output.should include "binge_drinking" }
67
+ specify { output.should include "your relationship is over" }
68
+ end
69
+
70
+ describe "#say_until" do
71
+ context "with a state that is always true" do
72
+ it "does not pass arguments to say" do
73
+ mean.should_not_receive(:say).with(:anything)
74
+ mean.should_receive(:say).with(no_args)
75
+ mean.say_until(:anything) { true }
76
+ end
77
+ end
78
+
79
+ context "with a state that transitions from false to true" do
80
+ it "passes arguments to say" do
81
+ mean.should_receive(:say).once.with(:anything)
82
+ mean.should_receive(:say).with(no_args)
83
+ index = 0
84
+ mean.say_until(:anything) { (index += 1) == 2 }
85
+ end
86
+ end
87
+ end
88
+
89
+ describe "#say_waiting" do
90
+ let(:output) { capture(:stdout) { mean.say_waiting } }
91
+
92
+ specify { output.should include "waiting" }
93
+ specify { output.should_not include "\n" }
94
+ end
95
+
96
+ describe "#say_boring" do
97
+ let(:contents) { { "boring" => ["ugh"] } }
98
+ let(:output) { capture(:stdout) { mean.say_boring } }
99
+
100
+ specify { output.should include "ugh" }
101
+ specify { output.should include "waiting" }
102
+ end
103
+ end
@@ -0,0 +1,13 @@
1
+ $:<<File.expand_path("../../lib", __FILE__)
2
+
3
+ require "manic_baker"
4
+
5
+ Dir.glob(File.expand_path("../support/**/*.rb", __FILE__)) { |f| require f }
6
+
7
+ RSpec.configure do |config|
8
+ config.include CaptureStreamHelpers
9
+ config.treat_symbols_as_metadata_keys_with_true_values = true
10
+ config.run_all_when_everything_filtered = true
11
+ config.filter_run :focus
12
+ config.order = 'random'
13
+ end
@@ -0,0 +1,14 @@
1
+ module CaptureStreamHelpers
2
+ def capture(stream)
3
+ begin
4
+ stream = stream.to_s
5
+ eval "$#{stream} = StringIO.new"
6
+ yield
7
+ result = eval("$#{stream}").string
8
+ ensure
9
+ eval("$#{stream} = #{stream.upcase}")
10
+ end
11
+
12
+ result
13
+ end
14
+ end
metadata ADDED
@@ -0,0 +1,161 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: manic_baker
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Doc Ritezel
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-06-30 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: fog
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
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: soloist
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
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: rspec
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: guard-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
+ - !ruby/object:Gem::Dependency
70
+ name: guard-bundler
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '>='
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: gem-release
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - '>='
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - '>='
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ description: Makes Joyent less manic
98
+ email:
99
+ - doc@minifast.co
100
+ executables:
101
+ - manic
102
+ extensions: []
103
+ extra_rdoc_files: []
104
+ files:
105
+ - .gitignore
106
+ - .rspec
107
+ - .ruby-gemset
108
+ - .ruby-version
109
+ - .travis.yml
110
+ - Gemfile
111
+ - Guardfile
112
+ - LICENSE.txt
113
+ - README.md
114
+ - bin/manic
115
+ - config/locales/en.yml
116
+ - lib/manic_baker.rb
117
+ - lib/manic_baker/cli.rb
118
+ - lib/manic_baker/config.rb
119
+ - lib/manic_baker/remote_config.rb
120
+ - lib/manic_baker/version.rb
121
+ - lib/thor/shell/mean.rb
122
+ - manic_baker.gemspec
123
+ - script/bootstrap.sh
124
+ - script/ci.sh
125
+ - spec/lib/manic_baker/cli_spec.rb
126
+ - spec/lib/manic_baker/config_spec.rb
127
+ - spec/lib/manic_baker/remote_config_spec.rb
128
+ - spec/lib/thor/shell/mean_spec.rb
129
+ - spec/spec_helper.rb
130
+ - spec/support/capture_stream_helpers.rb
131
+ homepage: https://github.com/minifast/manic_baker
132
+ licenses:
133
+ - MIT
134
+ metadata: {}
135
+ post_install_message:
136
+ rdoc_options: []
137
+ require_paths:
138
+ - lib
139
+ required_ruby_version: !ruby/object:Gem::Requirement
140
+ requirements:
141
+ - - '>='
142
+ - !ruby/object:Gem::Version
143
+ version: '0'
144
+ required_rubygems_version: !ruby/object:Gem::Requirement
145
+ requirements:
146
+ - - '>='
147
+ - !ruby/object:Gem::Version
148
+ version: '0'
149
+ requirements: []
150
+ rubyforge_project:
151
+ rubygems_version: 2.0.2
152
+ signing_key:
153
+ specification_version: 4
154
+ summary: Stop calling the Ghostbusters just because you don't like my music, mom
155
+ test_files:
156
+ - spec/lib/manic_baker/cli_spec.rb
157
+ - spec/lib/manic_baker/config_spec.rb
158
+ - spec/lib/manic_baker/remote_config_spec.rb
159
+ - spec/lib/thor/shell/mean_spec.rb
160
+ - spec/spec_helper.rb
161
+ - spec/support/capture_stream_helpers.rb