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.
- checksums.yaml +7 -0
- data/.gitignore +19 -0
- data/.rspec +2 -0
- data/.travis.yml +6 -0
- data/Gemfile +14 -0
- data/LICENSE.txt +22 -0
- data/README.md +60 -0
- data/Rakefile +62 -0
- data/bin/itamae +5 -0
- data/example/foo +1 -0
- data/example/node.json +1 -0
- data/example/recipe.rb +15 -0
- data/itamae.gemspec +32 -0
- data/lib/itamae.rb +13 -0
- data/lib/itamae/cli.rb +23 -0
- data/lib/itamae/logger.rb +44 -0
- data/lib/itamae/node.rb +9 -0
- data/lib/itamae/recipe.rb +46 -0
- data/lib/itamae/resources.rb +25 -0
- data/lib/itamae/resources/base.rb +151 -0
- data/lib/itamae/resources/directory.rb +26 -0
- data/lib/itamae/resources/file.rb +36 -0
- data/lib/itamae/resources/package.rb +15 -0
- data/lib/itamae/resources/remote_file.rb +16 -0
- data/lib/itamae/resources/template.rb +19 -0
- data/lib/itamae/runner.rb +49 -0
- data/lib/itamae/specinfra.rb +44 -0
- data/lib/itamae/version.rb +3 -0
- data/spec/integration/Vagrantfile +19 -0
- data/spec/integration/default_spec.rb +33 -0
- data/spec/integration/recipes/default.rb +26 -0
- data/spec/integration/recipes/hello.erb +1 -0
- data/spec/integration/recipes/hello.txt +1 -0
- data/spec/integration/recipes/node.json +3 -0
- data/spec/integration/spec_helper.rb +45 -0
- data/spec/unit/lib/itamae/logger_spec.rb +20 -0
- data/spec/unit/lib/itamae/node_spec.rb +6 -0
- data/spec/unit/lib/itamae/recipe_spec.rb +6 -0
- data/spec/unit/lib/itamae/resources/base_spec.rb +135 -0
- data/spec/unit/lib/itamae/resources/package_spec.rb +18 -0
- data/spec/unit/lib/itamae/resources/remote_file_spec.rb +23 -0
- data/spec/unit/lib/itamae/resources_spec.rb +14 -0
- data/spec/unit/lib/itamae/runner_spec.rb +29 -0
- data/spec/unit/spec_helper.rb +22 -0
- 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,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,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,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
|