itamae 1.0.0.beta1

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.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +19 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +6 -0
  5. data/Gemfile +14 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +60 -0
  8. data/Rakefile +62 -0
  9. data/bin/itamae +5 -0
  10. data/example/foo +1 -0
  11. data/example/node.json +1 -0
  12. data/example/recipe.rb +15 -0
  13. data/itamae.gemspec +32 -0
  14. data/lib/itamae.rb +13 -0
  15. data/lib/itamae/cli.rb +23 -0
  16. data/lib/itamae/logger.rb +44 -0
  17. data/lib/itamae/node.rb +9 -0
  18. data/lib/itamae/recipe.rb +46 -0
  19. data/lib/itamae/resources.rb +25 -0
  20. data/lib/itamae/resources/base.rb +151 -0
  21. data/lib/itamae/resources/directory.rb +26 -0
  22. data/lib/itamae/resources/file.rb +36 -0
  23. data/lib/itamae/resources/package.rb +15 -0
  24. data/lib/itamae/resources/remote_file.rb +16 -0
  25. data/lib/itamae/resources/template.rb +19 -0
  26. data/lib/itamae/runner.rb +49 -0
  27. data/lib/itamae/specinfra.rb +44 -0
  28. data/lib/itamae/version.rb +3 -0
  29. data/spec/integration/Vagrantfile +19 -0
  30. data/spec/integration/default_spec.rb +33 -0
  31. data/spec/integration/recipes/default.rb +26 -0
  32. data/spec/integration/recipes/hello.erb +1 -0
  33. data/spec/integration/recipes/hello.txt +1 -0
  34. data/spec/integration/recipes/node.json +3 -0
  35. data/spec/integration/spec_helper.rb +45 -0
  36. data/spec/unit/lib/itamae/logger_spec.rb +20 -0
  37. data/spec/unit/lib/itamae/node_spec.rb +6 -0
  38. data/spec/unit/lib/itamae/recipe_spec.rb +6 -0
  39. data/spec/unit/lib/itamae/resources/base_spec.rb +135 -0
  40. data/spec/unit/lib/itamae/resources/package_spec.rb +18 -0
  41. data/spec/unit/lib/itamae/resources/remote_file_spec.rb +23 -0
  42. data/spec/unit/lib/itamae/resources_spec.rb +14 -0
  43. data/spec/unit/lib/itamae/runner_spec.rb +29 -0
  44. data/spec/unit/spec_helper.rb +22 -0
  45. metadata +230 -0
@@ -0,0 +1,26 @@
1
+ require 'itamae'
2
+
3
+ module Itamae
4
+ module Resources
5
+ class Directory < Base
6
+ define_option :action, default: :create
7
+ define_option :path, type: String, default_name: true
8
+ define_option :mode, type: String
9
+ define_option :owner, type: String
10
+ define_option :group, type: String
11
+
12
+ def create_action
13
+ if ! backend.check_file_is_directory(path)
14
+ backend.create_file_as_directory(path)
15
+ end
16
+ if options[:mode]
17
+ backend.change_file_mode(path, options[:mode])
18
+ end
19
+ if options[:owner] || options[:group]
20
+ backend.change_file_owner(path, options[:owner], options[:group])
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+
@@ -0,0 +1,36 @@
1
+ require 'itamae'
2
+
3
+ module Itamae
4
+ module Resources
5
+ class File < Base
6
+ define_option :action, default: :create
7
+ define_option :path, type: String, default_name: true
8
+ define_option :content, type: String, default: ''
9
+ define_option :content_file, type: String
10
+ define_option :mode, type: String
11
+ define_option :owner, type: String
12
+ define_option :group, type: String
13
+
14
+ def create_action
15
+ src = if content_file
16
+ content_file
17
+ else
18
+ Tempfile.open('itamae') do |f|
19
+ f.write(content)
20
+ f.path
21
+ end
22
+ end
23
+
24
+ copy_file(src, path)
25
+
26
+ if mode
27
+ backend.change_file_mode(path, mode)
28
+ end
29
+ if owner || group
30
+ backend.change_file_owner(path, owner, group)
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+
@@ -0,0 +1,15 @@
1
+ require 'itamae'
2
+
3
+ module Itamae
4
+ module Resources
5
+ class Package < Base
6
+ define_option :action, default: :install
7
+ define_option :name, type: String, default_name: true
8
+
9
+ def install_action
10
+ run_specinfra(:install_package, name)
11
+ end
12
+ end
13
+ end
14
+ end
15
+
@@ -0,0 +1,16 @@
1
+ require 'itamae'
2
+
3
+ module Itamae
4
+ module Resources
5
+ class RemoteFile < File
6
+ define_option :source, type: String, required: true
7
+
8
+ def create_action
9
+ content_file(::File.expand_path(source, ::File.dirname(@recipe.path)))
10
+
11
+ super
12
+ end
13
+ end
14
+ end
15
+ end
16
+
@@ -0,0 +1,19 @@
1
+ require 'itamae'
2
+ require 'erb'
3
+ require 'tempfile'
4
+
5
+ module Itamae
6
+ module Resources
7
+ class Template < File
8
+ define_option :source, type: String, required: true
9
+
10
+ def create_action
11
+ src = ::File.expand_path(source, ::File.dirname(@recipe.path))
12
+ content(ERB.new(::File.read(src), nil, '-').result(binding))
13
+
14
+ super
15
+ end
16
+ end
17
+ end
18
+ end
19
+
@@ -0,0 +1,49 @@
1
+ require 'itamae'
2
+
3
+ module Itamae
4
+ class Runner
5
+ class << self
6
+ def run(recipe_files, backend, options)
7
+ backend = backend_from_options(backend, options)
8
+ runner = self.new
9
+ runner.node = node_from_options(options)
10
+
11
+ recipe_files.each do |path|
12
+ recipe = Recipe.new(runner, File.expand_path(path))
13
+ recipe.run
14
+ end
15
+ end
16
+
17
+ private
18
+ def node_from_options(options)
19
+ if options[:node_json]
20
+ path = File.expand_path(options[:node_json])
21
+ Logger.debug "Loading node data from #{path} ..."
22
+ hash = JSON.load(open(path))
23
+ else
24
+ hash = {}
25
+ end
26
+
27
+ Node.new(hash)
28
+ end
29
+
30
+ def backend_from_options(type, options)
31
+ case type
32
+ when :local
33
+ Itamae.create_local_backend
34
+ when :ssh
35
+ ssh_options = {}
36
+ ssh_options[:host] = options[:host]
37
+ ssh_options[:user] = options[:user] || Etc.getlogin
38
+ ssh_options[:keys] = [options[:key]] if options[:key]
39
+ ssh_options[:port] = options[:port] if options[:port]
40
+
41
+ Itamae.create_ssh_backend(ssh_options)
42
+ end
43
+ end
44
+ end
45
+
46
+ attr_accessor :node
47
+ end
48
+ end
49
+
@@ -0,0 +1,44 @@
1
+ require 'specinfra'
2
+
3
+ # TODO: move to specinfra
4
+
5
+ module Itamae
6
+ def self.backend=(backend)
7
+ @backend = backend
8
+ end
9
+
10
+ def self.backend
11
+ @backend
12
+ end
13
+
14
+ def self.create_local_backend
15
+ create_backend(:exec)
16
+ end
17
+
18
+ def self.create_ssh_backend(options)
19
+ Specinfra.configuration.request_pty = true
20
+
21
+ Specinfra.configuration.host = options.delete(:host)
22
+ Specinfra.configuration.ssh_options = options
23
+ create_backend(:ssh)
24
+ end
25
+
26
+ private
27
+ def self.create_backend(type)
28
+ Specinfra.configuration.backend = type
29
+ Itamae.backend = Specinfra.backend
30
+ end
31
+
32
+ module SpecinfraHelpers
33
+ module RunCommand
34
+ def backend
35
+ Itamae.backend
36
+ end
37
+
38
+ def run_command(cmd)
39
+ backend.run_command(cmd)
40
+ end
41
+ end
42
+ end
43
+ end
44
+
@@ -0,0 +1,3 @@
1
+ module Itamae
2
+ VERSION = "1.0.0.beta1"
3
+ end
@@ -0,0 +1,19 @@
1
+ # -*- mode: ruby -*-
2
+ # vi: set ft=ruby :
3
+
4
+ # Vagrantfile API/syntax version. Don't touch unless you know what you're doing!
5
+ VAGRANTFILE_API_VERSION = "2"
6
+
7
+ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
8
+ config.vm.define :trusty do |c|
9
+ c.vm.box = "ubuntu/trusty64"
10
+
11
+ c.vm.provision :shell, inline: <<-EOC
12
+ cat /etc/apt/sources.list | sed -e 's|http://[^ ]*|mirror://mirrors.ubuntu.com/mirrors.txt|g' > /tmp/sources.list
13
+ if !(diff -q /etc/apt/sources.list /tmp/sources.list); then
14
+ mv /tmp/sources.list /etc/apt/sources.list
15
+ apt-get update
16
+ fi
17
+ EOC
18
+ end
19
+ end
@@ -0,0 +1,33 @@
1
+ require 'spec_helper'
2
+
3
+ describe package('dstat') do
4
+ it { should be_installed }
5
+ end
6
+
7
+ describe package('sl') do
8
+ it { should be_installed }
9
+ end
10
+
11
+ describe file('/tmp/remote_file') do
12
+ it { should be_file }
13
+ its(:content) { should match(/Hello Itamae/) }
14
+ end
15
+
16
+ describe file('/tmp/directory') do
17
+ it { should be_directory }
18
+ it { should be_mode 700 }
19
+ it { should be_owned_by "vagrant" }
20
+ it { should be_grouped_into "vagrant" }
21
+ end
22
+
23
+ describe file('/tmp/template') do
24
+ it { should be_file }
25
+ its(:content) { should match(/Hello/) }
26
+ end
27
+
28
+ describe file('/tmp/file') do
29
+ it { should be_file }
30
+ its(:content) { should match(/Hello World/) }
31
+ end
32
+
33
+
@@ -0,0 +1,26 @@
1
+ package 'dstat' do
2
+ action :install
3
+ end
4
+
5
+ package 'sl'
6
+
7
+ remote_file "/tmp/remote_file" do
8
+ source "hello.txt"
9
+ end
10
+
11
+ directory "/tmp/directory" do
12
+ mode "0700"
13
+ owner "vagrant"
14
+ group "vagrant"
15
+ end
16
+
17
+ template "/tmp/template" do
18
+ source "hello.erb"
19
+ end
20
+
21
+ file "/tmp/file" do
22
+ content "Hello World"
23
+ end
24
+
25
+
26
+
@@ -0,0 +1 @@
1
+ <%= node['greeting'] %>
@@ -0,0 +1 @@
1
+ Hello Itamae
@@ -0,0 +1,3 @@
1
+ {
2
+ "greeting": "Hello"
3
+ }
@@ -0,0 +1,45 @@
1
+ require 'serverspec'
2
+ require 'net/ssh'
3
+ require 'tempfile'
4
+
5
+ set :backend, :ssh
6
+
7
+ def vagrant(cmd)
8
+ Bundler.with_clean_env do
9
+ env = {"VAGRANT_CWD" => File.dirname(__FILE__)}
10
+ system env, "/usr/bin/vagrant #{cmd}"
11
+ end
12
+ end
13
+
14
+ if ENV['ASK_SUDO_PASSWORD']
15
+ begin
16
+ require 'highline/import'
17
+ rescue LoadError
18
+ fail "highline is not available. Try installing it."
19
+ end
20
+ set :sudo_password, ask("Enter sudo password: ") { |q| q.echo = false }
21
+ else
22
+ set :sudo_password, ENV['SUDO_PASSWORD']
23
+ end
24
+
25
+ host = ENV['TARGET_HOST']
26
+
27
+ config = Tempfile.new('', Dir.tmpdir)
28
+ vagrant "ssh-config #{host} > #{config.path}"
29
+
30
+ options = Net::SSH::Config.for(host, [config.path])
31
+
32
+ options[:user] ||= Etc.getlogin
33
+
34
+ set :host, options[:host_name] || host
35
+ set :ssh_options, options
36
+
37
+ # Disable sudo
38
+ # set :disable_sudo, true
39
+
40
+ # Set environment variables
41
+ # set :env, :LANG => 'C', :LC_MESSAGES => 'C'
42
+
43
+ # Set PATH
44
+ # set :path, '/sbin:/usr/local/sbin:$PATH'
45
+
@@ -0,0 +1,20 @@
1
+ require 'spec_helper'
2
+
3
+ module Itamae
4
+ describe Logger do
5
+ let(:io) { StringIO.new }
6
+
7
+ before do
8
+ Logger.logger = ::Logger.new(io)
9
+ end
10
+
11
+ [:fatal, :error, :warn, :info, :debug].each do |level|
12
+ describe "##{level}" do
13
+ it "puts #{level} log" do
14
+ Logger.public_send(level, "CONTENT")
15
+ expect(io.string).to include('CONTENT')
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,6 @@
1
+ require 'spec_helper'
2
+
3
+ module Itamae
4
+ describe Node do
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ require 'spec_helper'
2
+
3
+ module Itamae
4
+ describe Recipe do
5
+ end
6
+ end
@@ -0,0 +1,135 @@
1
+ require 'itamae'
2
+
3
+ class DefineOptionTestResource < Itamae::Resources::Base
4
+ define_option :action, default: :create
5
+ define_option :default_option, default: :something
6
+ define_option :required_option, required: true
7
+ define_option :typed_option, type: Numeric
8
+ define_option :default_name_option, default_name: true
9
+ end
10
+
11
+ describe DefineOptionTestResource do
12
+ describe "define_option" do
13
+ describe "default" do
14
+ subject do
15
+ described_class.new(double(:recipe), 'resource name') do
16
+ required_option :required_value
17
+ end
18
+ end
19
+ it "returns the default value" do
20
+ expect(subject.options[:default_option]).to eq(:something)
21
+ end
22
+ end
23
+
24
+ describe "required" do
25
+ subject do
26
+ described_class.new(double(:recipe), 'resource name') do
27
+ #required_option :required_value
28
+ end
29
+ end
30
+ context "without setting required option" do
31
+ it "raises an error" do
32
+ expect do
33
+ subject
34
+ end.to raise_error(Itamae::Resources::OptionMissingError)
35
+ end
36
+ end
37
+ end
38
+
39
+ describe "type" do
40
+ context "with correct type value" do
41
+ subject do
42
+ described_class.new(double(:recipe), 'resource name') do
43
+ required_option :required_value
44
+ typed_option 10
45
+ end
46
+ end
47
+ it "returns the value" do
48
+ expect(subject.options[:typed_option]).to eq(10)
49
+ end
50
+ end
51
+
52
+ context "with incorrect type value" do
53
+ subject do
54
+ described_class.new(double(:recipe), 'resource name') do
55
+ required_option :required_value
56
+ typed_option "string"
57
+ end
58
+ end
59
+ it "raises an error" do
60
+ expect do
61
+ subject
62
+ end.to raise_error(Itamae::Resources::InvalidTypeError)
63
+ end
64
+ end
65
+ end
66
+
67
+ describe "default_name" do
68
+ context "without setting the value" do
69
+ subject do
70
+ described_class.new(double(:recipe), 'resource name') do
71
+ required_option :required_value
72
+ end
73
+ end
74
+ it "returns the resource name" do
75
+ expect(subject.options[:default_name_option]).
76
+ to eq("resource name")
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+
83
+ class TestResource < Itamae::Resources::Base
84
+ define_option :action, default: :create
85
+ define_option :option_key, required: false
86
+ end
87
+
88
+ describe TestResource do
89
+ let(:commands) { double(:commands) }
90
+ let(:runner) do
91
+ double(:runner)
92
+ end
93
+ let(:recipe) do
94
+ double(:recipe).tap do |r|
95
+ r.stub(:runner).and_return(runner)
96
+ end
97
+ end
98
+
99
+ subject(:resource) { described_class.new(recipe, "name") }
100
+
101
+ before do
102
+ Itamae.backend = double(:backend).tap do |b|
103
+ b.stub(:commands).and_return(commands)
104
+ end
105
+ end
106
+
107
+ describe "#run" do
108
+ before do
109
+ subject.action :action_name
110
+ end
111
+ it "executes <ACTION_NAME>_action method" do
112
+ expect(subject).to receive(:action_name_action)
113
+ subject.run
114
+ end
115
+ end
116
+
117
+ describe "#run_specinfra" do
118
+ it "runs specinfra's command by specinfra's backend" do
119
+ expect(Specinfra.command).to receive(:cmd).and_return("command")
120
+ expect(Itamae.backend).to receive(:run_command).with("command").
121
+ and_return(Specinfra::CommandResult.new(exit_status: 0))
122
+ subject.send(:run_specinfra, :cmd)
123
+ end
124
+ context "when the command execution failed" do
125
+ it "raises CommandExecutionError" do
126
+ expect(Specinfra.command).to receive(:cmd).and_return("command")
127
+ expect(Itamae.backend).to receive(:run_command).with("command").
128
+ and_return(Specinfra::CommandResult.new(exit_status: 1))
129
+ expect do
130
+ subject.send(:run_specinfra, :cmd)
131
+ end.to raise_error(Itamae::Resources::CommandExecutionError)
132
+ end
133
+ end
134
+ end
135
+ end