manic_baker 0.1.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 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