terraform 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,11 +1,12 @@
1
- Terraform is a small goal-oriented DSL for setting up a machine, similar in purpose to Chef and
2
- Puppet. Its design is inspired by Babushka, but it's simpler and tailored specifically for provisioning a machine for a webapp.
1
+ Terraform is a small goal-oriented DSL for setting up a machine, similar in purpose to Chef and Puppet. Its
2
+ design is inspired by Babushka, but it's simpler and tailored specifically for provisioning a machine for a
3
+ webapp.
3
4
 
4
5
  Usage
5
6
  -----
6
7
 
7
8
  require "terraform_dsl"
8
- include TerraformDsl
9
+ include Terraform::Dsl
9
10
  dep "my library" do
10
11
  met? { (check if your dependency is met) }
11
12
  meet { (install your dependency) }
@@ -15,10 +16,11 @@ A more detailed README is coming shortly.
15
16
 
16
17
  Contribute
17
18
  ----------
18
- When editing this gem, to test your changes, you can load your local copy of the gem in your project by using this in your Gemfile:
19
+ When editing this gem, to test your changes, you can load your local copy of the gem in your project by using
20
+ this in your Gemfile:
19
21
 
20
22
  gem "terraform", :path => "~/p/terraform"
21
23
 
22
24
  Credits
23
25
  -------
24
- Dmac -- thanks for the name!
26
+ Dmac -- thanks for the name!
data/Rakefile CHANGED
@@ -1 +1,11 @@
1
1
  require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ task :test => ["test:units"]
5
+
6
+ namespace :test do
7
+ Rake::TestTask.new(:units) do |task|
8
+ task.libs << "test"
9
+ task.test_files = FileList["test/unit/**/*_test.rb"]
10
+ end
11
+ end
@@ -5,6 +5,6 @@ module Terraform
5
5
  # Writes the terraform_dsl.rb to the given file or directory.
6
6
  def self.write_dsl_file(path)
7
7
  path = File.join(path, "terraform_dsl.rb") if File.directory?(path)
8
- FileUtils.cp(File.expand_path(File.join(File.dirname(__FILE__), "terraform/terraform_dsl.rb")), path)
8
+ FileUtils.cp(File.expand_path(File.join(File.dirname(__FILE__), "terraform/dsl.rb")), path)
9
9
  end
10
10
  end
@@ -0,0 +1,144 @@
1
+ require "fileutils"
2
+ require "digest/md5"
3
+
4
+ module Terraform
5
+ module Dsl
6
+ def dep(name)
7
+ @dependencies ||= []
8
+ # If a dep gets required or defined twice, only run it once.
9
+ return if @dependencies.find { |dep| dep[:name] == name }
10
+ @dependencies.push(@current_dependency = { :name => name })
11
+ yield
12
+ fail_and_exit "Error: no 'met?' block defined for dep '#{name}'." unless @current_dependency[:met?]
13
+ fail_and_exit "Error: no 'meet' block defined for dep '#{name}'." unless @current_dependency[:meet]
14
+ end
15
+ def met?(&block) @current_dependency[:met?] = block end
16
+ def meet(&block) @current_dependency[:meet] = block end
17
+ def in_path?(command) `which #{command}`.size > 0 end
18
+ def fail_and_exit(message) puts message; exit 1 end
19
+
20
+ # Runs a command and raises an exception if its exit status was nonzero.
21
+ # - silent: if false, log the command being run and its stdout. False by default.
22
+ # - check_exit_code: raises an error if the command had a non-zero exit code. True by default.
23
+ def shell(command, options = {})
24
+ silent = (options[:silent] != false)
25
+ puts command unless silent
26
+ output = `#{command}`
27
+ puts output unless output.empty? || silent
28
+ raise "#{command} had a failure exit status of #{$?.to_i}" unless $?.to_i == 0
29
+ true
30
+ end
31
+
32
+ def satisfy_dependencies
33
+ STDOUT.sync = true # Ensure that we flush logging output as we go along.
34
+ @dependencies.each do |dep|
35
+ unless dep[:met?].call
36
+ puts "* Dependency #{dep[:name]} is not met. Meeting it."
37
+ dep[:meet].call
38
+ fail_and_exit %Q("met?" for #{dep[:name]} is still false after running "meet".) unless dep[:met?].call
39
+ end
40
+ end
41
+ end
42
+
43
+ #
44
+ # These are very common tasks which are needed by almost everyone, and so they're bundled with this DSL.
45
+ #
46
+
47
+ def package_installed?(package) `dpkg -s #{package} 2> /dev/null | grep Status`.match(/\sinstalled/) end
48
+ def install_package(package)
49
+ # Specify a noninteractive frontend, so dpkg won't prompt you for info. -q is quiet; -y is "answer yes".
50
+ shell "sudo DEBIAN_FRONTEND=noninteractive apt-get install -qy #{package}"
51
+ end
52
+
53
+ def ensure_packages(*packages) packages.each { |package| ensure_package(package) } end
54
+ def ensure_package(package)
55
+ dep "package: #{package}" do
56
+ met? { package_installed?(package) }
57
+ meet { install_package(package) }
58
+ end
59
+ end
60
+
61
+ # Ensure an Ubuntu PPA is installed. The argument is the ppa location, in the form ppa:[USER]/[NAME]
62
+ def ensure_ppa(ppa)
63
+ ppa_part, location = ppa.split(":", 2)
64
+ fail_and_exit("PPA location must be of the form ppa:[USER]/[NAME]") unless (ppa_part == "ppa") && location
65
+ # The python-software-properties package provides the add-apt-repository convenience tool for editing
66
+ # the list of apt's source repositories.
67
+ ensure_package("python-software-properties")
68
+ dep "ppa: #{location}" do
69
+ met? { !`apt-cache policy 2> /dev/null | grep ppa.launchpad.net/#{location}/`.empty? }
70
+ meet do
71
+ shell "sudo add-apt-repository #{ppa}"#, :silent => true
72
+ shell "sudo apt-get update"#, :silent => true
73
+ end
74
+ end
75
+ end
76
+
77
+ def gem_installed?(gem) `gem list '#{gem}'`.include?(gem) end
78
+
79
+ def ensure_gem(gem)
80
+ dep "gem: #{gem}" do
81
+ met? { gem_installed?(gem) }
82
+ meet { shell "gem install #{gem} --no-ri --no-rdoc" }
83
+ end
84
+ end
85
+
86
+ # Ensures the file at dest_path is exactly the same as the one in source_path.
87
+ # Invokes the given block if the file is changed. Use this block to restart a service, for instance.
88
+ def ensure_file(source_path, dest_path, &on_change)
89
+ dep "file: #{dest_path}" do
90
+ met? do
91
+ raise "This file does not exist: #{source_path}" unless File.exists?(source_path)
92
+ File.exists?(dest_path) && (Digest::MD5.file(source_path) == Digest::MD5.file(dest_path))
93
+ end
94
+ meet do
95
+ FileUtils.cp(source_path, dest_path)
96
+ on_change.call if on_change
97
+ end
98
+ end
99
+ end
100
+
101
+ # A task which must be run once to be 'met'. For instance, this might be the DB migration script.
102
+ def ensure_run_once(name, &block)
103
+ dep "run task once: #{name}" do
104
+ has_run_once = false
105
+ met? { has_run_once }
106
+ meet do
107
+ yield
108
+ has_run_once = true
109
+ end
110
+ end
111
+ end
112
+
113
+ def ensure_rbenv
114
+ ensure_package "git-core"
115
+ dep "rbenv" do
116
+ met? { in_path?("rbenv") }
117
+ meet do
118
+ # These instructions are from https://github.com/sstephenson/rbenv/wiki/Using-rbenv-in-Production
119
+ shell "wget -q -O - https://raw.github.com/fesplugas/rbenv-installer/master/bin/rbenv-installer | bash"
120
+ # We need to run rbenv init after install, which adjusts the path. If exec is causing us problems
121
+ # down the road, we can perhaps simulate running rbenv init without execing.
122
+ unless ARGV.include?("--forked-after-rbenv") # To guard against an infinite forking loop.
123
+ exec "bash -c 'source ~/.bashrc; #{$0} --forked-after-rbenv'" # $0 is the current process's name.
124
+ end
125
+ end
126
+ end
127
+ end
128
+
129
+ # ruby_version is a rbenv ruby version string like "1.9.2-p290".
130
+ def ensure_rbenv_ruby(ruby_version)
131
+ ensure_rbenv
132
+ ensure_packages "curl", "build-essential", "libxslt1-dev", "libxml2-dev", "libssl-dev"
133
+
134
+ dep "rbenv ruby: #{ruby_version}" do
135
+ met? { `which ruby`.include?("rbenv") && `ruby -v`.include?(ruby_version.gsub("-", "")) }
136
+ meet do
137
+ puts "Compiling Ruby will take a few minutes."
138
+ shell "rbenv install #{ruby_version}"
139
+ shell "rbenv rehash"
140
+ end
141
+ end
142
+ end
143
+ end
144
+ end
@@ -1,3 +1,3 @@
1
1
  module Terraform
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
@@ -15,6 +15,9 @@ Gem::Specification.new do |s|
15
15
 
16
16
  s.files = `git ls-files`.split("\n")
17
17
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
- s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
19
19
  s.require_paths = ["lib"]
20
+
21
+ s.add_development_dependency "scope", "~> 0.2.3"
22
+ s.add_development_dependency "rr", "~> 1.0.4"
20
23
  end
@@ -0,0 +1,24 @@
1
+ require "bundler/setup"
2
+ require "minitest/autorun"
3
+ require "scope"
4
+ require "rr"
5
+ require "stringio"
6
+
7
+ $:.unshift(File.join(File.dirname(__FILE__), "../lib"))
8
+
9
+ module Scope
10
+ class TestCase
11
+ include RR::Adapters::MiniTest
12
+ end
13
+ end
14
+
15
+ module Kernel
16
+ def capture_output
17
+ result = StringIO.new
18
+ $stdout = result
19
+ yield
20
+ result.string
21
+ ensure
22
+ $stdout = STDOUT
23
+ end
24
+ end
@@ -0,0 +1,97 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), "../../test_helper.rb"))
2
+ require "terraform/dsl"
3
+
4
+ class DslTest < Scope::TestCase
5
+ include Terraform::Dsl
6
+
7
+ class ProcessExit < StandardError; end
8
+
9
+ # Methods to mock for met? and meet blocks
10
+ def do_met() end
11
+ def do_meet() end
12
+
13
+ setup do
14
+ stub(self).fail_and_exit(anything) { raise ProcessExit }
15
+ end
16
+
17
+ context "dep declarations" do
18
+ should "fail if there is no met? or meet given" do
19
+ assert_raises(ProcessExit) do
20
+ dep("no met?") { meet {} }
21
+ end
22
+ assert_raises(ProcessExit) do
23
+ dep("no meet") { met? {} }
24
+ end
25
+ end
26
+
27
+ should "not run meet if met? is satisfied" do
28
+ mock(self).do_met { true }
29
+ mock(self).do_meet.never
30
+ dep "foo" do
31
+ met? { do_met }
32
+ meet { do_meet }
33
+ end
34
+ satisfy_dependencies
35
+ end
36
+
37
+ should "run meet if met? is not satisfied" do
38
+ met_run = false
39
+ mock(self).do_met.twice { result = met_run; met_run = true; result }
40
+ mock(self).do_meet
41
+ dep "foo" do
42
+ met? { do_met }
43
+ meet { do_meet }
44
+ end
45
+ message = capture_output { satisfy_dependencies }
46
+ assert_match /Dependency foo is not met/, message
47
+ end
48
+
49
+ should "only meet a dep once" do
50
+ met_run = false
51
+ mock(self).do_met.twice { result = met_run; met_run = true; result }
52
+ mock(self).do_meet
53
+ 2.times do
54
+ dep "foo" do
55
+ met? { do_met }
56
+ meet { do_meet }
57
+ end
58
+ end
59
+ capture_output { satisfy_dependencies }
60
+ end
61
+
62
+ should "fail if met? still fails after running meet" do
63
+ mock(self).do_met.twice { false }
64
+ mock(self).do_meet
65
+ dep "foo" do
66
+ met? { do_met }
67
+ meet { do_meet }
68
+ end
69
+ assert_raises(ProcessExit) { capture_output { satisfy_dependencies } }
70
+ end
71
+ end
72
+
73
+ context "ensure_ppa" do
74
+ should "fail if the PPA name is not the expected form" do
75
+ assert_raises(ProcessExit) { ensure_ppa("blah") }
76
+ assert_raises(ProcessExit) { ensure_ppa("http://some.ppa.url") }
77
+ end
78
+
79
+ # TODO(caleb): Add more unit tests. Having trouble coming up with unit tests that don't feel
80
+ # fragile/artificial. For helpers like these, integration tests of some kind that run on a vagrant box or
81
+ # something may prove more useful.
82
+ end
83
+
84
+ context "ensure_run_once" do
85
+ should "run a task exactly once" do
86
+ mock(self).do_meet.never
87
+ meet_run = false
88
+ ensure_run_once("foo") { meet_run = true }
89
+ dep "run task once: foo" do
90
+ met? { do_met }
91
+ meet { do_meet }
92
+ end
93
+ capture_output { satisfy_dependencies }
94
+ assert meet_run
95
+ end
96
+ end
97
+ end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: terraform
3
3
  version: !ruby/object:Gem::Version
4
- hash: 29
4
+ hash: 27
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
8
  - 0
9
- - 1
10
- version: 0.0.1
9
+ - 2
10
+ version: 0.0.2
11
11
  platform: ruby
12
12
  authors:
13
13
  - Phil Crosby
@@ -15,10 +15,41 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2012-03-24 00:00:00 -07:00
18
+ date: 2012-03-27 00:00:00 -07:00
19
19
  default_executable:
20
- dependencies: []
21
-
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: scope
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ hash: 17
30
+ segments:
31
+ - 0
32
+ - 2
33
+ - 3
34
+ version: 0.2.3
35
+ type: :development
36
+ version_requirements: *id001
37
+ - !ruby/object:Gem::Dependency
38
+ name: rr
39
+ prerelease: false
40
+ requirement: &id002 !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ hash: 31
46
+ segments:
47
+ - 1
48
+ - 0
49
+ - 4
50
+ version: 1.0.4
51
+ type: :development
52
+ version_requirements: *id002
22
53
  description:
23
54
  email:
24
55
  - phil.crosby@gmail.com
@@ -34,9 +65,11 @@ files:
34
65
  - README.markdown
35
66
  - Rakefile
36
67
  - lib/terraform.rb
37
- - lib/terraform/terraform_dsl.rb
68
+ - lib/terraform/dsl.rb
38
69
  - lib/terraform/version.rb
39
70
  - terraform.gemspec
71
+ - test/test_helper.rb
72
+ - test/unit/terraform/dsl_test.rb
40
73
  has_rdoc: true
41
74
  homepage: http://github.com/philc/terraform
42
75
  licenses: []
@@ -71,5 +104,6 @@ rubygems_version: 1.6.2
71
104
  signing_key:
72
105
  specification_version: 3
73
106
  summary: Set up a cold, inhospitable system using Terraform.
74
- test_files: []
75
-
107
+ test_files:
108
+ - test/test_helper.rb
109
+ - test/unit/terraform/dsl_test.rb
@@ -1,125 +0,0 @@
1
- # This is small goal-oriented DSL for installing system components, similar in purpose to Chef and Puppet.
2
- # Its design is inspired by Babushka but it's simpler and tailored specifically for provisioning a machine
3
- # for a webapp.
4
- #
5
- # Usage:
6
- #
7
- # require "terraform_dsl"
8
- # include TerraformDsl
9
- # dep "my library" do
10
- # met? { (check if your dependency is met) }
11
- # meet { (install your dependency) }
12
- # end
13
-
14
- require "fileutils"
15
- require "digest/md5"
16
-
17
- module TerraformDsl
18
- def dep(name)
19
- @dependencies ||= []
20
- # If a dep gets required or defined twice, only run it once.
21
- return if @dependencies.find { |dep| dep[:name] == name }
22
- @dependencies.push(@current_dependency = { :name => name })
23
- yield
24
- end
25
- def met?(&block) @current_dependency[:met?] = block end
26
- def meet(&block) @current_dependency[:meet] = block end
27
- def in_path?(command) `which #{command}`.size > 0 end
28
- def fail_and_exit(message) puts message; exit 1 end
29
-
30
- # Runs a command and raises an exception if its exit status was nonzero.
31
- # - silent: if false, log the command being run and its stdout. False by default.
32
- # - check_exit_code: raises an error if the command had a non-zero exit code. True by default.
33
- def shell(command, options = {})
34
- silent = (options[:silent] != false)
35
- puts command unless silent
36
- output = `#{command}`
37
- puts output unless output.empty? || silent
38
- raise "#{command} had a failure exit status of #{$?.to_i}" unless $?.to_i == 0
39
- true
40
- end
41
-
42
- def satisfy_dependencies
43
- STDOUT.sync = true # Ensure that we flush logging output as we go along.
44
- @dependencies.each do |dep|
45
- unless dep[:met?].call
46
- puts "* Dependency #{dep[:name]} is not met. Meeting it."
47
- dep[:meet].call
48
- fail_and_exit %Q("met?" for #{dep[:name]} is still false after running "meet".) unless dep[:met?].call
49
- end
50
- end
51
- end
52
-
53
- #
54
- # These are very common tasks which are needed by almost everyone, and so they're bundled with this DSL.
55
- #
56
-
57
- def package_installed?(package) `dpkg -s #{package} 2> /dev/null | grep Status`.match(/\sinstalled/) end
58
- def install_package(package)
59
- # Specify a noninteractive frontend, so dpkg won't prompt you for info. -q is quiet; -y is "answer yes".
60
- shell "sudo DEBIAN_FRONTEND=noninteractive apt-get install -qy #{package}"
61
- end
62
-
63
- def ensure_packages(*packages) packages.each { |package| ensure_package(package) } end
64
- def ensure_package(package)
65
- dep package do
66
- met? { package_installed?(package) }
67
- meet { install_package(package) }
68
- end
69
- end
70
-
71
- def gem_installed?(gem) `gem list '#{gem}'`.include?(gem) end
72
-
73
- def ensure_gem(gem)
74
- dep gem do
75
- met? { gem_installed?(gem) }
76
- meet { shell "gem install #{gem} --no-ri --no-rdoc" }
77
- end
78
- end
79
-
80
- # Ensures the file at dest_path is exactly the same as the one in source_path.
81
- # Invokes the given block if the file is changed. Use this block to restart a service, for instance.
82
- def ensure_file(source_path, dest_path, &on_change)
83
- dep dest_path do
84
- met? do
85
- raise "This file does not exist: #{source_path}" unless File.exists?(source_path)
86
- File.exists?(dest_path) && (Digest::MD5.file(source_path) == Digest::MD5.file(dest_path))
87
- end
88
- meet do
89
- FileUtils.cp(source_path, dest_path)
90
- on_change.call if on_change
91
- end
92
- end
93
- end
94
-
95
- def ensure_rbenv
96
- ensure_package "git-core"
97
- dep "rbenv" do
98
- met? { in_path?("rbenv") }
99
- meet do
100
- # These instructions are from https://github.com/sstephenson/rbenv/wiki/Using-rbenv-in-Production
101
- shell "wget -q -O - https://raw.github.com/fesplugas/rbenv-installer/master/bin/rbenv-installer | bash"
102
- # We need to run rbenv init after install, which adjusts the path. If exec is causing us problems
103
- # down the road, we can perhaps simulate running rbenv init without execing.
104
- unless ARGV.include?("--forked-after-rbenv") # To guard against an infinite forking loop.
105
- exec "bash -c 'source ~/.bashrc; #{$0} --forked-after-rbenv'" # $0 is the current process's name.
106
- end
107
- end
108
- end
109
- end
110
-
111
- # ruby_version is a rbenv ruby version string like "1.9.2-p290".
112
- def ensure_rbenv_ruby(ruby_version)
113
- ensure_rbenv
114
- ensure_packages "curl", "build-essential", "libxslt1-dev", "libxml2-dev", "libssl-dev"
115
-
116
- dep "rbenv ruby #{ruby_version}" do
117
- met? { `which ruby`.include?("rbenv") && `ruby -v`.include?(ruby_version.gsub("-", "")) }
118
- meet do
119
- puts "Compiling Ruby will take a few minutes."
120
- shell "rbenv install #{ruby_version}"
121
- shell "rbenv rehash"
122
- end
123
- end
124
- end
125
- end