New upstream version 1.10.7
Antonio Terceiro
4 years ago
0 | language: ruby | |
1 | sudo: required | |
2 | dist: xenial | |
3 | services: | |
4 | - docker | |
5 | rvm: | |
6 | - 2.3 | |
7 | - 2.4 | |
8 | - 2.5 | |
9 | - 2.6 | |
10 | - 2.7 | |
11 | - ruby-head | |
12 | bundler_args: "--jobs=4 --retry=3" | |
13 | cache: | |
14 | bundler: true | |
15 | ||
16 | before_install: | |
17 | - travis_retry gem install bundler --no-document || travis_retry gem install bundler --no-document -v 1.17.3 | |
18 | ||
19 | script: | |
20 | - RUBYOPT=$SPEC_RUBYOPT bundle exec rake spec | |
21 | notifications: | |
22 | email: false | |
23 | slack: | |
24 | secure: PcecHsVS6lw89K5PllW8xFlzu0d04p6lYfxlUZL0/yp9flAczElJME4RshSMSkbnu5e2Iw8KUA2xB1jkAzDo9qYoXveaKyjkFUOb1ZxYIVxzzfoDDwUNTMmSoyjZjvbeBUcpxxmxy6nXa3zS+gA2ohqWhS9WTTlTqyM5RriDjZ8= | |
25 | matrix: | |
26 | allow_failures: | |
27 | - rvm: ruby-head | |
28 | include: | |
29 | - rvm: 2.6 | |
30 | env: SPEC_RUBYOPT="--jit" | |
31 | - rvm: ruby-head | |
32 | env: SPEC_RUBYOPT="--jit" | |
33 | branches: | |
34 | only: | |
35 | - master |
0 | ## Unreleased | |
1 | [full changelog](https://github.com/itamae-kitchen/itamae/compare/v1.10.7...master) | |
2 | ||
3 | ## v1.10.7 | |
4 | [full changelog](https://github.com/itamae-kitchen/itamae/compare/v1.10.6...v1.10.7) | |
5 | ||
6 | Improvements | |
7 | ||
8 | - [Improve `file` resource performance](https://github.com/itamae-kitchen/itamae/pull/310) | |
9 | ||
10 | ## v1.10.6 | |
11 | [full changelog](https://github.com/itamae-kitchen/itamae/compare/v1.10.5...v1.10.6) | |
12 | ||
13 | Improvements | |
14 | ||
15 | - [Don't use `sudo` when `--no-sudo` is passed](https://github.com/itamae-kitchen/itamae/pull/302) | |
16 | ||
17 | ## v1.10.5 | |
18 | [full changelog](https://github.com/itamae-kitchen/itamae/compare/v1.10.4...v1.10.5) | |
19 | ||
20 | Improvements | |
21 | ||
22 | - [Check http status code in `http_request` resource (by @takumin)](https://github.com/itamae-kitchen/itamae/pull/296) | |
23 | ||
24 | ## v1.10.4 | |
25 | [full changelog](https://github.com/itamae-kitchen/itamae/compare/v1.10.3...v1.10.4) | |
26 | ||
27 | Bugfixes | |
28 | ||
29 | - [Suppress Ruby warnings (by @pocke)](https://github.com/itamae-kitchen/itamae/pull/284) | |
30 | - [Suppress Ruby warning (by @pocke)](https://github.com/itamae-kitchen/itamae/pull/287) | |
31 | - [Run test cases correctly (by @pocke)](https://github.com/itamae-kitchen/itamae/pull/289) | |
32 | ||
33 | Improvements | |
34 | ||
35 | - [Add description to --tag option of docker subcommand (by @pocke)](https://github.com/itamae-kitchen/itamae/pull/286) | |
36 | - [Refine `itamae docker`'s created message (by @pocke)](https://github.com/itamae-kitchen/itamae/pull/288) | |
37 | ||
38 | ## v1.10.3 | |
39 | [full changelog](https://github.com/itamae-kitchen/itamae/compare/v1.10.2...v1.10.3) | |
40 | ||
41 | Bugfixes | |
42 | ||
43 | - [Make `send_file` aware of `user` option](https://github.com/itamae-kitchen/itamae/pull/277) | |
44 | ||
45 | ## v1.10.2 | |
46 | [full changelog](https://github.com/itamae-kitchen/itamae/compare/v1.10.1...v1.10.2) | |
47 | ||
48 | Bugfixes | |
49 | ||
50 | - [Disable mash warnings (review catch up)](https://github.com/itamae-kitchen/itamae/pull/273) | |
51 | ||
52 | ## v1.10.1 | |
53 | [full changelog](https://github.com/itamae-kitchen/itamae/compare/v1.10.0...v1.10.1) | |
54 | ||
55 | Bugfixes | |
56 | ||
57 | - [fail `--ohai` option when using ohai v13.0.1 or higher](https://github.com/itamae-kitchen/itamae/pull/251) | |
58 | ||
59 | ## v1.10.0 | |
60 | [full changelog](https://github.com/itamae-kitchen/itamae/compare/v1.9.13...v1.10.0) | |
61 | ||
62 | Features | |
63 | ||
64 | - [Support `only_if` and `not_if` inside a `define`](https://github.com/itamae-kitchen/itamae/pull/271) | |
65 | ||
66 | ## v1.9.13 | |
67 | [full changelog](https://github.com/itamae-kitchen/itamae/compare/v1.9.12...v1.9.13) | |
68 | ||
69 | Bugfixes | |
70 | ||
71 | - [Fixed. Can not create empty file](https://github.com/itamae-kitchen/itamae/pull/269) | |
72 | ||
73 | ## v1.9.12 | |
74 | [full changelog](https://github.com/itamae-kitchen/itamae/compare/v1.9.11...v1.9.12) | |
75 | ||
76 | Features | |
77 | ||
78 | - [jail backend: add support of FreeBSD Jail (`itamae jail`)](https://github.com/itamae-kitchen/itamae/pull/249) | |
79 | ||
80 | Bugfixes | |
81 | ||
82 | - [docker backend: Fixed edit action of file resource doesn't work with docker backend](https://github.com/itamae-kitchen/itamae/pull/257) | |
83 | ||
84 | Improvements | |
85 | ||
86 | - [Print '(dry-run)' first in dry-run mode](https://github.com/itamae-kitchen/itamae/pull/252) | |
87 | ||
88 | ## v1.9.11 | |
89 | ||
90 | Features | |
91 | ||
92 | - [docker backend: Support image tagging](https://github.com/itamae-kitchen/itamae/pull/230) | |
93 | - [docker backend: Support docker_container_create_options](https://github.com/itamae-kitchen/itamae/pull/231) | |
94 | ||
95 | ||
96 | Bugfixes | |
97 | ||
98 | - [Fix help subcommand](https://github.com/itamae-kitchen/itamae/pull/235) | |
99 | ||
0 | 100 | ## v1.9.10 |
1 | 101 | |
2 | 102 | Features |
438 | 538 | |
439 | 539 | Improvements |
440 | 540 | |
441 | - `source :auto` accepts a template without .erb extention. (by @ryotarai) | |
541 | - `source :auto` accepts a template without .erb extension. (by @ryotarai) | |
442 | 542 | |
443 | 543 | ## v1.1.21 |
444 | 544 |
1 | 1 | |
2 | 2 | # Specify your gem's dependencies in itamae.gemspec |
3 | 3 | gemspec |
4 | ||
5 | gem 'vagrant', github: 'ryotarai/vagrant', branch: 'latest-bundler' | |
6 | gem 'vagrant-digitalocean' | |
7 | 4 | |
8 | 5 | path = Pathname.new("Gemfile.local") |
9 | 6 | eval(path.read) if path.exist? |
0 | 0 | # [![](https://raw.githubusercontent.com/itamae-kitchen/itamae-logos/master/small/FA-Itamae-horizontal-01-180x72.png)](https://github.com/itamae-kitchen/itamae) |
1 | 1 | |
2 | [![Gem Version](https://badge.fury.io/rb/itamae.svg)](http://badge.fury.io/rb/itamae) [![Code Climate](https://codeclimate.com/github/ryotarai/itamae/badges/gpa.svg)](https://codeclimate.com/github/ryotarai/itamae) [![wercker status](https://app.wercker.com/status/3e7be3b982d3671940a07e3ef45d9f5f/s/master "wercker status")](https://app.wercker.com/project/bykey/3e7be3b982d3671940a07e3ef45d9f5f) [![Slack](https://img.shields.io/badge/slack-join-blue.svg)](https://itamae-slackin.herokuapp.com/) | |
2 | [![Gem Version](https://badge.fury.io/rb/itamae.svg)](http://badge.fury.io/rb/itamae) [![Code Climate](https://codeclimate.com/github/ryotarai/itamae/badges/gpa.svg)](https://codeclimate.com/github/ryotarai/itamae) [![Build Status](https://travis-ci.org/itamae-kitchen/itamae.svg?branch=master)](https://travis-ci.org/itamae-kitchen/itamae) [![Slack](https://img.shields.io/badge/slack-join-blue.svg)](https://join.slack.com/t/itamae/shared_invite/enQtNTExNTI3ODM1NTY5LTM5MWJlZTgwODE0YTUwMThiNzZjN2I1MGNlZjE2NjlmNzg5NTNlOTliMDhkNDNmNTQ2ZTgwMzZjNjI5NDJiZGI) | |
3 | 3 | |
4 | 4 | Simple and lightweight configuration management tool inspired by Chef. |
5 | 5 | |
76 | 76 | |
77 | 77 | ## Get Involved |
78 | 78 | |
79 | - [Join Slack team](https://itamae-slackin.herokuapp.com/) | |
79 | - [Join Slack team](https://itamae-slackin.herokuapp.com) | |
80 | 80 | |
81 | 81 | ## Presentations / Articles |
82 | 82 |
2 | 2 | require 'tempfile' |
3 | 3 | require 'net/ssh' |
4 | 4 | |
5 | vagrant_bin = 'vagrant' | |
5 | Dir['tasks/*.rb'].each do |file| | |
6 | require_relative file | |
7 | end | |
6 | 8 | |
7 | 9 | desc 'Run unit and integration specs.' |
8 | 10 | task :spec => ['spec:unit', 'spec:integration:all'] |
14 | 16 | end |
15 | 17 | |
16 | 18 | namespace :integration do |
17 | targets = [] | |
18 | status = `cd spec/integration && #{vagrant_bin} status` | |
19 | unless $?.exitstatus == 0 | |
20 | raise "vagrant status failed.\n#{status}" | |
21 | end | |
19 | targets = ["ubuntu:trusty"] | |
20 | container_name = 'itamae' | |
22 | 21 | |
23 | status.split("\n\n")[1].each_line do |line| | |
24 | targets << line.match(/^[^ ]+/)[0] | |
25 | end | |
26 | ||
27 | task :all => targets | |
22 | task :all => targets + ['spec:integration:local'] | |
28 | 23 | |
29 | 24 | targets.each do |target| |
30 | 25 | desc "Run provision and specs to #{target}" |
31 | task target => ["provision:#{target}", "serverspec:#{target}"] | |
26 | task target => ["docker:#{target}", "provision:#{target}", "serverspec:#{target}", 'clean_docker_container'] | |
27 | ||
28 | namespace :docker do | |
29 | desc "Run docker for #{target}" | |
30 | task target do | |
31 | sh "docker run --privileged -d --name #{container_name} #{target} /sbin/init" | |
32 | end | |
33 | end | |
32 | 34 | |
33 | 35 | namespace :provision do |
36 | desc "Run itamae to #{target}" | |
34 | 37 | task target do |
35 | config = Tempfile.new('', Dir.tmpdir) | |
36 | env = {"VAGRANT_CWD" => File.expand_path('./spec/integration')} | |
37 | system env, "#{vagrant_bin} up #{target}" | |
38 | system env, "#{vagrant_bin} ssh-config #{target} > #{config.path}" | |
39 | options = Net::SSH::Config.for(target, [config.path]) | |
40 | ||
41 | 38 | suites = [ |
42 | 39 | [ |
43 | 40 | "spec/integration/recipes/default.rb", |
44 | 41 | "spec/integration/recipes/default2.rb", |
45 | 42 | "spec/integration/recipes/redefine.rb", |
43 | "spec/integration/recipes/docker.rb", | |
46 | 44 | ], |
47 | 45 | [ |
48 | 46 | "--dry-run", |
50 | 48 | ], |
51 | 49 | ] |
52 | 50 | suites.each do |suite| |
53 | cmd = %w!bundle exec bin/itamae ssh! | |
54 | cmd << "-h" << options[:host_name] | |
55 | cmd << "-u" << options[:user] | |
56 | cmd << "-p" << options[:port].to_s | |
57 | cmd << "-i" << options[:keys].first | |
51 | cmd = %w!bundle exec ruby -w bin/itamae docker! | |
58 | 52 | cmd << "-l" << (ENV['LOG_LEVEL'] || 'debug') |
59 | 53 | cmd << "-j" << "spec/integration/recipes/node.json" |
54 | cmd << "--container" << container_name | |
55 | cmd << "--tag" << "itamae:latest" | |
60 | 56 | cmd += suite |
61 | 57 | |
62 | 58 | p cmd |
70 | 66 | namespace :serverspec do |
71 | 67 | desc "Run serverspec tests to #{target}" |
72 | 68 | RSpec::Core::RakeTask.new(target.to_sym) do |t| |
73 | ENV['TARGET_HOST'] = target | |
69 | ENV['DOCKER_CONTAINER'] = container_name | |
74 | 70 | t.ruby_opts = '-I ./spec/integration' |
75 | t.pattern = "spec/integration/*_spec.rb" | |
71 | t.pattern = "spec/integration/{default,docker}_spec.rb" | |
76 | 72 | end |
73 | end | |
74 | ||
75 | desc 'Clean a docker container for test' | |
76 | task :clean_docker_container do | |
77 | sh('docker', 'rm', '-f', container_name) | |
77 | 78 | end |
78 | 79 | end |
79 | 80 | end |
80 | 81 | end |
81 | 82 | |
83 | task :default => :spec |
0 | require 'net/https' | |
1 | require 'json' | |
2 | require 'time' | |
3 | ||
4 | http = Net::HTTP.new("api.digitalocean.com", 443) | |
5 | http.use_ssl = true | |
6 | ||
7 | res = http.start do | |
8 | http.get("/v2/droplets", "Authorization" => "Bearer #{ENV['DIGITALOCEAN_TOKEN']}") | |
9 | end | |
10 | ||
11 | droplets = JSON.parse(res.body)['droplets'] | |
12 | droplets.each do |droplet| | |
13 | next unless /^itamae-/ =~ droplet['name'] | |
14 | if Time.now - Time.parse(droplet['created_at']) >= 60 * 60 | |
15 | puts "destroying #{droplet}..." | |
16 | res = http.start do | |
17 | http.delete("/v2/droplets/#{droplet['id']}", "Authorization" => "Bearer #{ENV['DIGITALOCEAN_TOKEN']}") | |
18 | end | |
19 | end | |
20 | end | |
21 |
5 | 5 | Gem::Specification.new do |spec| |
6 | 6 | spec.name = "itamae" |
7 | 7 | spec.version = Itamae::VERSION |
8 | spec.authors = ["Ryota Arai"] | |
9 | spec.email = ["ryota.arai@gmail.com"] | |
8 | spec.authors = ["Ryota Arai", "Yusuke Nakamura", "sue445"] | |
9 | spec.email = ["ryota.arai@gmail.com", "yusuke1994525@gmail.com", "sue445@sue445.net"] | |
10 | 10 | spec.summary = %q{Simple Configuration Management Tool} |
11 | spec.homepage = "https://github.com/itamae-kitchen/itamae" | |
11 | spec.homepage = "https://itamae.kitchen/" | |
12 | 12 | spec.license = "MIT" |
13 | ||
14 | if spec.respond_to?(:metadata) | |
15 | spec.metadata["homepage_uri"] = spec.homepage | |
16 | spec.metadata["source_code_uri"] = "https://github.com/itamae-kitchen/itamae" | |
17 | spec.metadata["changelog_uri"] = "https://github.com/itamae-kitchen/itamae/blob/master/CHANGELOG.md" | |
18 | else | |
19 | raise "RubyGems 2.0 or newer is required to protect against " \ | |
20 | "public gem pushes." | |
21 | end | |
13 | 22 | |
14 | 23 | spec.files = `git ls-files`.split($/) |
15 | 24 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } |
22 | 31 | spec.add_runtime_dependency "ansi" |
23 | 32 | spec.add_runtime_dependency "schash", "~> 0.1.0" |
24 | 33 | |
25 | spec.add_development_dependency "bundler", "~> 1.3" | |
34 | spec.add_development_dependency "bundler", ">= 1.3" | |
26 | 35 | spec.add_development_dependency "rake" |
27 | 36 | spec.add_development_dependency "rspec", "~> 3.0" |
28 | 37 | spec.add_development_dependency "serverspec", "~> 2.1" |
8 | 8 | module Configuration |
9 | 9 | def self.sudo_password |
10 | 10 | return ENV['SUDO_PASSWORD'] if ENV['SUDO_PASSWORD'] |
11 | return @sudo_password if @sudo_password | |
11 | return @sudo_password if defined?(@sudo_password) | |
12 | 12 | |
13 | 13 | # TODO: Fix this dirty hack |
14 | 14 | return nil unless caller.any? {|call| call.include?('channel_data') } |
97 | 97 | @backend.receive_file(src, dst) |
98 | 98 | end |
99 | 99 | |
100 | def send_file(src, dst) | |
100 | def send_file(src, dst, user: nil) | |
101 | 101 | Itamae.logger.debug "Sending a file from '#{src}' to '#{dst}'..." |
102 | 102 | unless ::File.exist?(src) |
103 | 103 | raise SourceNotExistError, "The file '#{src}' doesn't exist." |
105 | 105 | unless ::File.file?(src) |
106 | 106 | raise SourceNotExistError, "'#{src}' is not a file." |
107 | 107 | end |
108 | @backend.send_file(src, dst) | |
108 | ||
109 | if self.instance_of?(Backend::Local) | |
110 | read_command = build_command("cat #{src.shellescape}", {}) | |
111 | write_command = build_command("cat > #{dst.shellescape}", user: user) | |
112 | command = [read_command, write_command].join(' | ') | |
113 | run_command(command) | |
114 | else | |
115 | @backend.send_file(src, dst) | |
116 | end | |
109 | 117 | end |
110 | 118 | |
111 | 119 | def send_directory(src, dst) |
179 | 187 | end |
180 | 188 | |
181 | 189 | user = options[:user] |
182 | if user | |
190 | if user && use_sudo? | |
183 | 191 | command = "cd ~#{user.shellescape} ; #{command}" |
184 | 192 | command = "sudo -H -u #{user.shellescape} -- #{shell.shellescape} -c #{command.shellescape}" |
185 | 193 | end |
186 | 194 | |
187 | 195 | command |
196 | end | |
197 | ||
198 | def use_sudo? | |
199 | return true unless @options.key?(:sudo) | |
200 | !!@options[:sudo] | |
188 | 201 | end |
189 | 202 | |
190 | 203 | def shell |
207 | 220 | def create_specinfra_backend |
208 | 221 | Specinfra::Backend::Exec.new( |
209 | 222 | shell: @options[:shell], |
223 | ) | |
224 | end | |
225 | end | |
226 | ||
227 | class Jexec < Base | |
228 | private | |
229 | def create_specinfra_backend | |
230 | Specinfra::Backend::Jexec.new( | |
231 | shell: @options[:shell], | |
232 | login_shell: @options[:login_shell], | |
233 | jail_name: @options[:jail_name], | |
210 | 234 | ) |
211 | 235 | end |
212 | 236 | end |
269 | 293 | class Docker < Base |
270 | 294 | def finalize |
271 | 295 | image = @backend.commit_container |
272 | Itamae.logger.info "Image created: #{image.id}" | |
296 | /\A(?<repo>.+?)(?:|:(?<tag>[^:]+))\z/.match(@options[:tag]) do |m| | |
297 | image.tag(repo: m[:repo], tag: m[:tag]) | |
298 | end | |
299 | log_message = "Image created: #{image.id}" | |
300 | log_message << ", and tagged as #{@options[:tag]}" if @options[:tag] | |
301 | Itamae.logger.info log_message | |
273 | 302 | end |
274 | 303 | |
275 | 304 | private |
288 | 317 | docker_image: @options[:image], |
289 | 318 | docker_container: @options[:container], |
290 | 319 | shell: @options[:shell], |
320 | docker_container_create_options: @options[:docker_container_create_options], | |
291 | 321 | ) |
292 | 322 | end |
293 | 323 | end |
4 | 4 | class CLI < Thor |
5 | 5 | GENERATE_TARGETS = %w[cookbook role].freeze |
6 | 6 | |
7 | class_option :log_level, type: :string, aliases: ['-l'], default: 'info' | |
8 | class_option :color, type: :boolean, default: true | |
9 | class_option :config, type: :string, aliases: ['-c'] | |
10 | ||
11 | 7 | def initialize(*) |
12 | 8 | super |
13 | 9 | |
14 | Itamae.logger.level = ::Logger.const_get(options[:log_level].upcase) | |
15 | Itamae.logger.formatter.colored = options[:color] | |
10 | Itamae.logger.level = ::Logger.const_get(options[:log_level].upcase) if options[:log_level] | |
11 | Itamae.logger.formatter.colored = options[:color] if options[:color] | |
16 | 12 | end |
17 | 13 | |
18 | 14 | def self.define_exec_options |
25 | 21 | option :ohai, type: :boolean, default: false, desc: "This option is DEPRECATED and will be unavailable." |
26 | 22 | option :profile, type: :string, desc: "[EXPERIMENTAL] Save profiling data", banner: "PATH" |
27 | 23 | option :detailed_exitcode, type: :boolean, default: false, desc: "exit code 0 - The run succeeded with no changes or failures, exit code 1 - The run failed, exit code 2 - The run succeeded, and some resources were changed" |
24 | option :log_level, type: :string, aliases: ['-l'], default: 'info' | |
25 | option :color, type: :boolean, default: true | |
26 | option :config, type: :string, aliases: ['-c'] | |
28 | 27 | end |
29 | 28 | |
30 | 29 | desc "local RECIPE [RECIPE...]", "Run Itamae locally" |
64 | 63 | option :image, type: :string, desc: "This option or 'container' option is required." |
65 | 64 | option :container, type: :string, desc: "This option or 'image' option is required." |
66 | 65 | option :tls_verify_peer, type: :boolean, default: true |
66 | option :tag, type: :string, desc: 'Tag name of created docker image.' | |
67 | 67 | def docker(*recipe_files) |
68 | 68 | if recipe_files.empty? |
69 | 69 | raise "Please specify recipe files." |
70 | 70 | end |
71 | 71 | |
72 | 72 | run(recipe_files, :docker, options) |
73 | end | |
74 | ||
75 | desc "jail RECIPE [RECIPE...]", "Run Itamae in jail" | |
76 | define_exec_options | |
77 | option :jail_name, type: :string, desc: "Jail Hostname" | |
78 | def jail(*recipe_files) | |
79 | if recipe_files.empty? | |
80 | raise "Please specify recipe files." | |
81 | end | |
82 | ||
83 | run(recipe_files, :jexec, options) | |
73 | 84 | end |
74 | 85 | |
75 | 86 | desc "version", "Print version" |
13 | 13 | FileUtils.cp(from, to) |
14 | 14 | else |
15 | 15 | ::File.read(from) |
16 | end | |
17 | end | |
18 | end | |
19 | ||
20 | class Docker < Exec | |
21 | def receive_file(from, to = nil) | |
22 | if to | |
23 | send_file(from, to) | |
24 | else | |
25 | run_command("cat #{from}").stdout | |
16 | 26 | end |
17 | 27 | end |
18 | 28 | end |
0 | require 'itamae' | |
1 | 0 | require 'logger' |
2 | 1 | require 'ansi/code' |
3 | 2 | |
71 | 70 | class Formatter |
72 | 71 | attr_accessor :colored |
73 | 72 | |
73 | def initialize | |
74 | @color = nil | |
75 | end | |
76 | ||
74 | 77 | def call(severity, datetime, progname, msg) |
75 | 78 | log = "%s : %s" % ["%5s" % severity, msg2str(msg)] |
76 | 79 |
0 | require 'itamae' | |
1 | 0 | require 'hashie' |
2 | 1 | require 'json' |
3 | 2 | require 'schash' |
9 | 8 | attr_reader :mash |
10 | 9 | |
11 | 10 | def initialize(hash, backend) |
12 | @mash = Hashie::Mash.new(hash) | |
11 | @mash = Itamae::Mash.new(hash) | |
13 | 12 | @backend = backend |
14 | 13 | end |
15 | 14 | |
42 | 41 | private |
43 | 42 | |
44 | 43 | def _reverse_merge(other_hash) |
45 | Hashie::Mash.new(other_hash).merge(@mash) | |
44 | Itamae::Mash.new(other_hash).merge(@mash) | |
46 | 45 | end |
47 | 46 | |
48 | 47 | def method_missing(method, *args) |
62 | 61 | def fetch_inventory_value(key) |
63 | 62 | value = @backend.host_inventory[key] |
64 | 63 | if value.is_a?(Hash) |
65 | value = Hashie::Mash.new(value) | |
64 | value = Itamae::Mash.new(value) | |
66 | 65 | end |
67 | 66 | |
68 | 67 | value |
0 | require 'itamae' | |
1 | ||
2 | 0 | module Itamae |
3 | 1 | class Notification < Struct.new(:defined_in_resource, :action, :target_resource_desc, :timing) |
4 | 2 | def self.create(*args) |
0 | require 'itamae' | |
1 | ||
2 | 0 | module Itamae |
3 | 1 | class Recipe |
4 | 2 | NotFoundError = Class.new(StandardError) |
160 | 158 | context.instance_eval(&@definition.class.definition_block) |
161 | 159 | end |
162 | 160 | |
161 | def run | |
162 | if @definition.do_not_run_because_of_only_if? | |
163 | Itamae.logger.debug "#{@definition.resource_type}[#{@definition.resource_name}] Execution skipped because of only_if attribute" | |
164 | return | |
165 | elsif @definition.do_not_run_because_of_not_if? | |
166 | Itamae.logger.debug "#{@definition.resource_type}[#{@definition.resource_name}] Execution skipped because of not_if attribute" | |
167 | return | |
168 | end | |
169 | ||
170 | super | |
171 | end | |
172 | ||
163 | 173 | private |
164 | 174 | |
165 | 175 | def show_banner |
0 | require 'itamae' | |
1 | 0 | require 'shellwords' |
2 | 1 | require 'hashie' |
3 | 2 | |
15 | 14 | def initialize(resource) |
16 | 15 | @resource = resource |
17 | 16 | |
18 | @attributes = Hashie::Mash.new | |
17 | @attributes = Itamae::Mash.new | |
19 | 18 | @notifications = [] |
20 | 19 | @subscriptions = [] |
21 | 20 | @verify_commands = [] |
157 | 156 | |
158 | 157 | def resource_type |
159 | 158 | self.class.name.split("::").last.scan(/[A-Z][^A-Z]+/).map(&:downcase).join('_') |
159 | end | |
160 | ||
161 | def do_not_run_because_of_only_if? | |
162 | @only_if_command && | |
163 | run_command(@only_if_command, error: false).exit_status != 0 | |
164 | end | |
165 | ||
166 | def do_not_run_because_of_not_if? | |
167 | @not_if_command && | |
168 | run_command(@not_if_command, error: false).exit_status == 0 | |
160 | 169 | end |
161 | 170 | |
162 | 171 | private |
211 | 220 | end |
212 | 221 | |
213 | 222 | def clear_current_attributes |
214 | @current_attributes = Hashie::Mash.new | |
223 | @current_attributes = Itamae::Mash.new | |
215 | 224 | end |
216 | 225 | |
217 | 226 | def pre_action |
269 | 278 | end |
270 | 279 | end |
271 | 280 | |
272 | def do_not_run_because_of_only_if? | |
273 | @only_if_command && | |
274 | run_command(@only_if_command, error: false).exit_status != 0 | |
275 | end | |
276 | ||
277 | def do_not_run_because_of_not_if? | |
278 | @not_if_command && | |
279 | run_command(@not_if_command, error: false).exit_status == 0 | |
280 | end | |
281 | ||
282 | 281 | def backend |
283 | 282 | runner.backend |
284 | 283 | end |
0 | require 'itamae' | |
1 | ||
2 | 0 | module Itamae |
3 | 1 | module Resource |
4 | 2 | class File < Base |
10 | 8 | define_attribute :group, type: String |
11 | 9 | define_attribute :block, type: Proc, default: proc {} |
12 | 10 | |
11 | class << self | |
12 | attr_accessor :sha256sum_available | |
13 | end | |
14 | ||
13 | 15 | def pre_action |
14 | 16 | current.exist = run_specinfra(:check_file_is_file, attributes.path) |
15 | 17 | |
28 | 30 | end |
29 | 31 | end |
30 | 32 | |
33 | if exists_and_not_modified? | |
34 | attributes.modified = false | |
35 | return | |
36 | end | |
37 | ||
31 | 38 | send_tempfile |
32 | 39 | compare_file |
33 | 40 | end |
114 | 121 | def compare_file |
115 | 122 | attributes.modified = false |
116 | 123 | unless @temppath |
124 | return | |
125 | end | |
126 | ||
127 | # When the path currently doesn't exist yet, :change_file_xxx should be performed against `@temppath`. | |
128 | # Checking that by `diff -q /dev/null xxx` doesn't work when xxx's content is "", because /dev/null's content is also "". | |
129 | if !current.exist && attributes.exist | |
130 | attributes.modified = true | |
117 | 131 | return |
118 | 132 | end |
119 | 133 | |
125 | 139 | # error |
126 | 140 | raise Itamae::Backend::CommandExecutionError, "diff command exited with 2" |
127 | 141 | end |
142 | end | |
143 | ||
144 | def exists_and_not_modified? | |
145 | return false unless current.exist && sha256sum_available? | |
146 | ||
147 | current_digest = run_command(["sha256sum", attributes.path]).stdout.split(/\s/, 2).first | |
148 | digest = if content_file | |
149 | Digest::SHA256.file(content_file).hexdigest | |
150 | else | |
151 | Digest::SHA256.hexdigest(attributes.content.to_s) | |
152 | end | |
153 | ||
154 | current_digest == digest | |
128 | 155 | end |
129 | 156 | |
130 | 157 | def show_content_diff |
175 | 202 | |
176 | 203 | if backend.is_a?(Itamae::Backend::Docker) |
177 | 204 | run_command(["mkdir", @temppath]) |
178 | backend.send_file(src, @temppath) | |
205 | backend.send_file(src, @temppath, user: attributes.user) | |
179 | 206 | @temppath = ::File.join(@temppath, ::File.basename(src)) |
180 | 207 | else |
181 | 208 | run_command(["touch", @temppath]) |
182 | 209 | run_specinfra(:change_file_mode, @temppath, '0600') |
183 | backend.send_file(src, @temppath) | |
210 | backend.send_file(src, @temppath, user: attributes.user) | |
184 | 211 | end |
185 | 212 | |
186 | 213 | run_specinfra(:change_file_mode, @temppath, '0600') |
188 | 215 | f.unlink if f |
189 | 216 | end |
190 | 217 | end |
218 | ||
219 | def sha256sum_available? | |
220 | return self.class.sha256sum_available unless self.class.sha256sum_available.nil? | |
221 | ||
222 | self.class.sha256sum_available = run_command(["sha256sum", "--version"], error: false).exit_status == 0 | |
223 | end | |
191 | 224 | end |
192 | 225 | end |
193 | 226 | end |
0 | require 'itamae' | |
1 | 0 | require 'uri' |
2 | 1 | require 'net/https' |
3 | 2 | |
5 | 4 | module Resource |
6 | 5 | class HttpRequest < File |
7 | 6 | RedirectLimitExceeded = Class.new(StandardError) |
7 | HTTPClientError = Class.new(StandardError) | |
8 | HTTPServerError = Class.new(StandardError) | |
9 | HTTPUnknownError = Class.new(StandardError) | |
8 | 10 | |
9 | 11 | alias_method :_action_create, :action_create |
10 | 12 | undef_method :action_create, :action_delete, :action_edit |
48 | 50 | response = http.method(attributes.action).call(uri.request_uri, attributes.message, attributes.headers) |
49 | 51 | end |
50 | 52 | |
51 | if response.kind_of?(Net::HTTPRedirection) | |
53 | case response | |
54 | when Net::HTTPSuccess | |
55 | break | |
56 | when Net::HTTPRedirection | |
52 | 57 | if redirects_followed < attributes.redirect_limit |
53 | 58 | uri = URI.parse(response["location"]) |
54 | 59 | redirects_followed += 1 |
56 | 61 | else |
57 | 62 | raise RedirectLimitExceeded |
58 | 63 | end |
64 | when Net::HTTPClientError | |
65 | raise HTTPClientError | |
66 | when Net::HTTPServerError | |
67 | raise HTTPServerError | |
59 | 68 | else |
60 | break | |
69 | raise HTTPUnknownError | |
61 | 70 | end |
62 | 71 | end |
63 | 72 |
0 | require 'itamae' | |
1 | 0 | require 'erb' |
2 | 1 | require 'tempfile' |
3 | 2 | |
37 | 36 | |
38 | 37 | def render_file(src) |
39 | 38 | template = ::File.read(src) |
40 | ERB.new(template, nil, '-').tap do |erb| | |
41 | erb.filename = src | |
42 | end.result(binding) | |
39 | erb = | |
40 | if ERB.instance_method(:initialize).parameters.assoc(:key) # Ruby 2.6+ | |
41 | ERB.new(template, trim_mode: '-') | |
42 | else | |
43 | ERB.new(template, nil, '-') | |
44 | end | |
45 | erb.filename = src | |
46 | erb.result(binding) | |
43 | 47 | end |
44 | 48 | |
45 | 49 | def node |
0 | require 'itamae' | |
1 | 0 | require 'itamae/resource/base' |
2 | 1 | require 'itamae/resource/file' |
3 | 2 | require 'itamae/resource/package' |
0 | require 'itamae' | |
1 | 0 | require 'json' |
2 | 1 | require 'yaml' |
3 | 2 | |
5 | 4 | class Runner |
6 | 5 | class << self |
7 | 6 | def run(recipe_files, backend_type, options) |
8 | Itamae.logger.info "Starting Itamae..." | |
7 | Itamae.logger.info "Starting Itamae... #{options[:dry_run] ? '(dry-run)' : ''}" | |
9 | 8 | |
10 | 9 | backend = Backend.create(backend_type, options) |
11 | 10 | runner = self.new(backend, options) |
102 | 101 | end |
103 | 102 | |
104 | 103 | Itamae.logger.info "Loading node data via ohai..." |
105 | hash.merge!(JSON.parse(@backend.run_command("ohai").stdout)) | |
104 | hash.merge!(JSON.parse(@backend.run_command("ohai 2>/dev/null").stdout)) | |
106 | 105 | end |
107 | 106 | |
108 | 107 | if @options[:node_json] |
0 | 0 | require "itamae/version" |
1 | 1 | require "itamae/runner" |
2 | require "itamae/cli" | |
3 | 2 | require "itamae/recipe" |
4 | 3 | require "itamae/resource" |
5 | 4 | require "itamae/handler" |
12 | 11 | require "itamae/definition" |
13 | 12 | require "itamae/ext" |
14 | 13 | require "itamae/generators" |
14 | require "itamae/mash" | |
15 | 15 | |
16 | 16 | module Itamae |
17 | 17 | # Your code goes here... |
0 | # -*- mode: ruby -*- | |
1 | # vi: set ft=ruby : | |
2 | require 'vagrant-digitalocean' | |
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.hostname = 'itamae-trusty' | |
10 | c.vm.hostname += "-#{ENV['WERCKER_BUILD_ID']}" if ENV['WERCKER_BUILD_ID'] | |
11 | c.vm.provider :virtualbox do |provider, override| | |
12 | override.vm.box = "ubuntu/trusty64" | |
13 | override.vm.provision :shell, inline: <<-EOC | |
14 | cat /etc/apt/sources.list | sed -e 's|http://[^ ]*|mirror://mirrors.ubuntu.com/mirrors.txt|g' > /tmp/sources.list | |
15 | if !(diff -q /etc/apt/sources.list /tmp/sources.list); then | |
16 | mv /tmp/sources.list /etc/apt/sources.list | |
17 | apt-get update | |
18 | fi | |
19 | echo America/New_York > /etc/timezone | |
20 | dpkg-reconfigure --frontend noninteractive tzdata | |
21 | EOC | |
22 | end | |
23 | ||
24 | c.vm.provider :digital_ocean do |provider, override| | |
25 | override.ssh.private_key_path = '~/.ssh/id_rsa.vagrant' | |
26 | override.vm.box = 'digital_ocean' | |
27 | override.vm.box_url = "https://github.com/smdahlen/vagrant-digitalocean/raw/master/box/digital_ocean.box" | |
28 | ||
29 | provider.ssh_key_name = ENV['WERCKER'] ? 'vagrant/wercker/itamae' : 'Vagrant' | |
30 | provider.token = ENV['DIGITALOCEAN_TOKEN'] | |
31 | provider.image = 'ubuntu-14-04-x64' # ubuntu | |
32 | provider.region = 'nyc3' | |
33 | provider.size = '512mb' | |
34 | end | |
35 | end | |
36 | end |
78 | 78 | |
79 | 79 | describe file('/tmp/http_request.html') do |
80 | 80 | it { should be_file } |
81 | its(:content) { should match(/"from": "itamae"/) } | |
81 | its(:content) { should match(/"from":\s*"itamae"/) } | |
82 | 82 | end |
83 | 83 | |
84 | 84 | describe file('/tmp/http_request_delete.html') do |
85 | 85 | it { should be_file } |
86 | its(:content) { should match(/"from": "itamae"/) } | |
86 | its(:content) { should match(/"from":\s*"itamae"/) } | |
87 | 87 | end |
88 | 88 | |
89 | 89 | describe file('/tmp/http_request_post.html') do |
90 | 90 | it { should be_file } |
91 | its(:content) do | |
92 | should match(/"from": "itamae"/) | |
93 | should match(/"love": "sushi"/) | |
94 | end | |
91 | its(:content) { should match(/"from":\s*"itamae"/) } | |
92 | its(:content) { should match(/"love":\s*"sushi"/) } | |
95 | 93 | end |
96 | 94 | |
97 | 95 | describe file('/tmp/http_request_put.html') do |
98 | 96 | it { should be_file } |
99 | its(:content) do | |
100 | should match(/"from": "itamae"/) | |
101 | should match(/"love": "sushi"/) | |
102 | end | |
97 | its(:content) { should match(/"from":\s*"itamae"/) } | |
98 | its(:content) { should match(/"love":\s*"sushi"/) } | |
103 | 99 | end |
104 | 100 | |
105 | 101 | describe file('/tmp/http_request_headers.html') do |
106 | 102 | it { should be_file } |
107 | its(:content) { should match(/"User-Agent": "Itamae"/) } | |
103 | its(:content) { should match(/"User-Agent":\s*"Itamae"/) } | |
108 | 104 | end |
109 | 105 | |
110 | 106 | describe file('/tmp/http_request_redirect.html') do |
111 | 107 | it { should be_file } |
112 | its(:content) { should match(/"from": "itamae"/) } | |
108 | its(:content) { should match(/"from":\s*"itamae"/) } | |
113 | 109 | end |
114 | 110 | |
115 | 111 | describe file('/tmp/notifies') do |
120 | 116 | describe file('/tmp/subscribes') do |
121 | 117 | it { should be_file } |
122 | 118 | its(:content) { should eq("2431") } |
123 | end | |
124 | ||
125 | describe file('/tmp/cron_stopped') do | |
126 | it { should be_file } | |
127 | its(:content) do | |
128 | expect(subject.content.lines.size).to eq 1 | |
129 | end | |
130 | end | |
131 | ||
132 | describe file('/tmp/cron_running') do | |
133 | it { should be_file } | |
134 | its(:content) do | |
135 | expect(subject.content.lines.size).to eq 2 | |
136 | end | |
137 | 119 | end |
138 | 120 | |
139 | 121 | describe file('/tmp-link') do |
195 | 177 | end |
196 | 178 | |
197 | 179 | describe command('gem list') do |
198 | its(:stdout) { should include('rake (11.1.0)') } | |
180 | its(:stdout) { should match(/^rake \(.*11.1.0.*\)/) } | |
199 | 181 | end |
200 | 182 | |
201 | 183 | describe command('gem list') do |
202 | 184 | its(:stdout) { should_not include('test-unit') } |
203 | 185 | end |
204 | 186 | |
205 | describe command('ri Bundler') do | |
206 | its(:stderr) { should eq("Nothing known about Bundler\n") } | |
187 | describe command('gem list') do | |
188 | its(:stdout) { should include('ast (2.0.0)') } | |
189 | end | |
190 | ||
191 | describe command('ri AST') do | |
192 | its(:stderr) { should eq("Nothing known about AST\n") } | |
207 | 193 | end |
208 | 194 | |
209 | 195 | describe file('/tmp/created_by_definition') do |
214 | 200 | describe file('/tmp/remote_file_in_definition') do |
215 | 201 | it { should be_file } |
216 | 202 | its(:content) { should eq("definition_example\n") } |
203 | end | |
204 | ||
205 | describe file('/tmp/created_by_definition_2_created') do | |
206 | it { should be_file } | |
207 | its(:content) { should eq("name:created,key:value2,message:Hello, Itamae\n") } | |
208 | end | |
209 | ||
210 | describe file('/tmp/remote_file_in_definition_2_created') do | |
211 | it { should be_file } | |
212 | its(:content) { should eq("definition_example_2\n") } | |
213 | end | |
214 | ||
215 | describe file('/tmp/created_by_definition_2_not_created') do | |
216 | it { should_not exist } | |
217 | end | |
218 | ||
219 | describe file('/tmp/remote_file_in_definition_2_not_created') do | |
220 | it { should_not exist } | |
221 | end | |
222 | ||
223 | describe file('/tmp/created_by_definition_3_created') do | |
224 | it { should be_file } | |
225 | its(:content) { should eq("name:created,key:value3,message:Hello, Itamae\n") } | |
226 | end | |
227 | ||
228 | describe file('/tmp/remote_file_in_definition_3_created') do | |
229 | it { should be_file } | |
230 | its(:content) { should eq("definition_example_3\n") } | |
231 | end | |
232 | ||
233 | describe file('/tmp/created_by_definition_3_not_created') do | |
234 | it { should_not exist } | |
235 | end | |
236 | ||
237 | describe file('/tmp/remote_file_in_definition_3_not_created') do | |
238 | it { should_not exist } | |
217 | 239 | end |
218 | 240 | |
219 | 241 | describe file('/tmp/multi_delayed_notifies') do |
292 | 314 | describe file('/tmp/subscribed_from_parent') do |
293 | 315 | it { should be_file } |
294 | 316 | end |
317 | ||
318 | describe file('/tmp/empty_file1') do | |
319 | it { should exist } | |
320 | it { should be_file } | |
321 | its(:content) { should eq "" } | |
322 | end | |
323 | ||
324 | describe file('/tmp/empty_file2') do | |
325 | it { should exist } | |
326 | it { should be_file } | |
327 | its(:content) { should eq "" } | |
328 | end | |
329 | ||
330 | describe file('/tmp/empty_file3') do | |
331 | it { should exist } | |
332 | it { should be_file } | |
333 | its(:content) { should eq "" } | |
334 | end |
0 | require 'spec_helper' | |
1 | ||
2 | describe file('/tmp/cron_stopped') do | |
3 | it { should be_file } | |
4 | its(:content) do | |
5 | expect(subject.content.lines.size).to eq 1 | |
6 | end | |
7 | end | |
8 | ||
9 | # FIXME: cron service is not running in docker... | |
10 | # | |
11 | # root@3450c6da6ea5:/# ps -C cron | |
12 | # PID TTY TIME CMD | |
13 | # root@3450c6da6ea5:/# service cron start | |
14 | # Rather than invoking init scripts through /etc/init.d, use the service(8) | |
15 | # utility, e.g. service cron start | |
16 | # | |
17 | # Since the script you are attempting to invoke has been converted to an | |
18 | # Upstart job, you may also use the start(8) utility, e.g. start cron | |
19 | # root@3450c6da6ea5:/# ps -C cron | |
20 | # PID TTY TIME CMD | |
21 | # root@3450c6da6ea5:/# | |
22 | ||
23 | # describe file('/tmp/cron_running') do | |
24 | # it { should be_file } | |
25 | # its(:content) do | |
26 | # expect(subject.content.lines.size).to eq 2 | |
27 | # end | |
28 | # end |
0 | describe file('/tmp/file_as_ordinary_user') do | |
1 | it { should be_file } | |
2 | it { should be_owned_by "itamae" } | |
3 | it { should be_grouped_into "itamae" } | |
4 | end | |
5 |
0 | require 'spec_helper' | |
1 | ||
2 | describe file('/tmp/remote_file') do | |
3 | it { should be_file } | |
4 | it { should be_owned_by "ordinary_san" } | |
5 | it { should be_grouped_into "ordinary_san" } | |
6 | its(:content) { should match(/Hello Itamae/) } | |
7 | end | |
8 | ||
9 | describe file('/tmp/remote_file_root') do | |
10 | it { should be_file } | |
11 | it { should be_owned_by "root" } | |
12 | it { should be_grouped_into "root" } | |
13 | its(:content) { should match(/Hello Itamae/) } | |
14 | end | |
15 | ||
16 | %w[/tmp/remote_file_another_ordinary /tmp/remote_file_another_ordinary_with_root].each do |path| | |
17 | describe file(path) do | |
18 | it { should be_file } | |
19 | it { should be_owned_by "itamae" } | |
20 | it { should be_grouped_into "itamae" } | |
21 | its(:content) { should match(/Hello Itamae/) } | |
22 | end | |
23 | end | |
24 | ||
25 | ### | |
26 | ||
27 | describe file('/tmp/file') do | |
28 | it { should be_file } | |
29 | it { should be_owned_by "ordinary_san" } | |
30 | it { should be_grouped_into "ordinary_san" } | |
31 | its(:content) { should match(/Hello World/) } | |
32 | end | |
33 | ||
34 | describe file('/tmp/file_root') do | |
35 | it { should be_file } | |
36 | it { should be_owned_by "root" } | |
37 | it { should be_grouped_into "root" } | |
38 | its(:content) { should match(/Hello World/) } | |
39 | end | |
40 | ||
41 | %w[/tmp/file_another_ordinary /tmp/file_another_ordinary_with_root].each do |path| | |
42 | describe file(path) do | |
43 | it { should be_file } | |
44 | it { should be_owned_by "itamae" } | |
45 | it { should be_grouped_into "itamae" } | |
46 | its(:content) { should match(/Hello World/) } | |
47 | end | |
48 | end | |
49 | ||
50 | ### | |
51 | ||
52 | describe file('/tmp/template') do | |
53 | it { should be_file } | |
54 | it { should be_owned_by "ordinary_san" } | |
55 | it { should be_grouped_into "ordinary_san" } | |
56 | its(:content) { should match(/Hello/) } | |
57 | its(:content) { should match(/Good bye/) } | |
58 | its(:content) { should match(/^total memory: \d+kB$/) } | |
59 | its(:content) { should match(/^uninitialized node key: $/) } | |
60 | end | |
61 | ||
62 | describe file('/tmp/template_root') do | |
63 | it { should be_file } | |
64 | it { should be_owned_by "root" } | |
65 | it { should be_grouped_into "root" } | |
66 | its(:content) { should match(/Hello/) } | |
67 | its(:content) { should match(/Good bye/) } | |
68 | its(:content) { should match(/^total memory: \d+kB$/) } | |
69 | its(:content) { should match(/^uninitialized node key: $/) } | |
70 | end | |
71 | ||
72 | %w[/tmp/template_another_ordinary /tmp/template_another_ordinary_with_root].each do |path| | |
73 | describe file(path) do | |
74 | it { should be_file } | |
75 | it { should be_owned_by "itamae" } | |
76 | it { should be_grouped_into "itamae" } | |
77 | its(:content) { should match(/Hello/) } | |
78 | its(:content) { should match(/Good bye/) } | |
79 | its(:content) { should match(/^total memory: \d+kB$/) } | |
80 | its(:content) { should match(/^uninitialized node key: $/) } | |
81 | end | |
82 | end | |
83 | ||
84 | ### | |
85 | ||
86 | describe file('/tmp/http_request.html') do | |
87 | it { should be_file } | |
88 | it { should be_owned_by "ordinary_san" } | |
89 | it { should be_grouped_into "ordinary_san" } | |
90 | its(:content) { should match(/"from":\s*"itamae"/) } | |
91 | end | |
92 | ||
93 | describe file('/tmp/http_request_root.html') do | |
94 | it { should be_file } | |
95 | it { should be_owned_by "root" } | |
96 | it { should be_grouped_into "root" } | |
97 | its(:content) { should match(/"from":\s*"itamae"/) } | |
98 | end | |
99 | ||
100 | %w[/tmp/http_request_another_ordinary.html /tmp/http_request_another_ordinary_with_root.html].each do |path| | |
101 | describe file(path) do | |
102 | it { should be_file } | |
103 | it { should be_owned_by "itamae" } | |
104 | it { should be_grouped_into "itamae" } | |
105 | its(:content) { should match(/"from":\s*"itamae"/) } | |
106 | end | |
107 | end |
44 | 44 | action :install |
45 | 45 | end |
46 | 46 | |
47 | package 'sl' do | |
48 | version '3.03-17' | |
49 | end | |
50 | ||
51 | 47 | package 'resolvconf' do |
52 | 48 | action :remove |
53 | 49 | end |
62 | 58 | |
63 | 59 | gem_package 'tzinfo' do |
64 | 60 | version '1.2.2' |
65 | end | |
66 | ||
67 | gem_package 'bundler' do | |
68 | options ['--no-ri', '--no-rdoc'] | |
69 | 61 | end |
70 | 62 | |
71 | 63 | gem_package 'rake' do |
82 | 74 | end |
83 | 75 | |
84 | 76 | gem_package 'test-unit' do |
85 | version '3.2.0' | |
77 | version '2.5.5' | |
86 | 78 | end |
87 | 79 | |
88 | 80 | gem_package 'test-unit' do |
89 | version '3.1.9' | |
81 | version '2.4.9' | |
90 | 82 | end |
91 | 83 | |
92 | 84 | gem_package 'test-unit' do |
222 | 214 | url "https://httpbin.org/redirect-to?url=https%3A%2F%2Fhttpbin.org%2Fget%3Ffrom%3Ditamae" |
223 | 215 | end |
224 | 216 | |
225 | ###### | |
226 | ||
227 | service "cron" do | |
228 | action :stop | |
229 | end | |
230 | ||
231 | execute "ps -C cron > /tmp/cron_stopped; true" | |
232 | ||
233 | service "cron" do | |
234 | action :start | |
235 | end | |
236 | ||
237 | execute "ps -C cron > /tmp/cron_running; true" | |
238 | ||
239 | ###### | |
240 | ||
241 | package "nginx" do | |
242 | options "--force-yes" | |
243 | end | |
244 | ||
245 | service "nginx" do | |
246 | action [:enable, :start] | |
247 | end | |
248 | ||
249 | execute "test -f /etc/rc3.d/S20nginx" # test | |
250 | execute "test $(ps h -C nginx | wc -l) -gt 0" # test | |
251 | ||
252 | service "nginx" do | |
253 | action [:disable, :stop] | |
254 | end | |
255 | ||
256 | execute "test ! -f /etc/rc3.d/S20nginx" # test | |
257 | execute "test $(ps h -C nginx | wc -l) -eq 0" # test | |
258 | ||
259 | ###### | |
260 | ||
261 | 217 | link "/tmp-link" do |
262 | 218 | to "/tmp" |
263 | 219 | end |
270 | 226 | |
271 | 227 | ###### |
272 | 228 | |
273 | execute "mkdir /tmp/link-force-no-dereference1" | |
229 | execute "mkdir -p /tmp/link-force-no-dereference1" | |
274 | 230 | link "link-force-no-dereference" do |
275 | 231 | cwd "/tmp" |
276 | 232 | to "link-force-no-dereference1" |
277 | 233 | force true |
278 | 234 | end |
279 | 235 | |
280 | execute "mkdir /tmp/link-force-no-dereference2" | |
236 | execute "mkdir -p /tmp/link-force-no-dereference2" | |
281 | 237 | link "link-force-no-dereference" do |
282 | 238 | cwd "/tmp" |
283 | 239 | to "link-force-no-dereference2" |
339 | 295 | |
340 | 296 | definition_example "name" do |
341 | 297 | key 'value' |
298 | end | |
299 | ||
300 | execute "touch /tmp/trigger_for_definition_example_2" | |
301 | ||
302 | definition_example_2 "created" do | |
303 | key "value2" | |
304 | only_if "test -f /tmp/trigger_for_definition_example_2" | |
305 | end | |
306 | ||
307 | definition_example_2 "not_created" do | |
308 | key "value2" | |
309 | not_if "test -f /tmp/trigger_for_definition_example_2" | |
310 | end | |
311 | ||
312 | definition_example_3 "created" do | |
313 | key "value3" | |
314 | not_if "test -f /tmp/this_file_is_not_exists" | |
315 | end | |
316 | ||
317 | definition_example_3 "not_created" do | |
318 | key "value3" | |
319 | only_if "test -f /tmp/this_file_is_not_exists" | |
342 | 320 | end |
343 | 321 | |
344 | 322 | ##### |
555 | 533 | |
556 | 534 | ### |
557 | 535 | |
536 | file "/tmp/empty_file1" do | |
537 | content "" | |
538 | end | |
539 | ||
540 | remote_file "/tmp/empty_file2" do | |
541 | source "files/empty_file" | |
542 | end | |
543 | ||
544 | template "/tmp/empty_file3" do | |
545 | source "templates/empty_file.erb" | |
546 | end | |
547 | ||
548 | ### | |
549 | ||
558 | 550 | v1 = node.memory.total |
559 | 551 | v2 = node[:memory][:total] |
560 | 552 | v3 = node['memory']['total'] |
3 | 3 | remote_file "/tmp/remote_file_in_definition" |
4 | 4 | end |
5 | 5 | |
6 | define :definition_example_2, key: 'default' do | |
7 | execute "echo 'name:#{params[:name]},key:#{params[:key]},message:#{node[:message]}' > /tmp/created_by_definition_2_#{params[:name]}" | |
8 | ||
9 | remote_file "/tmp/remote_file_in_definition_2_#{params[:name]}" do | |
10 | source "files/remote_file_in_definition_2" | |
11 | end | |
12 | end | |
13 | ||
14 | define :definition_example_3, key: 'default' do | |
15 | execute "echo 'name:#{params[:name]},key:#{params[:key]},message:#{node[:message]}' > /tmp/created_by_definition_3_#{params[:name]}" | |
16 | ||
17 | remote_file "/tmp/remote_file_in_definition_3_#{params[:name]}" do | |
18 | source "files/remote_file_in_definition_3" | |
19 | end | |
20 | end |
0 | definition_example_2 |
0 | definition_example_3 |
0 | package 'sl' do | |
1 | version '3.03-17' | |
2 | end | |
3 | ||
4 | ###### | |
5 | ||
6 | gem_package 'ast' do | |
7 | version '2.0.0' | |
8 | options ['--no-ri', '--no-rdoc'] | |
9 | end | |
10 | ||
11 | ###### | |
12 | ||
13 | service "cron" do | |
14 | action :stop | |
15 | end | |
16 | ||
17 | execute "ps -C cron > /tmp/cron_stopped; true" | |
18 | ||
19 | service "cron" do | |
20 | action :start | |
21 | end | |
22 | ||
23 | execute "ps -C cron > /tmp/cron_running; true" | |
24 | ||
25 | ###### | |
26 | ||
27 | package "nginx" do | |
28 | options "--force-yes" | |
29 | end | |
30 | ||
31 | service "nginx" do | |
32 | action [:enable, :start] | |
33 | end | |
34 | ||
35 | execute "test -f /etc/rc3.d/S20nginx" # test | |
36 | execute "test $(ps h -C nginx | wc -l) -gt 0" # test | |
37 | ||
38 | service "nginx" do | |
39 | action [:disable, :stop] | |
40 | end | |
41 | ||
42 | execute "test ! -f /etc/rc3.d/S20nginx" # test | |
43 | execute "test $(ps h -C nginx | wc -l) -eq 0" # test |
0 | package 'sl' | |
1 | ||
2 | ###### | |
3 | ||
4 | gem_package 'ast' do | |
5 | version '2.0.0' | |
6 | options ['--no-document'] | |
7 | end | |
8 | ||
9 | ###### | |
10 | ||
11 | # Docker backend raises an error with `user` option, so it tests only on `itamae local`. | |
12 | # After fix this error, please move this code and the spec to `default.rb`. | |
13 | file "/tmp/file_as_ordinary_user" do | |
14 | content "Hello World" | |
15 | user "itamae" | |
16 | owner "itamae" | |
17 | group "itamae" | |
18 | end |
0 | remote_file "/tmp/remote_file" do | |
1 | source "hello.txt" | |
2 | end | |
3 | ||
4 | remote_file "/tmp/remote_file_root" do | |
5 | user 'root' | |
6 | owner 'root' | |
7 | group 'root' | |
8 | source "hello.txt" | |
9 | end | |
10 | ||
11 | remote_file "/tmp/remote_file_another_ordinary" do | |
12 | user 'itamae' | |
13 | owner 'itamae' | |
14 | group 'itamae' | |
15 | source "hello.txt" | |
16 | end | |
17 | ||
18 | remote_file "/tmp/remote_file_another_ordinary_with_root" do | |
19 | user 'root' | |
20 | owner 'itamae' | |
21 | group 'itamae' | |
22 | source "hello.txt" | |
23 | end | |
24 | ||
25 | ### | |
26 | ||
27 | file "/tmp/file" do | |
28 | content "Hello World" | |
29 | end | |
30 | ||
31 | file "/tmp/file_root" do | |
32 | user 'root' | |
33 | owner 'root' | |
34 | group 'root' | |
35 | content 'Hello World' | |
36 | end | |
37 | ||
38 | file "/tmp/file_another_ordinary" do | |
39 | user 'itamae' | |
40 | owner 'itamae' | |
41 | group 'itamae' | |
42 | content 'Hello World' | |
43 | end | |
44 | ||
45 | file "/tmp/file_another_ordinary_with_root" do | |
46 | user 'root' | |
47 | owner 'itamae' | |
48 | group 'itamae' | |
49 | content 'Hello World' | |
50 | end | |
51 | ||
52 | ### | |
53 | ||
54 | template "/tmp/template" do | |
55 | source "hello.erb" | |
56 | variables goodbye: "Good bye" | |
57 | end | |
58 | ||
59 | template "/tmp/template_root" do | |
60 | user 'root' | |
61 | owner 'root' | |
62 | group 'root' | |
63 | source "hello.erb" | |
64 | variables goodbye: "Good bye" | |
65 | end | |
66 | ||
67 | template "/tmp/template_another_ordinary" do | |
68 | user 'itamae' | |
69 | owner 'itamae' | |
70 | group 'itamae' | |
71 | source "hello.erb" | |
72 | variables goodbye: "Good bye" | |
73 | end | |
74 | ||
75 | template "/tmp/template_another_ordinary_with_root" do | |
76 | user 'root' | |
77 | owner 'itamae' | |
78 | group 'itamae' | |
79 | source "hello.erb" | |
80 | variables goodbye: "Good bye" | |
81 | end | |
82 | ||
83 | ### | |
84 | ||
85 | http_request "/tmp/http_request.html" do | |
86 | url "https://httpbin.org/get?from=itamae" | |
87 | end | |
88 | ||
89 | http_request "/tmp/http_request_root.html" do | |
90 | user 'root' | |
91 | owner 'root' | |
92 | group 'root' | |
93 | url "https://httpbin.org/get?from=itamae" | |
94 | end | |
95 | ||
96 | http_request "/tmp/http_request_another_ordinary.html" do | |
97 | user 'itamae' | |
98 | owner 'itamae' | |
99 | group 'itamae' | |
100 | url "https://httpbin.org/get?from=itamae" | |
101 | end | |
102 | ||
103 | http_request "/tmp/http_request_another_ordinary_with_root.html" do | |
104 | user 'root' | |
105 | owner 'itamae' | |
106 | group 'itamae' | |
107 | url "https://httpbin.org/get?from=itamae" | |
108 | end |
0 | require 'serverspec' | |
1 | require 'net/ssh' | |
2 | require 'tempfile' | |
0 | require "serverspec" | |
1 | require "docker" | |
3 | 2 | |
4 | set :backend, :ssh | |
3 | set :backend, :docker | |
5 | 4 | |
6 | def vagrant(cmd) | |
7 | env = {"VAGRANT_CWD" => File.dirname(__FILE__)} | |
8 | system(env, "vagrant #{cmd}") | |
9 | end | |
10 | ||
11 | if ENV['ASK_SUDO_PASSWORD'] | |
12 | begin | |
13 | require 'highline/import' | |
14 | rescue LoadError | |
15 | fail "highline is not available. Try installing it." | |
16 | end | |
17 | set :sudo_password, ask("Enter sudo password: ") { |q| q.echo = false } | |
18 | else | |
19 | set :sudo_password, ENV['SUDO_PASSWORD'] | |
20 | end | |
21 | ||
22 | host = ENV['TARGET_HOST'] | |
23 | ||
24 | config = Tempfile.new('', Dir.tmpdir) | |
25 | vagrant "ssh-config #{host} > #{config.path}" | |
26 | ||
27 | options = Net::SSH::Config.for(host, [config.path]) | |
28 | ||
29 | options[:user] ||= Etc.getlogin | |
30 | ||
31 | set :host, options[:host_name] || host | |
32 | set :ssh_options, options | |
5 | set :docker_image, ENV["DOCKER_IMAGE"] | |
6 | set :docker_container, ENV["DOCKER_CONTAINER"] | |
33 | 7 | |
34 | 8 | # Disable sudo |
35 | 9 | # set :disable_sudo, true |
39 | 13 | |
40 | 14 | # Set PATH |
41 | 15 | # set :path, '/sbin:/usr/local/sbin:$PATH' |
16 | ||
17 | RSpec.configure do |config| | |
18 | unless ENV["CI"] | |
19 | # focus is enabled only local (Run all specs at CI) | |
20 | config.filter_run_when_matching :focus | |
21 | end | |
22 | end |
16 | 16 | |
17 | 17 | describe ".send_file" do |
18 | 18 | context "the source file doesn't exist" do |
19 | subject { -> { itamae_backend.send_file("src", "dst") } } | |
20 | it { expect(subject).to raise_error(Itamae::Backend::SourceNotExistError, "The file 'src' doesn't exist.") } | |
19 | subject { itamae_backend.send_file("src", "dst") } | |
20 | it { expect{ subject }.to raise_error(Itamae::Backend::SourceNotExistError, "The file 'src' doesn't exist.") } | |
21 | 21 | end |
22 | 22 | |
23 | 23 | context "the source file exist, but it is not a regular file" do |
24 | 24 | before { Dir.mkdir("src") } |
25 | subject { -> { itamae_backend.send_file("src", "dst") } } | |
26 | it { expect(subject).to raise_error(Itamae::Backend::SourceNotExistError, "'src' is not a file.") } | |
25 | subject { itamae_backend.send_file("src", "dst") } | |
26 | it { expect{ subject }.to raise_error(Itamae::Backend::SourceNotExistError, "'src' is not a file.") } | |
27 | 27 | end |
28 | 28 | |
29 | 29 | context "the source file is a regular file" do |
30 | 30 | before { FileUtils.touch("src") } |
31 | subject { -> { itamae_backend.send_file("src", "dst") } } | |
31 | subject { itamae_backend.send_file("src", "dst") } | |
32 | 32 | it { expect { subject }.not_to raise_error } |
33 | 33 | end |
34 | 34 | end |
35 | 35 | |
36 | 36 | describe ".send_directory" do |
37 | 37 | context "the source directory doesn't exist" do |
38 | subject { -> { itamae_backend.send_directory("src", "dst") } } | |
39 | it { expect(subject).to raise_error(Itamae::Backend::SourceNotExistError, "The directory 'src' doesn't exist.") } | |
38 | subject { itamae_backend.send_directory("src", "dst") } | |
39 | it { expect{ subject }.to raise_error(Itamae::Backend::SourceNotExistError, "The directory 'src' doesn't exist.") } | |
40 | 40 | end |
41 | 41 | |
42 | 42 | context "the source directory exist, but it is not a directory" do |
43 | 43 | before { FileUtils.touch("src") } |
44 | subject { -> { itamae_backend.send_directory("src", "dst") } } | |
45 | it { expect(subject).to raise_error(Itamae::Backend::SourceNotExistError, "'src' is not a directory.") } | |
44 | subject { itamae_backend.send_directory("src", "dst") } | |
45 | it { expect{ subject }.to raise_error(Itamae::Backend::SourceNotExistError, "'src' is not a directory.") } | |
46 | 46 | end |
47 | 47 | |
48 | 48 | context "the source directory is a directory" do |
49 | 49 | before { Dir.mkdir("src") } |
50 | subject { -> { itamae_backend.send_directory("src", "dst") } } | |
50 | subject { itamae_backend.send_directory("src", "dst") } | |
51 | 51 | it { expect { subject }.not_to raise_error } |
52 | 52 | end |
53 | 53 | end |
19 | 19 | expect(handler).to receive(:event).with(:name_started, :arg) |
20 | 20 | expect(handler).to receive(:event).with(:name_failed, :arg) |
21 | 21 | expect { |
22 | subject.event(:name, :arg) { raise } | |
23 | }.to raise_error | |
22 | subject.event(:name, :arg) { raise "name is failed" } | |
23 | }.to raise_error "name is failed" | |
24 | 24 | end |
25 | 25 | end |
26 | 26 | end |
0 | desc 'Run all integration tests on `itamae local` command' | |
1 | task 'spec:integration:local' => ['spec:integration:local:main', 'spec:integration:local:ordinary_user'] | |
2 | ||
3 | namespace 'spec:integration:local' do | |
4 | desc 'Run main integration test with `itamae local`' | |
5 | task 'main' do | |
6 | if RUBY_DESCRIPTION.include?('dev') | |
7 | $stderr.puts "This integration test is skipped with unreleased Ruby." | |
8 | $stderr.puts "Use released Ruby to execute this integration test." | |
9 | next | |
10 | end | |
11 | ||
12 | IntegrationLocalSpecRunner.new( | |
13 | [ | |
14 | [ | |
15 | "spec/integration/recipes/default.rb", | |
16 | "spec/integration/recipes/default2.rb", | |
17 | "spec/integration/recipes/redefine.rb", | |
18 | "spec/integration/recipes/local.rb", | |
19 | ], | |
20 | [ | |
21 | "--dry-run", | |
22 | "spec/integration/recipes/dry_run.rb", | |
23 | ], | |
24 | ], | |
25 | ['spec/integration/default_spec.rb'] | |
26 | ).run | |
27 | ||
28 | end | |
29 | ||
30 | desc 'Run integration test for ordinary user with `itamae local`' | |
31 | task 'ordinary_user' do | |
32 | if RUBY_DESCRIPTION.include?('dev') | |
33 | $stderr.puts "This integration test is skipped with unreleased Ruby." | |
34 | $stderr.puts "Use released Ruby to execute this integration test." | |
35 | next | |
36 | end | |
37 | ||
38 | runner = IntegrationLocalSpecRunner.new( | |
39 | [ | |
40 | [ | |
41 | "--dry-run", | |
42 | "spec/integration/recipes/ordinary_user.rb", | |
43 | ], | |
44 | [ | |
45 | "spec/integration/recipes/ordinary_user.rb" | |
46 | ], | |
47 | ], | |
48 | ['spec/integration/ordinary_user_spec.rb'], | |
49 | user: 'ordinary_san' | |
50 | ) | |
51 | runner.docker_exec 'useradd', 'ordinary_san', '-p', '*' | |
52 | runner.docker_exec 'useradd', 'itamae', '-p', '*', '--create-home' | |
53 | runner.docker_exec 'sh', '-c', 'echo "ordinary_san ALL=(ALL:ALL) NOPASSWD: ALL" >> /etc/sudoers' | |
54 | runner.run | |
55 | end | |
56 | end | |
57 | ||
58 | class IntegrationLocalSpecRunner | |
59 | CONTAINER_NAME = 'itamae' | |
60 | include FileUtils | |
61 | ||
62 | def initialize(suites, specs, ruby_version: RUBY_VERSION.split('.')[0..1].join('.'), user: nil) | |
63 | @suites = suites | |
64 | @specs = specs | |
65 | @ruby_version = ruby_version | |
66 | @user = user | |
67 | ||
68 | docker_run | |
69 | prepare | |
70 | end | |
71 | ||
72 | def run | |
73 | provision | |
74 | serverspec | |
75 | clean_docker_container | |
76 | end | |
77 | ||
78 | def docker_run | |
79 | mount_dir = Pathname(__dir__).join('../').to_s | |
80 | sh 'docker', 'run', '--privileged', '-d', '--name', CONTAINER_NAME, '-v', "#{mount_dir}:/itamae", "ruby:#{@ruby_version}", 'sleep', '1d' | |
81 | end | |
82 | ||
83 | def prepare | |
84 | docker_exec 'gem', 'install', 'bundler' | |
85 | docker_exec 'bundle', 'install', options: %w[--workdir /itamae] | |
86 | docker_exec 'apt-get', 'update', '-y' | |
87 | docker_exec 'apt-get', 'install', 'locales', 'sudo', '-y' | |
88 | docker_exec 'localedef', '-i', 'en_US', '-c', '-f', 'UTF-8', '-A', '/usr/share/locale/locale.alias', 'en_US.UTF-8' | |
89 | end | |
90 | ||
91 | def provision | |
92 | @suites.each do |suite| | |
93 | cmd = %W!bundle exec ruby -w bin/itamae local! | |
94 | cmd << "-l" << (ENV['LOG_LEVEL'] || 'debug') | |
95 | cmd << "-j" << "spec/integration/recipes/node.json" | |
96 | cmd += suite | |
97 | ||
98 | options = %w[--workdir /itamae] | |
99 | options.push('--user', @user) if @user | |
100 | docker_exec(*cmd, options: options) | |
101 | end | |
102 | end | |
103 | ||
104 | def serverspec | |
105 | ENV['DOCKER_CONTAINER'] = CONTAINER_NAME | |
106 | sh('bundle', 'exec', 'rspec', '-I', './spec/integration', *@specs) | |
107 | end | |
108 | ||
109 | def clean_docker_container | |
110 | sh('docker', 'rm', '-f', CONTAINER_NAME) | |
111 | end | |
112 | ||
113 | def docker_exec(*cmd, options: []) | |
114 | sh 'docker', 'exec', '--env', 'LANG=en_US.utf8', *options, CONTAINER_NAME, *cmd | |
115 | end | |
116 | end |
0 | box: wercker/rvm | |
1 | # Build definition | |
2 | build: | |
3 | # The steps that will be executed on build | |
4 | # See the Ruby section on the wercker devcenter: | |
5 | # http://devcenter.wercker.com/articles/languages/ruby.html | |
6 | steps: | |
7 | # Uncomment this to force RVM to use a specific Ruby version | |
8 | - rvm-use: | |
9 | version: 2.2.3 | |
10 | ||
11 | - script: | |
12 | name: update bundler | |
13 | code: gem update bundler | |
14 | ||
15 | # A step that executes `bundle install` command | |
16 | - bundle-install | |
17 | ||
18 | # A custom script step, name value is used in the UI | |
19 | # and the code value contains the command that get executed | |
20 | - script: | |
21 | name: echo ruby information | |
22 | code: | | |
23 | echo "ruby version $(ruby --version) running" | |
24 | echo "from location $(which ruby)" | |
25 | echo -p "gem list: $(gem list)" | |
26 | ||
27 | - script: | |
28 | name: create .ssh directory | |
29 | code: mkdir -p $HOME/.ssh | |
30 | ||
31 | - create-file: | |
32 | name: put private key | |
33 | filename: $HOME/.ssh/id_rsa.vagrant | |
34 | overwrite: true | |
35 | hide-from-log: true | |
36 | content: $DIGITALOCEAN_PRIVATE_KEY | |
37 | ||
38 | - create-file: | |
39 | name: put public key | |
40 | filename: $HOME/.ssh/id_rsa.vagrant.pub | |
41 | overwrite: true | |
42 | hide-from-log: true | |
43 | content: $DIGITALOCEAN_PUBLIC_KEY | |
44 | ||
45 | - script: | |
46 | name: chmod 600 id_rsa | |
47 | code: chmod 600 $HOME/.ssh/id_rsa.vagrant | |
48 | ||
49 | - script: | |
50 | name: install libxml and libxslt | |
51 | code: sudo apt-get install libxml2-dev libxslt1-dev | |
52 | ||
53 | - script: | |
54 | name: bundle install | |
55 | code: bundle install --deployment -j4 | |
56 | ||
57 | - script: | |
58 | name: start vm | |
59 | code: bundle exec vagrant up --provider=digital_ocean | |
60 | cwd: spec/integration | |
61 | ||
62 | # Add more steps here: | |
63 | - script: | |
64 | name: rspec | |
65 | code: bundle exec rake spec | |
66 | ||
67 | after-steps: | |
68 | - script: | |
69 | name: shutdown vm | |
70 | code: bundle exec vagrant destroy -f | |
71 | cwd: spec/integration | |
72 | ||
73 | - script: | |
74 | name: shutdown old vms | |
75 | code: bundle exec ruby ci/destroy_old_droplets.rb | |
76 |