itamae-1.9.10/0000755000175000017500000000000013017534453012121 5ustar scottscottitamae-1.9.10/LICENSE.txt0000644000175000017500000000206013017534453013742 0ustar scottscottCopyright (c) 2013-2015 Ryota Arai MIT License Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. itamae-1.9.10/Rakefile0000644000175000017500000000455313017534453013575 0ustar scottscottrequire "bundler/gem_tasks" require 'rspec/core/rake_task' require 'tempfile' require 'net/ssh' vagrant_bin = 'vagrant' desc 'Run unit and integration specs.' task :spec => ['spec:unit', 'spec:integration:all'] namespace :spec do RSpec::Core::RakeTask.new("unit") do |task| task.ruby_opts = '-I ./spec/unit' task.pattern = "./spec/unit{,/*/**}/*_spec.rb" end namespace :integration do targets = [] status = `cd spec/integration && #{vagrant_bin} status` unless $?.exitstatus == 0 raise "vagrant status failed.\n#{status}" end status.split("\n\n")[1].each_line do |line| targets << line.match(/^[^ ]+/)[0] end task :all => targets targets.each do |target| desc "Run provision and specs to #{target}" task target => ["provision:#{target}", "serverspec:#{target}"] namespace :provision do task target do config = Tempfile.new('', Dir.tmpdir) env = {"VAGRANT_CWD" => File.expand_path('./spec/integration')} system env, "#{vagrant_bin} up #{target}" system env, "#{vagrant_bin} ssh-config #{target} > #{config.path}" options = Net::SSH::Config.for(target, [config.path]) suites = [ [ "spec/integration/recipes/default.rb", "spec/integration/recipes/default2.rb", "spec/integration/recipes/redefine.rb", ], [ "--dry-run", "spec/integration/recipes/dry_run.rb", ], ] suites.each do |suite| cmd = %w!bundle exec bin/itamae ssh! cmd << "-h" << options[:host_name] cmd << "-u" << options[:user] cmd << "-p" << options[:port].to_s cmd << "-i" << options[:keys].first cmd << "-l" << (ENV['LOG_LEVEL'] || 'debug') cmd << "-j" << "spec/integration/recipes/node.json" cmd += suite p cmd unless system(*cmd) raise "#{cmd} failed" end end end end namespace :serverspec do desc "Run serverspec tests to #{target}" RSpec::Core::RakeTask.new(target.to_sym) do |t| ENV['TARGET_HOST'] = target t.ruby_opts = '-I ./spec/integration' t.pattern = "spec/integration/*_spec.rb" end end end end end itamae-1.9.10/.gitignore0000644000175000017500000000026113017534453014110 0ustar scottscott*.gem *.rbc .bundle .config .yardoc Gemfile.lock InstalledFiles _yardoc coverage doc/ lib/bundler/man pkg rdoc spec/reports test/tmp test/version_tmp tmp .vagrant Gemfile.local itamae-1.9.10/README.md0000644000175000017500000000563313017534453013407 0ustar scottscott# [![](https://raw.githubusercontent.com/itamae-kitchen/itamae-logos/master/small/FA-Itamae-horizontal-01-180x72.png)](https://github.com/itamae-kitchen/itamae) [![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/) Simple and lightweight configuration management tool inspired by Chef. - [CHANGELOG](https://github.com/itamae-kitchen/itamae/blob/master/CHANGELOG.md) ## Concept - Chef-like DSL (but not compatible with Chef) - Simpler and lighter weight than Chef - Only recipes - Idempotent ## Installation ``` $ gem install itamae ``` ## Getting Started Create a recipe file as `recipe.rb`: ```ruby package 'nginx' do action :install end service 'nginx' do action [:enable, :start] end ``` And then excute `itamae` command to apply a recipe to a local machine. ``` $ itamae local recipe.rb INFO : Starting Itamae... INFO : Recipe: /home/user/recipe.rb INFO : package[nginx] INFO : action: install INFO : installed will change from 'false' to 'true' INFO : service[nginx] INFO : action: enable INFO : action: start ``` Or you can apply a recipe to a remote machine by `itamae ssh`. ``` $ itamae ssh --host host001.example.jp recipe.rb ``` You can also apply a recipe to Vagrant VM by `itamae ssh --vagrant`. ``` $ itamae ssh --vagrant --host vm_name recipe.rb ``` You can find further information to use Itamae on [Itamae Wiki](https://github.com/itamae-kitchen/itamae/wiki). Enjoy! ## Documentation https://github.com/itamae-kitchen/itamae/wiki ## Run tests Requirements: Vagrant ``` $ bundle exec rake spec ``` ## Get Involved - [Join Slack team](https://itamae-slackin.herokuapp.com/) ## Presentations / Articles ### in Japanese - [Itamae - Infra as Code 現状確認会](https://speakerdeck.com/ryotarai/itamae-infra-as-code-xian-zhuang-que-ren-hui) - [クックパッドのサーバプロビジョニング事情 - クックパッド開発者ブログ](http://techlife.cookpad.com/entry/2015/05/12/080000) - [Itamaeが構成管理を仕込みます! ~新進気鋭の国産・構成管理ツール~:連載|gihyo.jp … 技術評論社](http://gihyo.jp/admin/serial/01/itamae) ## Contributing If you have a problem, please [create an issue](https://github.com/itamae-kitchen/itamae/issues/new) or a pull request. 1. Fork it 2. Create your feature branch (`git checkout -b my-new-feature`) 3. Commit your changes (`git commit -am 'Add some feature'`) 4. Push to the branch (`git push origin my-new-feature`) 5. Create new Pull Request itamae-1.9.10/Gemfile0000644000175000017500000000052013017534453013411 0ustar scottscottsource 'https://rubygems.org' # Specify your gem's dependencies in itamae.gemspec gemspec gem 'vagrant', github: 'ryotarai/vagrant', branch: 'latest-bundler' gem 'vagrant-digitalocean' path = Pathname.new("Gemfile.local") eval(path.read) if path.exist? group :test do if RUBY_PLATFORM.include?('darwin') gem 'growl' end end itamae-1.9.10/wercker.yml0000644000175000017500000000375213017534453014315 0ustar scottscottbox: wercker/rvm # Build definition build: # The steps that will be executed on build # See the Ruby section on the wercker devcenter: # http://devcenter.wercker.com/articles/languages/ruby.html steps: # Uncomment this to force RVM to use a specific Ruby version - rvm-use: version: 2.2.3 - script: name: update bundler code: gem update bundler # A step that executes `bundle install` command - bundle-install # A custom script step, name value is used in the UI # and the code value contains the command that get executed - script: name: echo ruby information code: | echo "ruby version $(ruby --version) running" echo "from location $(which ruby)" echo -p "gem list: $(gem list)" - script: name: create .ssh directory code: mkdir -p $HOME/.ssh - create-file: name: put private key filename: $HOME/.ssh/id_rsa.vagrant overwrite: true hide-from-log: true content: $DIGITALOCEAN_PRIVATE_KEY - create-file: name: put public key filename: $HOME/.ssh/id_rsa.vagrant.pub overwrite: true hide-from-log: true content: $DIGITALOCEAN_PUBLIC_KEY - script: name: chmod 600 id_rsa code: chmod 600 $HOME/.ssh/id_rsa.vagrant - script: name: install libxml and libxslt code: sudo apt-get install libxml2-dev libxslt1-dev - script: name: bundle install code: bundle install --deployment -j4 - script: name: start vm code: bundle exec vagrant up --provider=digital_ocean cwd: spec/integration # Add more steps here: - script: name: rspec code: bundle exec rake spec after-steps: - script: name: shutdown vm code: bundle exec vagrant destroy -f cwd: spec/integration - script: name: shutdown old vms code: bundle exec ruby ci/destroy_old_droplets.rb itamae-1.9.10/itamae.gemspec0000644000175000017500000000251013017534453014724 0ustar scottscott# coding: utf-8 lib = File.expand_path('../lib', __FILE__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require 'itamae/version' Gem::Specification.new do |spec| spec.name = "itamae" spec.version = Itamae::VERSION spec.authors = ["Ryota Arai"] spec.email = ["ryota.arai@gmail.com"] spec.summary = %q{Simple Configuration Management Tool} spec.homepage = "https://github.com/itamae-kitchen/itamae" spec.license = "MIT" spec.files = `git ls-files`.split($/) spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) spec.require_paths = ["lib"] spec.add_runtime_dependency "thor" spec.add_runtime_dependency "specinfra", [">= 2.64.0", "< 3.0.0"] spec.add_runtime_dependency "hashie" spec.add_runtime_dependency "ansi" spec.add_runtime_dependency "schash", "~> 0.1.0" spec.add_development_dependency "bundler", "~> 1.3" spec.add_development_dependency "rake" spec.add_development_dependency "rspec", "~> 3.0" spec.add_development_dependency "serverspec", "~> 2.1" spec.add_development_dependency "pry-byebug" spec.add_development_dependency "docker-api", "~> 1.20" spec.add_development_dependency "fakefs" spec.add_development_dependency "fluent-logger" end itamae-1.9.10/CHANGELOG.md0000644000175000017500000003750213017534453013741 0ustar scottscott## v1.9.10 Features - [Add depth attribute to git resource](https://github.com/itamae-kitchen/itamae/pull/219) - [Support force link a direcotory](https://github.com/itamae-kitchen/itamae/pull/229) - [Add support password authentication for ssh](https://github.com/itamae-kitchen/itamae/pull/227) Bugfixes - [Run a resource subscribing a resource in child recipe](https://github.com/itamae-kitchen/itamae/pull/224) - [Change file owner first, then change file permissions](https://github.com/itamae-kitchen/itamae/pull/228) Improvements - [Dir.exists? is deprecated, use Dir.exist?](https://github.com/itamae-kitchen/itamae/pull/226) ## v1.9.9 Features - [`itamae ssh` now accepts `--ssh-config` option](https://github.com/itamae-kitchen/itamae/pull/211) - [Introduce `--login-shell` option](https://github.com/itamae-kitchen/itamae/pull/217) - [`gem_package` resource has `uninstall` action](https://github.com/itamae-kitchen/itamae/pull/216) Bugfixes - [`send_file` fails against docker backend](https://github.com/itamae-kitchen/itamae/pull/215) ## v1.9.8 Bugfixes - [edit action of file resource: Keep mtime if no change](https://github.com/itamae-kitchen/itamae/pull/212) ## v1.9.7 (pre) Bugfixes - [Mark a file as updated in dry-run mode (by @ryotarai)](https://github.com/itamae-kitchen/itamae/pull/208) - [Do not surround LF with the ANSI escape sequence (by @daic-h)](https://github.com/itamae-kitchen/itamae/pull/209) ## v1.9.6 Features - [Introduce `--detailed-exitcode` option. (by @ryotarai)](https://github.com/itamae-kitchen/itamae/pull/206) Bugfixes - [If `git rev-list` fails, do `git fetch origin` (by @ryotarai)](https://github.com/itamae-kitchen/itamae/pull/205) - [If gid passed to user resource is a String, treat it as group name. (by @ryotarai)](https://github.com/itamae-kitchen/itamae/pull/207) ## v1.9.5 Bugfixes - [Set mode and owner correctly when file not changed (by @sorah)](https://github.com/itamae-kitchen/itamae/commit/438d79e4ef714f637f8f1cb50b01293e5232340a) - [Make tempfile unreadable to secure (by @sorah)](https://github.com/itamae-kitchen/itamae/commit/7af1d29fc020e57f3587aace728fbb40e35669cf) - [Accept any objects as a log message (by @abicky)](https://github.com/itamae-kitchen/itamae/pull/195) ## v1.9.4 Bugfixes - [Fix a bug that displays inappropriate diff in file deletion (by @takus)](https://github.com/itamae-kitchen/itamae/pull/200) - [Show diff on edit action of file resource in dry-run mode. (by @ryotarai)](https://github.com/itamae-kitchen/itamae/pull/197) - [Stop to call `chown --reference` and `chmod --reference` (by @yuichiro-naito)](https://github.com/itamae-kitchen/itamae/pull/193) ## v1.9.3 Improvements - [Support redirect on http_request resource (by @hico-horiuchi)](https://github.com/itamae-kitchen/itamae/pull/190) - [Use /bin/bash as default shell if shell is not set (by @hico-horiuchi)](https://github.com/itamae-kitchen/itamae/pull/192) - [Stop replacing files which are not updated (by @KitaitiMakoto)](https://github.com/itamae-kitchen/itamae/pull/194) ## v1.9.2 Features - [New option: `options` for `gem_package` resource (by @hico-horiuchi)](https://github.com/itamae-kitchen/itamae/pull/186) Improvements - [Execute `vagrant ssh-config` under `Bundler.with_clean_env` (by @hfm)](https://github.com/itamae-kitchen/itamae/pull/188) - [Specify type of `recursive` option for `git` resource and `force` option for `link` resource (by @k0kubun)](https://github.com/itamae-kitchen/itamae/pull/189) ## v1.9.1 Features - [Add `get`, `post`, `put`, `delete` and `options` actions to `http_request` resource (by @hico-horiuchi)](https://github.com/itamae-kitchen/itamae/pull/184) ## v1.9.0 Features - [New resource: `http_request` resource (by @hico-horiuchi)](https://github.com/itamae-kitchen/itamae/pull/180) - [Introduce Handler which handles events from Itamae (by @ryotarai)](https://github.com/itamae-kitchen/itamae/pull/181) - Compatibility can be broken because this is experimental feature Improvements - [Optimize `git` resource for fixed revision (by @k0kubun)](https://github.com/itamae-kitchen/itamae/pull/182) - Rename `--dot` option to `--recipe-graph` option. (by @ryotarai) - Compatibility can be broken because this is experimental feature ## v1.8.0 Features - [`generate` and `destroy` subcommands to manipulate cookbooks and roles (by @k0kubun)](https://github.com/itamae-kitchen/itamae/pull/176) Improvements - [Fallback to autoload resource plugin (by @k0kubun)](https://github.com/itamae-kitchen/itamae/pull/179) ## v1.7.0 No change ## v1.7.0.pre Features - `--profile` option (by @ryotarai) - `--profile PATH` saves executed commands to `PATH` in JSON format - Compatibility can be broken because this is experimental feature Bugfixes - [Suppress errors of `edit` action of `file` resource when the target file doesn't exist in `dry-run` mode (by @ryotarai)](https://github.com/itamae-kitchen/itamae/pull/144) ## v1.6.3 Features - [New command: `itamae init` which creates files and directories following the best practices (by @hmadison)](https://github.com/itamae-kitchen/itamae/pull/172) ## v1.6.2 Bugfixes - [Treat recipe name, arg of `include_recipe`, including `::` twice or more properly (by @sue445)](https://github.com/itamae-kitchen/itamae/pull/171) ## v1.6.1 Bugfixes - [Send a notification from `edit` action of `file` resource properly (by @kurochan)](https://github.com/itamae-kitchen/itamae/pull/169) ## v1.6.0 Improvements - [Ignore `--node-yaml` when the result is false (by @k0kubun)](https://github.com/itamae-kitchen/itamae/pull/165) - [Allow `include_recipe` to omit `.rb` extension (by @k0kubun)](https://github.com/itamae-kitchen/itamae/pull/166) - This is backward-compatible change - [Allow `load_recipes` to load plugin recipes directly (by @k0kubun)](https://github.com/itamae-kitchen/itamae/pull/167) ## v1.5.2 Improvements - [`include_recipe 'plugin_name'` loads `itamae/plugin/recipe/plugin_name/default.rb` too (by @ryotarai)](https://github.com/itamae-kitchen/itamae/pull/162) ## v1.5.1 Improvements - [Logger can be injected one which doesn't have `color` method. (by @ryotarai)](https://github.com/itamae-kitchen/itamae/commit/7c50f376f69029836047f26ab0a46b41b928c0d3) ## v1.5.0 Improvements - [Make a logger injectable from outside of Itamae. (by @ryotarai)](https://github.com/itamae-kitchen/itamae/pull/160) ## v1.4.5 Improvements - [Load `default.rb` if `include_recipe` is called with directory path. (by @Rudolph-Miller)](https://github.com/itamae-kitchen/itamae/pull/156) ## v1.4.4 Features - `--shell` option for `local`, `ssh` and `docker` subcommands. If it is set, it will be used instead of /bin/sh (by @ryotarai) ## v1.4.3 Bugfixes - [Restore original attributes of a resource after each action. (by @ryotarai)](https://github.com/itamae-kitchen/itamae/commit/28d33da3cb67c6a7635e47845b0055cb17df53a8) ## v1.4.2 Improvements - [Load plugin gems that is not managed by bundler. (by @KitaitiMakoto)](https://github.com/itamae-kitchen/itamae/pull/151) ## v1.4.1 Improvements - [`gem_binary` of `gem_package` resource accepts an Array too. (by @eagletmt)](https://github.com/itamae-kitchen/itamae/pull/149) - [`git` resource executes `git clone` if the destination directory is empty (by @tacahilo)](https://github.com/itamae-kitchen/itamae/pull/150) ## v1.4.0 Improvements - Make `cwd` a common attribute. (idea by @tacahilo) - It was an attribute for execute resource - When `user` attribute is set, change directory to the user's home directory. (idea by @tacahilo) - even if cd command fail, it will be ignored - directory specified by cwd will take precedence over this ## v1.3.6 Bugfixes - `create` action of `file` resource without `content` attribute changes mode and owner without touching the content of the file (by @ryotarai) ## v1.3.5 Improvements - [`create` action of `file` resource without `content` attribute changes mode and owner without touching the content of the file (by @ryotarai)](https://github.com/itamae-kitchen/itamae/compare/itamae-kitchen:d4a0abc...itamae-kitchen:3eae144) Bugfixes - [Edit action of file resource should set owner and mode if specified (by @eagletmt)](https://github.com/itamae-kitchen/itamae/pull/143) ## v1.3.4 Improvements - [Output stdout/err logs during command execution (by @ryotarai)](https://github.com/itamae-kitchen/itamae/commit/24f140dd9744f30c645422959a6a72b6e31eacc4) ## v1.3.3 Improvements - [Add `container` option to `docker` subcommand (by @marcy-terui)](https://github.com/itamae-kitchen/itamae/pull/142) ## v1.3.2 Features - [Add `recursive` attribute to `git` resource (by @mmasaki)](https://github.com/itamae-kitchen/itamae/pull/140) ## v1.3.1 Features - [Add `delete` action to `directory` resource (by @rrreeeyyy)](https://github.com/itamae-kitchen/itamae/pull/139) ## v1.3.0 Improvements - Update `HOME` environment variable when `user` attribute is specified. (incompatible change) (by @ryotarai) ## v1.2.21 Improvements - [Show error message when specified action is unavailable in dry_run mode (by @eagletmt)](https://github.com/itamae-kitchen/itamae/pull/137) - [Fix deprecation warnings in unit tests (by @eagletmt)](https://github.com/itamae-kitchen/itamae/pull/138) ## v1.2.20 Improvements - [Wrap host inventory value with Hashie::Mash to access it by a method call (by @ryotarai)](https://github.com/itamae-kitchen/itamae/pull/135) ## v1.2.19 Features - [`create_home` attribute of user resource (by @xibbar)](https://github.com/itamae-kitchen/itamae/pull/131) ## v1.2.18 Features - `run_command` method in a recipe, definition and resource (by @ryotarai) ## v1.2.17 Features - [Support provider for service resource (by @sonots)](https://github.com/itamae-kitchen/itamae/pull/134) ## v1.2.16 Improvements - [`force` option for `link` resource (by @mikeda)](https://github.com/itamae-kitchen/itamae/pull/128) ## v1.2.15 Bugfixes - [Fix --no-sudo to work properly (by @evalphobia)](https://github.com/itamae-kitchen/itamae/pull/126) - [Fix a glitch on raising exception when source doesn't exist (by @mozamimy)](https://github.com/itamae-kitchen/itamae/pull/125) ## v1.2.14 Features - "edit" action of "file" resource (by @ryotarai) ## v1.2.13 Features - [Add "shell" attribute to user resource (by @toritori0318)](https://github.com/itamae-kitchen/itamae/pull/120) ## v1.2.12 Bugfixes - Run delayed notifications created by a delayed notification. (by @ryotarai) - Set updated false after executing resources. (by @ryotarai) ## v1.2.11 Bugfixes - [Show difference of user resource when it is created. by @gongo](https://github.com/itamae-kitchen/itamae/pull/118) ## v1.2.10 Bugfixes - [Use given attribute value even if it's falsey (by @sorah)](https://github.com/itamae-kitchen/itamae/pull/117) ## v1.2.9 Bugfixes - Do not use local variable named `variables`. (by @ryotarai) If `variables` is used as local variable's name, the following causes a syntax error. ``` template "..." do variables foo: bar # variables(foo: bar) # This never cause a syntax error end ``` See also: https://bugs.ruby-lang.org/issues/11016 ## v1.2.8 Improvements - [Load ~/.ssh/config (by @maruware)](https://github.com/itamae-kitchen/itamae/pull/115) ## v1.2.7 Bugfixes - Backend::Docker#finalize should be public. (by @mizzy) ## v1.2.6 - Remove code for debugging... (by @ryotarai) ## v1.2.5 Bugfixes - Bugs in definition feature. (by @ryotarai) ## v1.2.4 Improvements - Use specinfra/core instead of specinfra. (by @ryotarai) ## v1.2.3 Bugfixes - Bugs in Node class (by @ryotarai) ## v1.2.2 Improvements - Refactor Backend and Runner class for multi backends. (by @ryotarai) ## v1.2.1 (yanked) ## v1.2.0 Feature - Docker backend (by @ryotarai) - This backend builds a Docker image. - Usage: `itamae docker --image baseimage recipe.rb` - NOTE: This feature is experimental. - Compatibility can be broken because this is experimental feature ## v1.1.26 Bugfix - Always outdent. (by @ryotarai) ## v1.1.25 Improvements - Make logging less verbose by default. (by @eagletmt) - Change indent width from 3 to 2. (by @ryotarai) ## v1.1.24 Bugfixes - Make `node` accessible from define block. (by @ryotarai) ## v1.1.23 Feature - Validate node attributes by `Node#validate!` (by @ryotarai) ## v1.1.22 Improvements - `source :auto` accepts a template without .erb extention. (by @ryotarai) ## v1.1.21 Bugfixes - Ignore CommandExecutionError during listing installed gems. (by @eagletmt) - because `gem` command may not be installed in dry-run mode ## v1.1.20 Features - `source :auto` of remote_file and template resources. (by @ryotarai) - details: https://github.com/itamae-kitchen/itamae/issues/94 ## v1.1.19 Features - `verify` attribute - command will be executed after running resource action. (by @ryotarai) - If it fails, Itamae will abort (notifications will not be executed) Improvements - [`--vagrant` option without `--host` assumes the VM name `default` (by @muratayusuke)](https://github.com/itamae-kitchen/itamae/pull/91) - `delayed` is a valid notification timing. (by @ryotarai) - same as Chef - If invalid notification timing is provided, an error will be raised. (by @ryotarai) ## v1.1.18 Improvements - [Add remove action to package resource (by @eagletmt)](https://github.com/itamae-kitchen/itamae/pull/92) - Colorize diff output of file resource (by @ryotarai) - removed lines in red - inserted lines in green ## v1.1.17 Bugfixes - Do not remove space char in output of diff. (by @ryotarai) ## v1.1.16 Features - `source` attribute of `gem_package` resource. (by @ryotarai) ## v1.1.15 Features - Implement `gem_package` resource. (by @ryotarai) ## v1.1.14 Improvements - Start a service only if the service is not running. (by @ryotarai) - Stop a service only if the service is running. (by @ryotarai) ## v1.1.13 Improvements - [Set executed attr of execute resource for logging purpose. (by @ryotarai)](https://github.com/itamae-kitchen/itamae/pull/86) - [Colorize diff output of file resource green. (by @ryotarai)](https://github.com/itamae-kitchen/itamae/pull/87) ## v1.1.12 Bugfixes - [Update home directory of user resource if changed. (by @ryotarai)](https://github.com/itamae-kitchen/itamae/commit/0b5ad5245af8a7849d36d0598f06b7adb9ac025a) ## v1.1.11 Bugfixes - [Do not include recipes which are already included. (by @ryotarai)](https://github.com/itamae-kitchen/itamae/pull/85) - This may break backward compatibility. ## v1.1.10 Feature - `--dot` option to write dependency graph of recipes - Compatibility can be broken because this is experimental feature ## v1.1.9 Improvements - Show template file path when rendering the template fails. (by @ryotarai) ## v1.1.8 Improvements - [Show differences in green (by @mizzy)](https://github.com/itamae-kitchen/itamae/pull/82) ## v1.1.7 Bugfixes - Fix a typo bug (by @ryotarai) ## v1.1.6 (yanked) Improvements - [Normalize mode value of file resource by prepending '0' (by @sorah)](https://github.com/itamae-kitchen/itamae/pull/76) Bugfixes - [Fix a problem that occurs when the current value is false. (by @mizzy)](https://github.com/itamae-kitchen/itamae/pull/75) ## v1.1.5 Bugfixes - Clear current attributes before each action. (by @ryotarai) - Turn on updated-flag after each action. (by @ryotarai) ## v1.1.4 Bugfixes - `Node#[]` with unknown key returns nil. (by @nownabe) - https://github.com/itamae-kitchen/itamae/pull/71 ## v1.1.3 Features - `group` resource (Thanks to @a2ikm) - https://github.com/itamae-kitchen/itamae/pull/70 ## v1.1.2 Features - `user` resource accepts group name (String) as its `gid`. (by @ryotarai) ## v1.1.1 Features - New resource `remote_directory` which transfers a directory from local to remote like `remote_file` resource. (by @k0kubun) - https://github.com/itamae-kitchen/itamae/pull/66 ## v1.1.0 Incompatible changes - [`uid` and `gid` attributes of `user` resource accept only Integer. (by @eagletmt)](https://github.com/itamae-kitchen/itamae/pull/65) itamae-1.9.10/spec/0000755000175000017500000000000013017534453013053 5ustar scottscottitamae-1.9.10/spec/integration/0000755000175000017500000000000013017534453015376 5ustar scottscottitamae-1.9.10/spec/integration/Vagrantfile0000644000175000017500000000262713017534453017572 0ustar scottscott# -*- mode: ruby -*- # vi: set ft=ruby : require 'vagrant-digitalocean' # Vagrantfile API/syntax version. Don't touch unless you know what you're doing! VAGRANTFILE_API_VERSION = "2" Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| config.vm.define :trusty do |c| c.vm.hostname = 'itamae-trusty' c.vm.hostname += "-#{ENV['WERCKER_BUILD_ID']}" if ENV['WERCKER_BUILD_ID'] c.vm.provider :virtualbox do |provider, override| override.vm.box = "ubuntu/trusty64" override.vm.provision :shell, inline: <<-EOC cat /etc/apt/sources.list | sed -e 's|http://[^ ]*|mirror://mirrors.ubuntu.com/mirrors.txt|g' > /tmp/sources.list if !(diff -q /etc/apt/sources.list /tmp/sources.list); then mv /tmp/sources.list /etc/apt/sources.list apt-get update fi echo America/New_York > /etc/timezone dpkg-reconfigure --frontend noninteractive tzdata EOC end c.vm.provider :digital_ocean do |provider, override| override.ssh.private_key_path = '~/.ssh/id_rsa.vagrant' override.vm.box = 'digital_ocean' override.vm.box_url = "https://github.com/smdahlen/vagrant-digitalocean/raw/master/box/digital_ocean.box" provider.ssh_key_name = ENV['WERCKER'] ? 'vagrant/wercker/itamae' : 'Vagrant' provider.token = ENV['DIGITALOCEAN_TOKEN'] provider.image = 'ubuntu-14-04-x64' # ubuntu provider.region = 'nyc3' provider.size = '512mb' end end end itamae-1.9.10/spec/integration/recipes/0000755000175000017500000000000013017534453017030 5ustar scottscottitamae-1.9.10/spec/integration/recipes/default2.rb0000644000175000017500000000015513017534453021064 0ustar scottscottfile "put file in default2.rb" do action :nothing path "/tmp/created_in_default2" content 'Hello' end itamae-1.9.10/spec/integration/recipes/redefine.rb0000644000175000017500000000065013017534453021137 0ustar scottscott# spec/integration/recipes/redefine.rb define :echo_hello, version: nil do file "put file in redefine.rb" do action :create path "/tmp/created_in_redefine" content 'first' end end # Duplicated definitions define :echo_hello, version: nil do file "put file in redefine.rb" do action :create path "/tmp/created_in_redefine" content 'second' end end # Execute echo_hello "execute echo_hello!" itamae-1.9.10/spec/integration/recipes/node.json0000644000175000017500000000003213017534453020643 0ustar scottscott{ "greeting": "Hello" } itamae-1.9.10/spec/integration/recipes/default.rb0000644000175000017500000002365413017534453021013 0ustar scottscottnode.reverse_merge!({ message: "Hello, Itamae" }) execute 'apt-get update' execute 'deluser --remove-home itamae2' do only_if "id itamae2" end include_recipe "./included.rb" include_recipe "./included.rb" # including the same recipe is expected to be skipped. user "create itamae user" do uid 123 username "itamae" password "$1$ltOY8bZv$iZ57f1KAp8jwKViNm3pze." home '/home/foo' shell '/bin/sh' end user "update itamae user" do uid 1234 username "itamae" password "$1$TQz9gPMl$nHYrsA5W2ZdZ0Yn021BQH1" home '/home/itamae' shell '/bin/dash' end directory "/home/itamae" do mode "755" owner "itamae" group "itamae" end user "create itamae2 user with create home directory" do username "itamae2" create_home true home "/home/itamae2" shell "/bin/sh" end ###### package 'dstat' do action :install end package 'sl' do version '3.03-17' end package 'resolvconf' do action :remove end ###### package "ruby" gem_package 'tzinfo' do version '1.1.0' end gem_package 'tzinfo' do version '1.2.2' end gem_package 'bundler' do options ['--no-ri', '--no-rdoc'] end gem_package 'rake' do version '11.1.0' end gem_package 'rake' do version '11.2.2' end gem_package 'rake' do action :uninstall version '11.2.2' end gem_package 'test-unit' do version '3.2.0' end gem_package 'test-unit' do version '3.1.9' end gem_package 'test-unit' do action :uninstall end ###### execute "echo -n > /tmp/notifies" execute "echo -n 1 >> /tmp/notifies" do action :nothing end execute "echo -n 2 >> /tmp/notifies" do notifies :run, "execute[echo -n 1 >> /tmp/notifies]" end execute "echo -n 3 >> /tmp/notifies" do action :nothing end execute "echo -n 4 >> /tmp/notifies" do notifies :run, "execute[echo -n 3 >> /tmp/notifies]", :immediately end ###### execute "echo -n > /tmp/subscribes" execute "echo -n 1 >> /tmp/subscribes" do action :nothing subscribes :run, "execute[echo -n 2 >> /tmp/subscribes]" end execute "echo -n 2 >> /tmp/subscribes" execute "echo -n 3 >> /tmp/subscribes" do action :nothing subscribes :run, "execute[echo -n 4 >> /tmp/subscribes]", :immediately end execute "echo -n 4 >> /tmp/subscribes" ###### remote_file "/tmp/remote_file" do source "hello.txt" end remote_file "/tmp/remote_file_auto" do source :auto end ###### directory "/tmp/directory" do mode "700" owner "itamae" group "itamae" end directory "/tmp/directory_never_exist1" do action :create end directory "/tmp/directory_never_exist1" do action :delete end template "/tmp/template" do source "hello.erb" variables goodbye: "Good bye" end template "/tmp/template_auto" do source :auto variables goodbye: "Good bye" end file "/tmp/file" do content "Hello World" mode "777" end file "/tmp/file_with_suid" do content "Hello World" mode "4755" owner "itamae" group "itamae" end execute "echo 'Hello Execute' > /tmp/execute" file "/tmp/never_exist1" do only_if "exit 1" end file "/tmp/never_exist2" do not_if "exit 0" end ###### http_request "/tmp/http_request.html" do url "https://httpbin.org/get?from=itamae" end http_request "/tmp/http_request_delete.html" do action :delete url "https://httpbin.org/delete?from=itamae" end http_request "/tmp/http_request_post.html" do action :post message "love=sushi" url "https://httpbin.org/post?from=itamae" end http_request "/tmp/http_request_put.html" do action :put message "love=sushi" url "https://httpbin.org/put?from=itamae" end http_request "/tmp/http_request_headers.html" do headers "User-Agent" => "Itamae" url "https://httpbin.org/get" end http_request "/tmp/http_request_redirect.html" do redirect_limit 1 url "https://httpbin.org/redirect-to?url=https%3A%2F%2Fhttpbin.org%2Fget%3Ffrom%3Ditamae" end ###### service "cron" do action :stop end execute "ps -C cron > /tmp/cron_stopped; true" service "cron" do action :start end execute "ps -C cron > /tmp/cron_running; true" ###### package "nginx" do options "--force-yes" end service "nginx" do action [:enable, :start] end execute "test -f /etc/rc3.d/S20nginx" # test execute "test $(ps h -C nginx | wc -l) -gt 0" # test service "nginx" do action [:disable, :stop] end execute "test ! -f /etc/rc3.d/S20nginx" # test execute "test $(ps h -C nginx | wc -l) -eq 0" # test ###### link "/tmp-link" do to "/tmp" end execute "touch /tmp-link-force" link "/tmp-link-force" do to "/tmp" force true end ###### execute "mkdir /tmp/link-force-no-dereference1" link "link-force-no-dereference" do cwd "/tmp" to "link-force-no-dereference1" force true end execute "mkdir /tmp/link-force-no-dereference2" link "link-force-no-dereference" do cwd "/tmp" to "link-force-no-dereference2" force true end ##### local_ruby_block "greeting" do block do Itamae.logger.info "板前" end end ##### package "git" git "/tmp/git_repo" do repository "https://github.com/ryotarai/infrataster.git" revision "v0.1.0" end git "/tmp/git_repo_submodule" do repository "https://github.com/mmasaki/fake_repo_including_submodule.git" recursive true end git "/tmp/git_repo_depth_1" do repository "https://github.com/ryotarai/infrataster.git" depth 1 end ##### execute "echo -n \"$HOME\n$(pwd)\" > /tmp/created_by_itamae_user" do user "itamae" end ##### execute "echo 'notify to resource in default2.rb'" do notifies :create, "file[put file in default2.rb]" end ##### file "/tmp/never_exist3" do action :create end file "/tmp/never_exist3" do action :delete end ##### include_recipe "define/default.rb" definition_example "name" do key 'value' end ##### file "/tmp/never_exist4" do action :nothing end file "/tmp/file1" do content "Hello, World" end file "/tmp/file1" do content "Hello, World" notifies :create, "file[/tmp/never_exist4]" end ##### file "/tmp/file_create_without_content" do content "Hello, World" end file "/tmp/file_create_without_content" do owner "itamae" group "itamae" mode "600" end ##### execute 'true' do verify 'true' end ##### execute 'echo 1 > /tmp/multi_delayed_notifies' do notifies :run, "execute[echo 2 >> /tmp/multi_delayed_notifies]" end execute 'echo 2 >> /tmp/multi_delayed_notifies' do action :nothing notifies :run, "execute[echo 3 >> /tmp/multi_delayed_notifies]" end execute 'echo 3 >> /tmp/multi_delayed_notifies' do action :nothing notifies :run, "execute[echo 4 >> /tmp/multi_delayed_notifies]" end execute 'echo 4 >> /tmp/multi_delayed_notifies' do action :nothing end ##### execute 'echo 1 > /tmp/multi_immediately_notifies' do notifies :run, "execute[echo 2 >> /tmp/multi_immediately_notifies]", :immediately end execute 'echo 2 >> /tmp/multi_immediately_notifies' do action :nothing notifies :run, "execute[echo 3 >> /tmp/multi_immediately_notifies]", :immediately end execute 'echo 3 >> /tmp/multi_immediately_notifies' do action :nothing notifies :run, "execute[echo 4 >> /tmp/multi_immediately_notifies]", :immediately end execute 'echo 4 >> /tmp/multi_immediately_notifies' do action :nothing end ##### execute 'echo -n 1 > /tmp/file_edit_notifies' do action :nothing end file '/tmp/file_edit_sample' do content 'Hello, world' owner 'itamae' group 'itamae' mode '444' end file '/tmp/file_edit_sample' do action :edit owner 'itamae2' group 'itamae2' mode '400' block do |content| content.gsub!('world', 'Itamae') end notifies :run, "execute[echo -n 1 > /tmp/file_edit_notifies]" end file '/tmp/file_edit_with_suid' do content 'Hello, world' owner 'itamae' group 'itamae' mode '600' end file '/tmp/file_edit_with_suid' do action :edit owner 'itamae2' group 'itamae2' mode '4755' end file '/tmp/file_edit_keeping_mode_owner' do content 'Hello, world' owner 'itamae' group 'itamae' mode '444' end file '/tmp/file_edit_keeping_mode_owner' do action :edit block do |content| content.gsub!('world', 'Itamae') end end ### execute "f=/tmp/file_edit_with_content_change_updates_timestamp && echo 'Hello, world' > $f && touch -d 2016-05-02T01:23:45Z $f" file "/tmp/file_edit_with_content_change_updates_timestamp" do action :edit block do |content| content.gsub!('world', 'Itamae') end end ### execute "touch -d 2016-05-02T12:34:56Z /tmp/file_edit_without_content_change_keeping_timestamp" file "/tmp/file_edit_without_content_change_keeping_timestamp" do action :edit block do |content| # no change end end ### file '/tmp/file_without_content_change_updates_mode_and_owner' do action :create content 'Hello, world' owner 'itamae' group 'itamae' mode '444' end file '/tmp/file_without_content_change_updates_mode_and_owner' do action :create content 'Hello, world' # no change owner 'itamae2' group 'itamae2' mode '666' end ### execute "touch -d 2016-05-01T01:23:45Z /tmp/file_with_content_change_updates_timestamp" file "/tmp/file_with_content_change_updates_timestamp" do content "Hello, world" end ### execute "f=/tmp/file_without_content_change_keeping_timestamp && echo 'Hello, world' > $f && touch -d 2016-05-01T12:34:56Z $f" file "/tmp/file_without_content_change_keeping_timestamp" do content "Hello, world\n" end ### unless run_command("echo -n Hello").stdout == "Hello" raise "run_command in a recipe failed" end define :run_command_in_definition do unless run_command("echo -n Hello").stdout == "Hello" raise "run_command in a definition failed" end end execute "echo Hello" do unless run_command("echo -n Hello").stdout == "Hello" raise "run_command in a resource failed" end end local_ruby_block 'execute run_command' do block do unless run_command("echo -n Hello").stdout == "Hello" raise "run_command in local_ruby_block failed" end end end execute "touch /tmp/subscribed_from_parent" do action :nothing subscribes :run, 'execute[subscribed from parent]' end ### v1 = node.memory.total v2 = node[:memory][:total] v3 = node['memory']['total'] unless v1 == v2 && v2 == v3 && v1 =~ /\A\d+kB\z/ raise "failed to fetch host inventory value (#{v1}, #{v2}, #{v3})" end itamae-1.9.10/spec/integration/recipes/templates/0000755000175000017500000000000013017534453021026 5ustar scottscottitamae-1.9.10/spec/integration/recipes/templates/template_auto.erb0000644000175000017500000000021513017534453024361 0ustar scottscott<%= node['greeting'] %> <%= @goodbye %> total memory: <%= node['memory']['total'] %> uninitialized node key: <%= node['un-initialized'] %> itamae-1.9.10/spec/integration/recipes/dry_run.rb0000644000175000017500000000015713017534453021042 0ustar scottscottfile "/tmp/it_does_not_exist" do action :edit block do |content| content.gsub!("foo", "bar") end end itamae-1.9.10/spec/integration/recipes/hello.txt0000644000175000017500000000001513017534453020670 0ustar scottscottHello Itamae itamae-1.9.10/spec/integration/recipes/files/0000755000175000017500000000000013017534453020132 5ustar scottscottitamae-1.9.10/spec/integration/recipes/files/remote_file_auto0000644000175000017500000000001513017534453023373 0ustar scottscottHello Itamae itamae-1.9.10/spec/integration/recipes/hello.erb0000644000175000017500000000021513017534453020623 0ustar scottscott<%= node['greeting'] %> <%= @goodbye %> total memory: <%= node['memory']['total'] %> uninitialized node key: <%= node['un-initialized'] %> itamae-1.9.10/spec/integration/recipes/define/0000755000175000017500000000000013017534453020262 5ustar scottscottitamae-1.9.10/spec/integration/recipes/define/default.rb0000644000175000017500000000032713017534453022235 0ustar scottscottdefine :definition_example, key: 'default' do execute "echo 'name:#{params[:name]},key:#{params[:key]},message:#{node[:message]}' > /tmp/created_by_definition" remote_file "/tmp/remote_file_in_definition" end itamae-1.9.10/spec/integration/recipes/define/files/0000755000175000017500000000000013017534453021364 5ustar scottscottitamae-1.9.10/spec/integration/recipes/define/files/remote_file_in_definition0000644000175000017500000000002313017534453026472 0ustar scottscottdefinition_example itamae-1.9.10/spec/integration/recipes/included.rb0000644000175000017500000000053113017534453021143 0ustar scottscottrequire 'pathname' included_flag_file = Pathname.new("/tmp/included_rb_is_included") if included_flag_file.exist? && included_flag_file.read == $$.to_s raise "included.rb should not be included twice." else included_flag_file.write($$.to_s) end execute "touch /tmp/included_recipe" execute "subscribed from parent" do command "true" end itamae-1.9.10/spec/integration/default_spec.rb0000644000175000017500000001632413017534453020367 0ustar scottscottrequire 'spec_helper' describe user("itamae") do it { should exist } it { should have_uid 1234 } it { should have_home_directory '/home/itamae' } it { should have_login_shell '/bin/dash' } end describe file('/tmp/included_recipe') do it { should be_file } end describe package('dstat') do it { should be_installed } end describe package('sl') do it { should be_installed } end describe package('resolvconf') do it { should_not be_installed } end %w!/tmp/remote_file /tmp/remote_file_auto!.each do |f| describe file(f) do it { should be_file } its(:content) { should match(/Hello Itamae/) } end end describe file('/tmp/directory') do it { should be_directory } it { should be_mode 700 } it { should be_owned_by "itamae" } it { should be_grouped_into "itamae" } end describe file('/tmp/directory_never_exist1') do it { should_not be_directory } end %w!/tmp/template /tmp/template_auto!.each do |f| describe file(f) do it { should be_file } its(:content) { should match(/Hello/) } its(:content) { should match(/Good bye/) } its(:content) { should match(/^total memory: \d+kB$/) } its(:content) { should match(/^uninitialized node key: $/) } end end describe file('/tmp/file') do it { should be_file } its(:content) { should match(/Hello World/) } it { should be_mode 777 } end describe file('/tmp/file_with_suid') do it { should be_file } it { should be_mode 4755 } it { should be_owned_by "itamae" } it { should be_grouped_into "itamae" } end describe file('/tmp/execute') do it { should be_file } its(:content) { should match(/Hello Execute/) } end describe file('/tmp/never_exist1') do it { should_not be_file } end describe file('/tmp/never_exist2') do it { should_not be_file } end describe file('/tmp/http_request.html') do it { should be_file } its(:content) { should match(/"from": "itamae"/) } end describe file('/tmp/http_request_delete.html') do it { should be_file } its(:content) { should match(/"from": "itamae"/) } end describe file('/tmp/http_request_post.html') do it { should be_file } its(:content) do should match(/"from": "itamae"/) should match(/"love": "sushi"/) end end describe file('/tmp/http_request_put.html') do it { should be_file } its(:content) do should match(/"from": "itamae"/) should match(/"love": "sushi"/) end end describe file('/tmp/http_request_headers.html') do it { should be_file } its(:content) { should match(/"User-Agent": "Itamae"/) } end describe file('/tmp/http_request_redirect.html') do it { should be_file } its(:content) { should match(/"from": "itamae"/) } end describe file('/tmp/notifies') do it { should be_file } its(:content) { should eq("2431") } end describe file('/tmp/subscribes') do it { should be_file } its(:content) { should eq("2431") } end describe file('/tmp/cron_stopped') do it { should be_file } its(:content) do expect(subject.content.lines.size).to eq 1 end end describe file('/tmp/cron_running') do it { should be_file } its(:content) do expect(subject.content.lines.size).to eq 2 end end describe file('/tmp-link') do it { should be_linked_to '/tmp' } its(:content) do expect(subject.content.lines.size).to eq 0 end end describe file('/tmp-link-force') do it { should be_linked_to '/tmp' } end describe file('/tmp/link-force-no-dereference') do it { should be_linked_to 'link-force-no-dereference2' } end describe file('/tmp/link-force-no-dereference/link-force-no-dereference2') do it { should_not exist } end describe command('cd /tmp/git_repo && git rev-parse HEAD') do its(:stdout) { should match(/3116e170b89dc0f7315b69c1c1e1fd7fab23ac0d/) } end describe command('cd /tmp/git_repo_submodule/empty_repo && cat README.md') do its(:stdout) { should match(/Empty Repo/) } end describe command('cd /tmp/git_repo_depth_1 && git rev-list --count HEAD') do its(:stdout) { should eq "1\n" } end describe file('/tmp/created_by_itamae_user') do it { should be_file } it { should be_owned_by 'itamae' } its(:content) { should eq("/home/itamae\n/home/itamae") } end describe file('/tmp/created_in_default2') do it { should be_file } end describe file('/tmp/never_exist3') do it { should_not be_file } end describe file('/tmp/never_exist4') do it { should_not be_file } end describe file('/tmp/created_in_redefine') do it { should be_file } its(:content) { should match(/first/) } end describe command('gem list') do its(:stdout) { should include('tzinfo (1.2.2, 1.1.0)') } end describe command('gem list') do its(:stdout) { should include('rake (11.1.0)') } end describe command('gem list') do its(:stdout) { should_not include('test-unit') } end describe command('ri Bundler') do its(:stderr) { should eq("Nothing known about Bundler\n") } end describe file('/tmp/created_by_definition') do it { should be_file } its(:content) { should eq("name:name,key:value,message:Hello, Itamae\n") } end describe file('/tmp/remote_file_in_definition') do it { should be_file } its(:content) { should eq("definition_example\n") } end describe file('/tmp/multi_delayed_notifies') do it { should be_file } its(:content) { should eq("1\n2\n3\n4\n") } end describe file('/tmp/multi_immediately_notifies') do it { should be_file } its(:content) { should eq("1\n2\n3\n4\n") } end describe file('/tmp/file_edit_sample') do it { should be_file } its(:content) { should eq("Hello, Itamae") } it { should be_mode 400 } it { should be_owned_by "itamae2" } it { should be_grouped_into "itamae2" } end describe file('/tmp/file_edit_with_suid') do it { should be_file } it { should be_mode 4755 } it { should be_owned_by "itamae2" } it { should be_grouped_into "itamae2" } end describe file('/tmp/file_edit_keeping_mode_owner') do it { should be_file } its(:content) { should eq("Hello, Itamae") } it { should be_mode 444 } it { should be_owned_by "itamae" } it { should be_grouped_into "itamae" } end describe file('/tmp/file_edit_with_content_change_updates_timestamp') do its(:mtime) { should be > DateTime.iso8601("2016-05-02T01:23:45Z") } end describe file('/tmp/file_edit_without_content_change_keeping_timestamp') do its(:mtime) { should eq(DateTime.iso8601("2016-05-02T12:34:56Z")) } end describe file('/home/itamae2') do it { should be_directory } it { should be_owned_by "itamae2" } it { should be_grouped_into "itamae2" } end describe file('/tmp/file_create_without_content') do its(:content) { should eq("Hello, World") } it { should be_mode 600 } it { should be_owned_by "itamae" } it { should be_grouped_into "itamae" } end describe file('/tmp/file_edit_notifies') do its(:content) { should eq("1") } end describe file('/tmp/file_without_content_change_updates_mode_and_owner') do its(:content) { should eq("Hello, world") } it { should be_mode 666 } it { should be_owned_by "itamae2" } it { should be_grouped_into "itamae2" } end describe file('/tmp/file_with_content_change_updates_timestamp') do its(:mtime) { should be > DateTime.iso8601("2016-05-01T01:23:45Z") } end describe file('/tmp/file_without_content_change_keeping_timestamp') do its(:mtime) { should eq(DateTime.iso8601("2016-05-01T12:34:56Z")) } end describe file('/tmp/subscribed_from_parent') do it { should be_file } end itamae-1.9.10/spec/integration/spec_helper.rb0000644000175000017500000000160413017534453020215 0ustar scottscottrequire 'serverspec' require 'net/ssh' require 'tempfile' set :backend, :ssh def vagrant(cmd) env = {"VAGRANT_CWD" => File.dirname(__FILE__)} system(env, "vagrant #{cmd}") end if ENV['ASK_SUDO_PASSWORD'] begin require 'highline/import' rescue LoadError fail "highline is not available. Try installing it." end set :sudo_password, ask("Enter sudo password: ") { |q| q.echo = false } else set :sudo_password, ENV['SUDO_PASSWORD'] end host = ENV['TARGET_HOST'] config = Tempfile.new('', Dir.tmpdir) vagrant "ssh-config #{host} > #{config.path}" options = Net::SSH::Config.for(host, [config.path]) options[:user] ||= Etc.getlogin set :host, options[:host_name] || host set :ssh_options, options # Disable sudo # set :disable_sudo, true # Set environment variables # set :env, :LANG => 'C', :LC_MESSAGES => 'C' # Set PATH # set :path, '/sbin:/usr/local/sbin:$PATH' itamae-1.9.10/spec/unit/0000755000175000017500000000000013017534453014032 5ustar scottscottitamae-1.9.10/spec/unit/spec_helper.rb0000644000175000017500000000150313017534453016647 0ustar scottscottrequire 'itamae' # This file was generated by the `rspec --init` command. Conventionally, all # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. # Require this file using `require "spec_helper"` to ensure that it is only # loaded once. # # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration RSpec.configure do |config| config.run_all_when_everything_filtered = true config.filter_run :focus # Run specs in random order to surface order dependencies. If you find an # order dependency and want to debug it, you can fix the order by providing # the seed, which is printed after each run. # --seed 1234 config.order = 'random' config.raise_errors_for_deprecations! end Itamae.logger = ::Logger.new(StringIO.new) Specinfra.configuration.error_on_missing_backend_type = false itamae-1.9.10/spec/unit/lib/0000755000175000017500000000000013017534453014600 5ustar scottscottitamae-1.9.10/spec/unit/lib/itamae/0000755000175000017500000000000013017534453016040 5ustar scottscottitamae-1.9.10/spec/unit/lib/itamae/recipe_spec.rb0000644000175000017500000000010413017534453020641 0ustar scottscottrequire 'spec_helper' module Itamae describe Recipe do end end itamae-1.9.10/spec/unit/lib/itamae/resource_spec.rb0000644000175000017500000000114113017534453021223 0ustar scottscottrequire 'spec_helper' module Itamae describe Resource do describe ".parse_description" do context "with valid description" do it "returns type and name" do expect(described_class.parse_description("this-is_type[this-is_name]")). to eq(["this-is_type", "this-is_name"]) end end context "with invalid description" do it "raises an error" do expect do described_class.parse_description("[this-is_type][this-is_name]") end.to raise_error(Itamae::Resource::ParseError) end end end end end itamae-1.9.10/spec/unit/lib/itamae/logger_spec.rb0000644000175000017500000000213513017534453020657 0ustar scottscottrequire 'spec_helper' module Itamae describe Logger do describe "#debug" do context "`msg` is a String" do it "indents the message" do expect_any_instance_of(::Logger).to receive(:debug).with(" msg") Itamae.logger.with_indent do Itamae.logger.debug("msg") end end end context "`msg` is an Exception" do let(:msg) { ::Exception.new("error") } before do allow(msg).to receive(:backtrace) { %w!frame1 frame2! } end it "indents the error message and the backtrace" do expect_any_instance_of(::Logger).to receive(:debug).with(<<-MSG.rstrip) error (Exception) frame1 frame2 MSG Itamae.logger.with_indent do Itamae.logger.debug(msg) end end end context "`msg` is an Array" do it "indents the message" do expect_any_instance_of(::Logger).to receive(:debug).with(" []") Itamae.logger.with_indent do Itamae.logger.debug([]) end end end end end end itamae-1.9.10/spec/unit/lib/itamae/handler/0000755000175000017500000000000013017534453017455 5ustar scottscottitamae-1.9.10/spec/unit/lib/itamae/handler/base_spec.rb0000644000175000017500000000144713017534453021734 0ustar scottscottrequire 'spec_helper' describe Itamae::Handler::Base do subject(:handler) { described_class.new({}) } context "when receiving recipe_started event" do it "stores the payload" do subject.event(:recipe_started, :payload) expect(subject.recipes).to eq([:payload]) end end context "when receiving recipe_completed event" do before do subject.event(:recipe_started, :payload) end it "pops the payload" do subject.event(:recipe_completed, :payload) expect(subject.recipes).to eq([]) end end context "when receiving recipe_failed event" do before do subject.event(:recipe_started, :payload) end it "pops the payload" do subject.event(:recipe_failed, :payload) expect(subject.recipes).to eq([]) end end end itamae-1.9.10/spec/unit/lib/itamae/handler/fluentd_spec.rb0000644000175000017500000000100113017534453022445 0ustar scottscottrequire 'spec_helper' require 'itamae/handler/fluentd' describe Itamae::Handler::Fluentd do subject(:handler) do described_class.new(options).tap do |h| h.fluent_logger = fluent_logger end end let(:options) { {'hostname' => 'me'} } let(:fluent_logger) { Fluent::Logger::TestLogger.new } describe '#event' do it 'posts a record to fluent logger' do subject.event(:type, {arg: 'value'}) expect(fluent_logger.queue).to eq([{arg: 'value', hostname: 'me'}]) end end end itamae-1.9.10/spec/unit/lib/itamae/backend_spec.rb0000644000175000017500000000715613017534453020777 0ustar scottscottrequire 'spec_helper' require 'fakefs/spec_helpers' module Itamae module Backend describe Base do include FakeFS::SpecHelpers class Klass < Itamae::Backend::Base def initialize(_, backend) @backend = backend end end let(:backend) { double('backend', send_file: nil, send_directory: nil) } let(:itamae_backend) { Klass.new('dummy', backend) } describe ".send_file" do context "the source file doesn't exist" do subject { -> { itamae_backend.send_file("src", "dst") } } it { expect(subject).to raise_error(Itamae::Backend::SourceNotExistError, "The file 'src' doesn't exist.") } end context "the source file exist, but it is not a regular file" do before { Dir.mkdir("src") } subject { -> { itamae_backend.send_file("src", "dst") } } it { expect(subject).to raise_error(Itamae::Backend::SourceNotExistError, "'src' is not a file.") } end context "the source file is a regular file" do before { FileUtils.touch("src") } subject { -> { itamae_backend.send_file("src", "dst") } } it { expect { subject }.not_to raise_error } end end describe ".send_directory" do context "the source directory doesn't exist" do subject { -> { itamae_backend.send_directory("src", "dst") } } it { expect(subject).to raise_error(Itamae::Backend::SourceNotExistError, "The directory 'src' doesn't exist.") } end context "the source directory exist, but it is not a directory" do before { FileUtils.touch("src") } subject { -> { itamae_backend.send_directory("src", "dst") } } it { expect(subject).to raise_error(Itamae::Backend::SourceNotExistError, "'src' is not a directory.") } end context "the source directory is a directory" do before { Dir.mkdir("src") } subject { -> { itamae_backend.send_directory("src", "dst") } } it { expect { subject }.not_to raise_error } end end end describe Ssh do describe "#ssh_options" do subject { ssh.send(:ssh_options) } let!(:ssh) { described_class.new(options) } let!(:host_name) { "example.com" } let!(:default_option) do opts = {} opts[:host_name] = nil opts.merge!(Net::SSH::Config.for(host_name)) opts[:user] = opts[:user] || Etc.getlogin opts end context "with host option" do let(:options) { {host: host_name} } it { is_expected.to eq( default_option.merge({host_name: host_name}) ) } end context "with ssh_config option" do around do |example| Tempfile.create("ssh_config") do |temp| temp.write(<_action method" do expect(subject).to receive(:action_name) subject.run end end end itamae-1.9.10/spec/unit/lib/itamae/handler_spec.rb0000644000175000017500000000034113017534453021012 0ustar scottscottrequire 'spec_helper' module Itamae describe Handler do describe ".from_type" do it "returns handler class" do expect(described_class.from_type('debug')).to eq(Handler::Debug) end end end end itamae-1.9.10/spec/unit/lib/itamae/runner_spec.rb0000644000175000017500000000137413017534453020715 0ustar scottscottrequire 'spec_helper' require 'tmpdir' module Itamae describe Runner do subject { described_class.new(double(:node)) } around do |example| Dir.mktmpdir do |dir| Dir.chdir(dir) do example.run end end end describe ".run" do let(:recipes) { %w! ./recipe1.rb ./recipe2.rb ! } it "runs each recipe with the runner" do pending "Rewrite later" recipes.each do |r| recipe = double(:recipe) allow(Recipe).to receive(:new).with( an_instance_of(Itamae::Runner), File.expand_path(r) ).and_return(recipe) expect(recipe).to receive(:run) end described_class.run(recipes, :local, {}) end end end end itamae-1.9.10/spec/unit/lib/itamae/node_spec.rb0000644000175000017500000000064513017534453020331 0ustar scottscottrequire 'spec_helper' module Itamae describe Node do let(:backend) { nil } describe "#reverse_merge" do it "merges a hash but the method receiver's value will be preferred" do a = described_class.new({a: :b, c: :d}, backend) expected = described_class.new({a: :b, c: :d, e: :f}, backend) expect(a.reverse_merge(a: :c, e: :f).mash).to eq(expected.mash) end end end end itamae-1.9.10/bin/0000755000175000017500000000000013017534453012671 5ustar scottscottitamae-1.9.10/bin/itamae0000755000175000017500000000007513017534453014061 0ustar scottscott#!/usr/bin/env ruby require 'itamae/cli' Itamae::CLI.start itamae-1.9.10/ci/0000755000175000017500000000000013017534453012514 5ustar scottscottitamae-1.9.10/ci/destroy_old_droplets.rb0000644000175000017500000000112413017534453017302 0ustar scottscottrequire 'net/https' require 'json' require 'time' http = Net::HTTP.new("api.digitalocean.com", 443) http.use_ssl = true res = http.start do http.get("/v2/droplets", "Authorization" => "Bearer #{ENV['DIGITALOCEAN_TOKEN']}") end droplets = JSON.parse(res.body)['droplets'] droplets.each do |droplet| next unless /^itamae-/ =~ droplet['name'] if Time.now - Time.parse(droplet['created_at']) >= 60 * 60 puts "destroying #{droplet}..." res = http.start do http.delete("/v2/droplets/#{droplet['id']}", "Authorization" => "Bearer #{ENV['DIGITALOCEAN_TOKEN']}") end end end itamae-1.9.10/.rspec0000644000175000017500000000001413017534453013231 0ustar scottscott--color -fd itamae-1.9.10/lib/0000755000175000017500000000000013017534453012667 5ustar scottscottitamae-1.9.10/lib/itamae.rb0000644000175000017500000000066113017534453014457 0ustar scottscottrequire "itamae/version" require "itamae/runner" require "itamae/cli" require "itamae/recipe" require "itamae/resource" require "itamae/handler" require "itamae/handler_proxy" require "itamae/recipe_children" require "itamae/logger" require "itamae/node" require "itamae/backend" require "itamae/notification" require "itamae/definition" require "itamae/ext" require "itamae/generators" module Itamae # Your code goes here... end itamae-1.9.10/lib/itamae/0000755000175000017500000000000013017534453014127 5ustar scottscottitamae-1.9.10/lib/itamae/handler.rb0000644000175000017500000000057613017534453016101 0ustar scottscottrequire 'itamae/handler/base' module Itamae module Handler def self.from_type(type) first_time = true class_name = type.split('_').map(&:capitalize).join self.const_get(class_name) rescue NameError require "itamae/handler/#{type}" if first_time first_time = false retry else raise end end end end itamae-1.9.10/lib/itamae/node.rb0000644000175000017500000000302713017534453015403 0ustar scottscottrequire 'itamae' require 'hashie' require 'json' require 'schash' module Itamae class Node ValidationError = Class.new(StandardError) attr_reader :mash def initialize(hash, backend) @mash = Hashie::Mash.new(hash) @backend = backend end def reverse_merge(other_hash) self.class.new(_reverse_merge(other_hash), @backend) end def reverse_merge!(other_hash) @mash.replace(_reverse_merge(other_hash)) end def [](key) if @mash.has_key?(key) @mash[key] else fetch_inventory_value(key) end end def validate!(&block) errors = Schash::Validator.new(&block).validate(@mash) unless errors.empty? errors.each do |error| Itamae.logger.error "'#{error.position.join('->')}' #{error.message}" end raise ValidationError end end private def _reverse_merge(other_hash) Hashie::Mash.new(other_hash).merge(@mash) end def method_missing(method, *args) if @mash.respond_to?(method) return @mash.public_send(method, *args) elsif args.empty? && value = fetch_inventory_value(method) return value end super end def respond_to?(method, priv = false) @mash.respond_to?(method, priv) || super end def fetch_inventory_value(key) value = @backend.host_inventory[key] if value.is_a?(Hash) value = Hashie::Mash.new(value) end value rescue NotImplementedError, NameError nil end end end itamae-1.9.10/lib/itamae/ext.rb0000644000175000017500000000003713017534453015254 0ustar scottscottrequire 'itamae/ext/specinfra' itamae-1.9.10/lib/itamae/logger.rb0000644000175000017500000000541113017534453015734 0ustar scottscottrequire 'itamae' require 'logger' require 'ansi/code' module Itamae module Logger module Helper def with_indent indent yield ensure outdent end def with_indent_if(condition, &block) if condition with_indent(&block) else block.call end end def indent self.indent_depth += 1 end def outdent self.indent_depth -= 1 self.indent_depth = 0 if self.indent_depth < 0 end def indent_depth @indent_depth ||= 0 end def indent_depth=(val) @indent_depth = val end def color(code, &block) if self.formatter.respond_to?(:color) self.formatter.color(code, &block) else block.call end end %w!debug info warn error fatal unknown!.each do |level| module_eval(<<-EOC, __FILE__, __LINE__ + 1) def #{level}(msg) super(indent_msg(msg)) end EOC end private def indent_msg(msg) spaces = " " * indent_depth case msg when ::String "#{spaces}#{msg}" when ::Exception "#{spaces}#{msg.message} (#{msg.class})\n" << (msg.backtrace || []).map {|f| "#{spaces}#{f}"}.join("\n") else "#{spaces}#{msg.inspect}" end end end class Formatter attr_accessor :colored def call(severity, datetime, progname, msg) log = "%s : %s" % ["%5s" % severity, msg2str(msg)] (colored ? colorize(log, severity) : log) + "\n" end def color(code) prev_color = @color @color = code yield ensure @color = prev_color end private def msg2str(msg) case msg when ::String msg when ::Exception "#{ msg.message } (#{ msg.class })\n" << (msg.backtrace || []).join("\n") else msg.inspect end end def colorize(str, severity) if @color color_code = @color else color_code = case severity when "INFO" :clear when "WARN" :magenta when "ERROR" :red else :clear end end ANSI.public_send(color_code) { str } end end end @logger = ::Logger.new($stdout).tap do |l| l.formatter = Itamae::Logger::Formatter.new end.extend(Itamae::Logger::Helper) class << self def logger @logger end def logger=(l) @logger = l.extend(Itamae::Logger::Helper) end end end itamae-1.9.10/lib/itamae/backend.rb0000644000175000017500000001753313017534453016054 0ustar scottscottrequire 'specinfra/core' require 'singleton' require 'io/console' require 'net/ssh' Specinfra::Configuration.error_on_missing_backend_type = true module Specinfra module Configuration def self.sudo_password return ENV['SUDO_PASSWORD'] if ENV['SUDO_PASSWORD'] return @sudo_password if @sudo_password # TODO: Fix this dirty hack return nil unless caller.any? {|call| call.include?('channel_data') } print "sudo password: " @sudo_password = STDIN.noecho(&:gets).strip print "\n" @sudo_password end end end module Itamae module Backend UnknownBackendTypeError = Class.new(StandardError) CommandExecutionError = Class.new(StandardError) SourceNotExistError = Class.new(StandardError) class << self def create(type, opts = {}) self.const_get(type.capitalize).new(opts) end end class Base attr_reader :executed_commands def initialize(options) @options = options @backend = create_specinfra_backend @executed_commands = [] end def run_command(commands, options = {}) options = {error: true}.merge(options) command = build_command(commands, options) Itamae.logger.debug "Executing `#{command}`..." result = nil Itamae.logger.with_indent do reset_output_handler result = run_command_with_profiling(command) flush_output_handler_buffer if result.exit_status == 0 || !options[:error] method = :debug message = "exited with #{result.exit_status}" else method = :error message = "Command `#{command}` failed. (exit status: #{result.exit_status})" unless Itamae.logger.level == ::Logger::DEBUG result.stdout.each_line do |l| log_output_line("stdout", l, :error) end result.stderr.each_line do |l| log_output_line("stderr", l, :error) end end end Itamae.logger.public_send(method, message) end if options[:error] && result.exit_status != 0 raise CommandExecutionError end result end def get_command(*args) @backend.command.get(*args) end def receive_file(src, dst = nil) if dst Itamae.logger.debug "Receiving a file from '#{src}' to '#{dst}'..." else Itamae.logger.debug "Receiving a file from '#{src}'..." end @backend.receive_file(src, dst) end def send_file(src, dst) Itamae.logger.debug "Sending a file from '#{src}' to '#{dst}'..." unless ::File.exist?(src) raise SourceNotExistError, "The file '#{src}' doesn't exist." end unless ::File.file?(src) raise SourceNotExistError, "'#{src}' is not a file." end @backend.send_file(src, dst) end def send_directory(src, dst) Itamae.logger.debug "Sending a directory from '#{src}' to '#{dst}'..." unless ::File.exist?(src) raise SourceNotExistError, "The directory '#{src}' doesn't exist." end unless ::File.directory?(src) raise SourceNotExistError, "'#{src}' is not a directory." end @backend.send_directory(src, dst) end def host_inventory @backend.host_inventory end def finalize # pass end private def create_specinfra_backend raise NotImplementedError end def reset_output_handler @buf = {} %w!stdout stderr!.each do |output_name| @buf[output_name] = "" handler = lambda do |str| lines = str.split(/\r?\n/, -1) @buf[output_name] += lines.pop unless lines.empty? lines[0] = @buf[output_name] + lines[0] @buf[output_name] = "" lines.each do |l| log_output_line(output_name, l) end end end @backend.public_send("#{output_name}_handler=", handler) end end def flush_output_handler_buffer @buf.each do |output_name, line| next if line.empty? log_output_line(output_name, line) end end def log_output_line(output_name, line, severity = :debug) line = line.gsub(/[[:cntrl:]]/, '') Itamae.logger.public_send(severity, "#{output_name} | #{line}") end def build_command(commands, options) if commands.is_a?(Array) command = commands.map do |cmd| cmd.shellescape end.join(' ') else command = commands end cwd = options[:cwd] if cwd command = "cd #{cwd.shellescape} && #{command}" end user = options[:user] if user command = "cd ~#{user.shellescape} ; #{command}" command = "sudo -H -u #{user.shellescape} -- #{shell.shellescape} -c #{command.shellescape}" end command end def shell @options[:shell] || '/bin/sh' end def run_command_with_profiling(command) start_at = Time.now result = @backend.run_command(command) duration = Time.now.to_f - start_at.to_f @executed_commands << {command: command, duration: duration} result end end class Local < Base private def create_specinfra_backend Specinfra::Backend::Exec.new( shell: @options[:shell], ) end end class Ssh < Base private def create_specinfra_backend Specinfra::Backend::Ssh.new( request_pty: true, host: ssh_options[:host_name], disable_sudo: disable_sudo?, ssh_options: ssh_options, shell: @options[:shell], login_shell: @options[:login_shell], ) end def ssh_options opts = {} opts[:host_name] = @options[:host] # from ssh-config ssh_config_files = @options[:ssh_config] ? [@options[:ssh_config]] : Net::SSH::Config.default_files opts.merge!(Net::SSH::Config.for(@options[:host], ssh_config_files)) opts[:user] = @options[:user] || opts[:user] || Etc.getlogin opts[:password] = @options[:password] if @options[:password] opts[:keys] = [@options[:key]] if @options[:key] opts[:port] = @options[:port] if @options[:port] if @options[:vagrant] config = Tempfile.new('', Dir.tmpdir) hostname = opts[:host_name] || 'default' vagrant_cmd = "vagrant ssh-config #{hostname} > #{config.path}" if defined?(Bundler) Bundler.with_clean_env do `#{vagrant_cmd}` end else `#{vagrant_cmd}` end opts.merge!(Net::SSH::Config.for(hostname, [config.path])) end if @options[:ask_password] print "password: " password = STDIN.noecho(&:gets).strip print "\n" opts.merge!(password: password) end opts end def disable_sudo? !@options[:sudo] end end class Docker < Base def finalize image = @backend.commit_container Itamae.logger.info "Image created: #{image.id}" end private def create_specinfra_backend begin require 'docker' rescue LoadError Itamae.logger.fatal "To use docker backend, please install 'docker-api' gem" end # TODO: Move to Specinfra? Excon.defaults[:ssl_verify_peer] = @options[:tls_verify_peer] ::Docker.logger = Itamae.logger Specinfra::Backend::Docker.new( docker_image: @options[:image], docker_container: @options[:container], shell: @options[:shell], ) end end end end itamae-1.9.10/lib/itamae/handler/0000755000175000017500000000000013017534453015544 5ustar scottscottitamae-1.9.10/lib/itamae/handler/debug.rb0000644000175000017500000000027313017534453017161 0ustar scottscottmodule Itamae module Handler class Debug < Base def event(type, payload = {}) super Itamae.logger.info("EVENT:#{type} #{payload}") end end end end itamae-1.9.10/lib/itamae/handler/json.rb0000644000175000017500000000062713017534453017047 0ustar scottscottmodule Itamae module Handler class Json < Base def initialize(*) super require 'time' open_file end def event(type, payload = {}) super @f.puts({'time' => Time.now.iso8601, 'event' => type, 'payload' => payload}.to_json) end private def open_file @f = open(@options.fetch('path'), 'a') end end end end itamae-1.9.10/lib/itamae/handler/fluentd.rb0000644000175000017500000000200713017534453017531 0ustar scottscottmodule Itamae module Handler class Fluentd < Base attr_accessor :fluent_logger # for test def initialize(*) super load_fluent_logger end def event(type, payload = {}) super unless @fluent_logger.post(type, payload.merge(hostname: hostname)) Itamae.logger.warn "Sending logs to Fluentd failed: #{@fluent_logger.last_error}" end end private def load_fluent_logger begin require 'fluent-logger' rescue LoadError raise "Loading fluent-logger gem failed. Please install 'fluent-logger' gem to use fluentd handler." end @fluent_logger = Fluent::Logger::FluentLogger.new(tag_prefix, host: fluentd_host, port: fluentd_port) end def tag_prefix @options['tag_prefix'] || 'itamae_server' end def fluentd_host @options['host'] || 'localhost' end def fluentd_port (@options['port'] || 24224).to_i end end end end itamae-1.9.10/lib/itamae/handler/base.rb0000644000175000017500000000152413017534453017005 0ustar scottscottrequire 'socket' module Itamae module Handler class Base attr_reader :recipes, :resources, :actions def initialize(options) @options = options @recipes = [] @resources = [] @actions = [] end def event(type, payload = {}) case type when :recipe_started @recipes << payload when :recipe_completed, :recipe_failed @recipes.pop when :resource_started @resources << payload when :resource_completed, :resource_failed @resources.pop when :action_started @actions << payload when :action_completed, :action_failed @actions.pop end end private def hostname @hostname ||= @options['hostname'] || Socket.gethostname end end end end itamae-1.9.10/lib/itamae/cli.rb0000644000175000017500000001065113017534453015226 0ustar scottscottrequire 'itamae' require 'thor' module Itamae class CLI < Thor GENERATE_TARGETS = %w[cookbook role].freeze class_option :log_level, type: :string, aliases: ['-l'], default: 'info' class_option :color, type: :boolean, default: true class_option :config, type: :string, aliases: ['-c'] def initialize(*) super Itamae.logger.level = ::Logger.const_get(options[:log_level].upcase) Itamae.logger.formatter.colored = options[:color] end def self.define_exec_options option :recipe_graph, type: :string, desc: "[EXPERIMENTAL] Write recipe dependency graph in DOT", banner: "PATH" option :node_json, type: :string, aliases: ['-j'] option :node_yaml, type: :string, aliases: ['-y'] option :dry_run, type: :boolean, aliases: ['-n'] option :shell, type: :string, default: "/bin/sh" option :login_shell, type: :boolean, default: false option :ohai, type: :boolean, default: false, desc: "This option is DEPRECATED and will be unavailable." option :profile, type: :string, desc: "[EXPERIMENTAL] Save profiling data", banner: "PATH" 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" end desc "local RECIPE [RECIPE...]", "Run Itamae locally" define_exec_options def local(*recipe_files) if recipe_files.empty? raise "Please specify recipe files." end run(recipe_files, :local, options) end desc "ssh RECIPE [RECIPE...]", "Run Itamae via ssh" define_exec_options option :host, type: :string, aliases: ['-h'] option :user, type: :string, aliases: ['-u'] option :key, type: :string, aliases: ['-i'] option :port, type: :numeric, aliases: ['-p'] option :ssh_config, type: :string option :vagrant, type: :boolean, default: false option :ask_password, type: :boolean, default: false option :sudo, type: :boolean, default: true def ssh(*recipe_files) if recipe_files.empty? raise "Please specify recipe files." end unless options[:host] || options[:vagrant] raise "Please set '-h ' or '--vagrant'" end run(recipe_files, :ssh, options) end desc "docker RECIPE [RECIPE...]", "Create Docker image" define_exec_options option :image, type: :string, desc: "This option or 'container' option is required." option :container, type: :string, desc: "This option or 'image' option is required." option :tls_verify_peer, type: :boolean, default: true def docker(*recipe_files) if recipe_files.empty? raise "Please specify recipe files." end run(recipe_files, :docker, options) end desc "version", "Print version" def version puts "Itamae v#{Itamae::VERSION}" end desc "init NAME", "Create a new project" def init(name) generator = Generators::Project.new generator.destination_root = name generator.invoke_all end desc 'generate [cookbook|role] [NAME]', 'Initialize role or cookbook (short-cut alias: "g")' map 'g' => 'generate' def generate(target, name) validate_generate_target!('generate', target) generator = Generators.find(target).new generator.destination_root = File.join("#{target}s", name) generator.copy_files end desc 'destroy [cookbook|role] [NAME]', 'Undo role or cookbook (short-cut alias: "d")' map 'd' => 'destroy' def destroy(target, name) validate_generate_target!('destroy', target) generator = Generators.find(target).new generator.destination_root = File.join("#{target}s", name) generator.remove_files end private def options @itamae_options ||= super.dup.tap do |options| if config = options[:config] options.merge!(YAML.load_file(config)) end end end def validate_generate_target!(command, target) unless GENERATE_TARGETS.include?(target) msg = %Q!ERROR: "itamae #{command}" was called with "#{target}" ! msg << "but expected to be in #{GENERATE_TARGETS.inspect}" fail InvocationError, msg end end def run(recipe_files, backend_type, options) runner = Runner.run(recipe_files, backend_type, options) if options[:detailed_exitcode] && runner.diff? exit 2 end end end end itamae-1.9.10/lib/itamae/handler_proxy.rb0000644000175000017500000000125413017534453017334 0ustar scottscottmodule Itamae class HandlerProxy def initialize @instances = [] end def register_instance(instance) @instances << instance end def event(*args, &block) if block_given? _event_with_block(*args, &block) else _event(*args) end end private def _event(*args) @instances.each do |i| i.event(*args) end end def _event_with_block(event_name, *args, &block) event("#{event_name}_started".to_sym, *args) block.call rescue event("#{event_name}_failed".to_sym, *args) raise else event("#{event_name}_completed".to_sym, *args) end end end itamae-1.9.10/lib/itamae/resource.rb0000644000175000017500000000377113017534453016313 0ustar scottscottrequire 'itamae' require 'itamae/resource/base' require 'itamae/resource/file' require 'itamae/resource/package' require 'itamae/resource/remote_directory' require 'itamae/resource/remote_file' require 'itamae/resource/directory' require 'itamae/resource/template' require 'itamae/resource/http_request' require 'itamae/resource/execute' require 'itamae/resource/service' require 'itamae/resource/link' require 'itamae/resource/local_ruby_block' require 'itamae/resource/git' require 'itamae/resource/user' require 'itamae/resource/group' require 'itamae/resource/gem_package' module Itamae module Resource Error = Class.new(StandardError) AttributeMissingError = Class.new(StandardError) InvalidTypeError = Class.new(StandardError) ParseError = Class.new(StandardError) class << self def to_camel_case(str) str.split('_').map {|part| part.capitalize}.join end def get_resource_class(method) begin self.const_get(to_camel_case(method.to_s)) rescue NameError begin ::Itamae::Plugin::Resource.const_get(to_camel_case(method.to_s)) rescue NameError autoload_plugin_resource(method) end end end def autoload_plugin_resource(method) begin require "itamae/plugin/resource/#{method}" ::Itamae::Plugin::Resource.const_get(to_camel_case(method.to_s)) rescue LoadError, NameError raise Error, "#{method} resource is missing." end end def define_resource(name, klass) class_name = to_camel_case(name.to_s) if Resource.const_defined?(class_name) Itamae.logger.warn "Redefine class. (#{class_name})" return end Resource.const_set(class_name, klass) end def parse_description(desc) if /\A([^\[]+)\[([^\]]+)\]\z/ =~ desc [$1, $2] else raise ParseError, "'#{desc}' doesn't represent a resource." end end end end end itamae-1.9.10/lib/itamae/recipe_children.rb0000644000175000017500000000366413017534453017604 0ustar scottscottmodule Itamae class RecipeChildren < Array NotFoundError = Class.new(StandardError) def find_resource_by_description(desc) # desc is like 'resource_type[name]' resources.find do |resource| type, name = Itamae::Resource.parse_description(desc) resource.resource_type == type && resource.resource_name == name end.tap do |resource| unless resource raise NotFoundError, "'#{desc}' resource is not found." end end end def subscribing(target) resources.map do |resource| resource.subscriptions.select do |subscription| subscription.resource == target end end.flatten end def find_recipe_by_path(path) recipes.find do |recipe| recipe.path == path end end def resources self.map do |item| case item when Resource::Base item when Recipe item.children.resources end end.flatten end def recipes(options = {}) options = {recursive: true}.merge(options) self.select do |item| item.is_a?(Recipe) end.map do |recipe| if options[:recursive] [recipe] + recipe.children.recipes else recipe end end.flatten end def run self.each do |resource| resource.run end end # returns dependencies graph in DOT def dependency_in_dot result = "" result << "digraph recipes {\n" result << " rankdir=LR;\n" result << _dependency_in_dot result << "}" result end def _dependency_in_dot result = "" recipes(recursive: false).each do |recipe| recipe.children.recipes(recursive: false).each do |child_recipe| result << %{ "#{recipe.path}" -> "#{child_recipe.path}";\n} end result << recipe.children._dependency_in_dot end result end end end itamae-1.9.10/lib/itamae/generators.rb0000644000175000017500000000057013017534453016627 0ustar scottscottrequire "itamae/generators/cookbook" require "itamae/generators/project" require "itamae/generators/role" module Itamae module Generators def self.find(target) case target when 'cookbook' Cookbook when 'project' Project when 'role' Role else raise "Unexpected target: #{target}" end end end end itamae-1.9.10/lib/itamae/notification.rb0000644000175000017500000000165213017534453017146 0ustar scottscottrequire 'itamae' module Itamae class Notification < Struct.new(:defined_in_resource, :action, :target_resource_desc, :timing) def self.create(*args) self.new(*args).tap(&:validate!) end def resource runner.children.find_resource_by_description(target_resource_desc) end def run action_resource.run(action) end def action_resource resource end def runner defined_in_resource.recipe.runner end def delayed? [:delay, :delayed].include?(timing) end def immediately? timing == :immediately end def validate! unless [:delay, :delayed, :immediately].include?(timing) Itamae.logger.error "'#{timing}' is not valid notification timing. (Valid option is delayed or immediately)" abort end end end class Subscription < Notification def action_resource defined_in_resource end end end itamae-1.9.10/lib/itamae/ext/0000755000175000017500000000000013017534453014727 5ustar scottscottitamae-1.9.10/lib/itamae/ext/specinfra.rb0000644000175000017500000000127213017534453017230 0ustar scottscott# TODO: Send patches to Specinfra module Specinfra module Backend class Base def receive_file(from, to = nil) raise NotImplementedError end end class Exec < Base def receive_file(from, to = nil) if to FileUtils.cp(from, to) else ::File.read(from) end end end class Ssh < Exec def receive_file(from, to = nil) scp_download!(from, to) end private def scp_download!(from, to, opt={}) if get_config(:scp).nil? set_config(:scp, create_scp) end scp = get_config(:scp) scp.download!(from, to, opt) end end end end itamae-1.9.10/lib/itamae/recipe.rb0000644000175000017500000001022413017534453015722 0ustar scottscottrequire 'itamae' module Itamae class Recipe NotFoundError = Class.new(StandardError) attr_reader :path attr_reader :runner attr_reader :children attr_reader :delayed_notifications class << self def find_recipe_in_gem(recipe) plugin_name, recipe_file = recipe.split('::', 2) recipe_file = recipe_file.gsub("::", "/") if recipe_file gem_name = "itamae-plugin-recipe-#{plugin_name}" begin gem gem_name rescue LoadError end spec = Gem.loaded_specs.values.find do |spec| spec.name == gem_name end return nil unless spec candidate_files = [] if recipe_file recipe_file += '.rb' unless recipe_file.end_with?('.rb') candidate_files << "#{plugin_name}/#{recipe_file}" else candidate_files << "#{plugin_name}/default.rb" candidate_files << "#{plugin_name}.rb" end candidate_files.map do |file| File.join(spec.lib_dirs_glob, 'itamae', 'plugin', 'recipe', file) end.find do |path| File.exist?(path) end end end def initialize(runner, path) @runner = runner @path = path @delayed_notifications = [] @children = RecipeChildren.new end def dir ::File.dirname(@path) end def load(vars = {}) context = EvalContext.new(self, vars) context.instance_eval(File.read(path), path, 1) end def run show_banner @runner.handler.event(:recipe, path: @path) do Itamae.logger.with_indent do @children.run run_delayed_notifications end end end private def run_delayed_notifications @delayed_notifications.uniq! do |notification| [notification.action, notification.action_resource] end while notification = @delayed_notifications.shift notification.run end end def show_banner Itamae.logger.info "Recipe: #{@path}" end class EvalContext def initialize(recipe, vars) @recipe = recipe vars.each do |k, v| define_singleton_method(k) { v } end end def respond_to_missing?(method, include_private = false) Resource.get_resource_class(method) true rescue NameError false end def method_missing(*args, &block) super unless args.size == 2 method, name = args begin klass = Resource.get_resource_class(method) rescue NameError super end resource = klass.new(@recipe, name, &block) @recipe.children << resource end def define(name, params = {}, &block) Resource.define_resource(name, Definition.create_class(name, params, @recipe, &block)) end def include_recipe(target) expanded_path = ::File.expand_path(target, File.dirname(@recipe.path)) expanded_path = ::File.join(expanded_path, 'default.rb') if ::Dir.exist?(expanded_path) expanded_path.concat('.rb') unless expanded_path.end_with?('.rb') candidate_paths = [expanded_path, Recipe.find_recipe_in_gem(target)].compact path = candidate_paths.find {|path| File.exist?(path) } unless path raise NotFoundError, "Recipe not found. (#{target})" end if runner.children.find_recipe_by_path(path) Itamae.logger.debug "Recipe, #{path}, is skipped because it is already included" return end recipe = Recipe.new(runner, path) @recipe.children << recipe recipe.load end def node runner.node end def runner @recipe.runner end def run_command(*args) runner.backend.run_command(*args) end end class RecipeFromDefinition < Recipe attr_accessor :definition def load(vars = {}) context = EvalContext.new(self, vars) context.instance_eval(&@definition.class.definition_block) end private def show_banner Itamae.logger.debug "#{@definition.resource_type}[#{@definition.resource_name}]" end end end end itamae-1.9.10/lib/itamae/resource/0000755000175000017500000000000013017534453015756 5ustar scottscottitamae-1.9.10/lib/itamae/resource/group.rb0000644000175000017500000000201513017534453017435 0ustar scottscottrequire 'itamae' module Itamae module Resource class Group < Base define_attribute :action, default: :create define_attribute :groupname, type: String, default_name: true define_attribute :gid, type: Integer def set_current_attributes current.exist = exist? if current.exist current.gid = run_specinfra(:get_group_gid, attributes.groupname).stdout.strip.to_i end end def action_create(options) if run_specinfra(:check_group_exists, attributes.groupname) if attributes.gid && attributes.gid != current.gid run_specinfra(:update_group_gid, attributes.groupname, attributes.gid) updated! end else options = { gid: attributes.gid, } run_specinfra(:add_group, attributes.groupname, options) updated! end end private def exist? run_specinfra(:check_group_exists, attributes.groupname) end end end end itamae-1.9.10/lib/itamae/resource/directory.rb0000644000175000017500000000357213017534453020316 0ustar scottscottrequire 'itamae' module Itamae module Resource class Directory < Base define_attribute :action, default: :create define_attribute :path, type: String, default_name: true define_attribute :mode, type: String define_attribute :owner, type: String define_attribute :group, type: String def pre_action case @current_action when :create attributes.exist = true when :delete attributes.exist = false end end def show_differences current.mode = current.mode.rjust(4, '0') if current.mode attributes.mode = attributes.mode.rjust(4, '0') if attributes.mode super end def set_current_attributes exist = run_specinfra(:check_file_is_directory, attributes.path) current.exist = exist if exist current.mode = run_specinfra(:get_file_mode, attributes.path).stdout.chomp current.owner = run_specinfra(:get_file_owner_user, attributes.path).stdout.chomp current.group = run_specinfra(:get_file_owner_group, attributes.path).stdout.chomp else current.mode = nil current.owner = nil current.group = nil end end def action_create(options) if !run_specinfra(:check_file_is_directory, attributes.path) run_specinfra(:create_file_as_directory, attributes.path) end if attributes.mode run_specinfra(:change_file_mode, attributes.path, attributes.mode) end if attributes.owner || attributes.group run_specinfra(:change_file_owner, attributes.path, attributes.owner, attributes.group) end end def action_delete(options) if run_specinfra(:check_file_is_directory, attributes.path) run_specinfra(:remove_file, attributes.path) end end end end end itamae-1.9.10/lib/itamae/resource/local_ruby_block.rb0000644000175000017500000000040413017534453021606 0ustar scottscottrequire 'itamae' module Itamae module Resource class LocalRubyBlock < Base define_attribute :action, default: :run define_attribute :block, type: Proc def action_run(options) attributes.block.call end end end end itamae-1.9.10/lib/itamae/resource/link.rb0000644000175000017500000000174513017534453017247 0ustar scottscottrequire 'itamae' module Itamae module Resource class Link < Base define_attribute :action, default: :create define_attribute :link, type: String, default_name: true define_attribute :to, type: String, required: true define_attribute :force, type: [TrueClass, FalseClass], default: false def pre_action case @current_action when :create attributes.exist = true end end def set_current_attributes current.exist = run_specinfra(:check_file_is_link, attributes.link) if current.exist current.to = run_specinfra(:get_file_link_target, attributes.link).stdout.strip end end def action_create(options) unless run_specinfra(:check_file_is_linked_to, attributes.link, attributes.to) run_specinfra(:link_file_to, attributes.link, attributes.to, force: attributes.force, no_dereference: attributes.force) end end end end end itamae-1.9.10/lib/itamae/resource/http_request.rb0000644000175000017500000000452113017534453021034 0ustar scottscottrequire 'itamae' require 'uri' require 'net/https' module Itamae module Resource class HttpRequest < File RedirectLimitExceeded = Class.new(StandardError) alias_method :_action_create, :action_create undef_method :action_create, :action_delete, :action_edit define_attribute :action, default: :get define_attribute :headers, type: Hash, default: {} define_attribute :message, type: String, default: "" define_attribute :redirect_limit, type: Integer, default: 10 define_attribute :url, type: String, required: true def pre_action attributes.exist = true attributes.content = fetch_content send_tempfile compare_file end def show_differences current.mode = current.mode.rjust(4, '0') if current.mode attributes.mode = attributes.mode.rjust(4, '0') if attributes.mode super show_content_diff end def fetch_content uri = URI.parse(attributes.url) response = nil redirects_followed = 0 loop do http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true if uri.scheme == "https" case attributes.action when :delete, :get, :options response = http.method(attributes.action).call(uri.request_uri, attributes.headers) when :post, :put response = http.method(attributes.action).call(uri.request_uri, attributes.message, attributes.headers) end if response.kind_of?(Net::HTTPRedirection) if redirects_followed < attributes.redirect_limit uri = URI.parse(response["location"]) redirects_followed += 1 Itamae.logger.debug "Following redirect #{redirects_followed}/#{attributes.redirect_limit}" else raise RedirectLimitExceeded end else break end end response.body end def action_delete(options) _action_create(options) end def action_get(options) _action_create(options) end def action_options(options) _action_create(options) end def action_post(options) _action_create(options) end def action_put(options) _action_create(options) end end end end itamae-1.9.10/lib/itamae/resource/package.rb0000644000175000017500000000237113017534453017701 0ustar scottscottrequire 'itamae' module Itamae module Resource class Package < Base define_attribute :action, default: :install define_attribute :name, type: String, default_name: true define_attribute :version, type: String define_attribute :options, type: String def pre_action case @current_action when :install attributes.installed = true when :remove attributes.installed = false end end def set_current_attributes current.installed = run_specinfra(:check_package_is_installed, attributes.name) if current.installed current.version = run_specinfra(:get_package_version, attributes.name).stdout.strip end end def action_install(action_options) unless run_specinfra(:check_package_is_installed, attributes.name, attributes.version) run_specinfra(:install_package, attributes.name, attributes.version, attributes.options) updated! end end def action_remove(action_options) if run_specinfra(:check_package_is_installed, attributes.name, nil) run_specinfra(:remove_package, attributes.name, attributes.options) updated! end end end end end itamae-1.9.10/lib/itamae/resource/remote_directory.rb0000644000175000017500000000512713017534453021667 0ustar scottscottrequire 'itamae' module Itamae module Resource class RemoteDirectory < Base define_attribute :action, default: :create define_attribute :path, type: String, default_name: true define_attribute :source, type: String, required: true define_attribute :mode, type: String define_attribute :owner, type: String define_attribute :group, type: String def pre_action directory = ::File.expand_path(attributes.source, ::File.dirname(@recipe.path)) src = ::File.expand_path(directory, ::File.dirname(@recipe.path)) @temppath = ::File.join(runner.tmpdir, Time.now.to_f.to_s) backend.send_directory(src, @temppath) case @current_action when :create attributes.exist = true end end def set_current_attributes current.exist = run_specinfra(:check_file_is_directory, attributes.path) if current.exist current.mode = run_specinfra(:get_file_mode, attributes.path).stdout.chomp current.owner = run_specinfra(:get_file_owner_user, attributes.path).stdout.chomp current.group = run_specinfra(:get_file_owner_group, attributes.path).stdout.chomp else current.mode = nil current.owner = nil current.group = nil end end def show_differences super if current.exist diff = run_command(["diff", "-u", attributes.path, @temppath], error: false) if diff.exit_status == 0 # no change Itamae.logger.debug "directory content will not change" else Itamae.logger.info "diff:" diff.stdout.each_line do |line| Itamae.logger.info "#{line.strip}" end end end end def action_create(options) if attributes.mode run_specinfra(:change_file_mode, @temppath, attributes.mode) end if attributes.owner || attributes.group run_specinfra(:change_file_owner, @temppath, attributes.owner, attributes.group) end if run_specinfra(:check_file_is_file, attributes.path) unless check_command(["diff", "-q", @temppath, attributes.path]) updated! end else updated! end run_specinfra(:remove_file, attributes.path) run_specinfra(:move_file, @temppath, attributes.path) end def action_delete(options) if run_specinfra(:check_file_is_directory, attributes.path) run_specinfra(:remove_file, attributes.path) end end end end end itamae-1.9.10/lib/itamae/resource/file.rb0000644000175000017500000001317313017534453017227 0ustar scottscottrequire 'itamae' module Itamae module Resource class File < Base define_attribute :action, default: :create define_attribute :path, type: String, default_name: true define_attribute :content, type: String, default: nil define_attribute :mode, type: String define_attribute :owner, type: String define_attribute :group, type: String define_attribute :block, type: Proc, default: proc {} def pre_action current.exist = run_specinfra(:check_file_is_file, attributes.path) case @current_action when :create attributes.exist = true when :delete attributes.exist = false when :edit attributes.exist = true if !runner.dry_run? || current.exist content = backend.receive_file(attributes.path) attributes.block.call(content) attributes.content = content end end send_tempfile compare_file end def set_current_attributes current.modified = false if current.exist current.mode = run_specinfra(:get_file_mode, attributes.path).stdout.chomp current.owner = run_specinfra(:get_file_owner_user, attributes.path).stdout.chomp current.group = run_specinfra(:get_file_owner_group, attributes.path).stdout.chomp else current.mode = nil current.owner = nil current.group = nil end end def show_differences current.mode = current.mode.rjust(4, '0') if current.mode attributes.mode = attributes.mode.rjust(4, '0') if attributes.mode super if @temppath && @current_action != :delete show_content_diff end end def action_create(options) if !current.exist && !@temppath run_command(["touch", attributes.path]) end change_target = attributes.modified ? @temppath : attributes.path if attributes.owner || attributes.group run_specinfra(:change_file_owner, change_target, attributes.owner, attributes.group) end if attributes.mode run_specinfra(:change_file_mode, change_target, attributes.mode) end if attributes.modified run_specinfra(:move_file, @temppath, attributes.path) end end def action_delete(options) if run_specinfra(:check_file_is_file, attributes.path) run_specinfra(:remove_file, attributes.path) end end def action_edit(options) change_target = attributes.modified ? @temppath : attributes.path if attributes.owner || attributes.group || attributes.modified owner = attributes.owner || run_specinfra(:get_file_owner_user, attributes.path).stdout.chomp group = attributes.group || run_specinfra(:get_file_owner_group, attributes.path).stdout.chomp run_specinfra(:change_file_owner, change_target, owner, group) end if attributes.mode || attributes.modified mode = attributes.mode || run_specinfra(:get_file_mode, attributes.path).stdout.chomp run_specinfra(:change_file_mode, change_target, mode) end if attributes.modified run_specinfra(:move_file, @temppath, attributes.path) end end private def compare_to if current.exist attributes.path else '/dev/null' end end def compare_file attributes.modified = false unless @temppath return end case run_command(["diff", "-q", compare_to, @temppath], error: false).exit_status when 1 # diff found attributes.modified = true when 2 # error raise Itamae::Backend::CommandExecutionError, "diff command exited with 2" end end def show_content_diff if attributes.modified Itamae.logger.info "diff:" diff = run_command(["diff", "-u", compare_to, @temppath], error: false) diff.stdout.each_line do |line| color = if line.start_with?('+') :green elsif line.start_with?('-') :red else :clear end Itamae.logger.color(color) do Itamae.logger.info line.chomp end end runner.handler.event(:file_content_changed, diff: diff.stdout) else # no change Itamae.logger.debug "file content will not change" end end # will be overridden def content_file nil end def send_tempfile if !attributes.content && !content_file @temppath = nil return end begin src = if content_file content_file else f = Tempfile.open('itamae') f.write(attributes.content) f.close f.path end @temppath = ::File.join(runner.tmpdir, Time.now.to_f.to_s) if backend.is_a?(Itamae::Backend::Docker) run_command(["mkdir", @temppath]) backend.send_file(src, @temppath) @temppath = ::File.join(@temppath, ::File.basename(src)) else run_command(["touch", @temppath]) run_specinfra(:change_file_mode, @temppath, '0600') backend.send_file(src, @temppath) end run_specinfra(:change_file_mode, @temppath, '0600') ensure f.unlink if f end end end end end itamae-1.9.10/lib/itamae/resource/service.rb0000644000175000017500000000343313017534453017746 0ustar scottscottrequire 'itamae' module Itamae module Resource class Service < Base define_attribute :action, default: :nothing define_attribute :name, type: String, default_name: true define_attribute :provider, type: Symbol, default: nil def initialize(*args) super @under = attributes.provider ? "_under_#{attributes.provider}" : "" end def pre_action case @current_action when :start, :restart attributes.running = true when :stop attributes.running = false when :enable attributes.enabled = true when :disable attributes.enabled = false end end def set_current_attributes current.running = run_specinfra(:"check_service_is_running#{@under}", attributes.name) current.enabled = run_specinfra(:"check_service_is_enabled#{@under}", attributes.name) end def action_start(options) unless current.running run_specinfra(:"start_service#{@under}", attributes.name) end end def action_stop(options) if current.running run_specinfra(:"stop_service#{@under}", attributes.name) end end def action_restart(options) run_specinfra(:"restart_service#{@under}", attributes.name) end def action_reload(options) if current.running run_specinfra(:"reload_service#{@under}", attributes.name) end end def action_enable(options) unless current.enabled run_specinfra(:"enable_service#{@under}", attributes.name) end end def action_disable(options) if current.enabled run_specinfra(:"disable_service#{@under}", attributes.name) end end end end end itamae-1.9.10/lib/itamae/resource/git.rb0000644000175000017500000000573613017534453017101 0ustar scottscottrequire 'itamae' module Itamae module Resource class Git < Base DEPLOY_BRANCH = "deploy" define_attribute :action, default: :sync define_attribute :destination, type: String, default_name: true define_attribute :repository, type: String, required: true define_attribute :revision, type: String define_attribute :recursive, type: [TrueClass, FalseClass], default: false define_attribute :depth, type: Integer def pre_action case @current_action when :sync attributes.exist = true end end def set_current_attributes current.exist = run_specinfra(:check_file_is_directory, attributes.destination) end def action_sync(options) ensure_git_available new_repository = false if check_empty_dir cmd = ['git', 'clone'] cmd << '--recursive' if attributes.recursive cmd += ['--depth', attributes.depth.to_s] if attributes.depth cmd << attributes.repository << attributes.destination run_command(cmd) new_repository = true end target = if attributes.revision get_revision(attributes.revision) else fetch_origin! run_command_in_repo("git ls-remote origin HEAD | cut -f1").stdout.strip end if new_repository || target != get_revision('HEAD') updated! deploy_old_created = false if current_branch == DEPLOY_BRANCH run_command_in_repo("git branch -m deploy-old") deploy_old_created = true end fetch_origin! run_command_in_repo(["git", "checkout", target, "-b", DEPLOY_BRANCH]) if deploy_old_created run_command_in_repo("git branch -d deploy-old") end end end private def ensure_git_available unless run_command("which git", error: false).exit_status == 0 raise "`git` command is not available. Please install git." end end def check_empty_dir run_command("test -z \"$(ls -A #{shell_escape(attributes.destination)})\"", error: false).success? end def run_command_in_repo(*args) unless args.last.is_a?(Hash) args << {} end args.last[:cwd] = attributes.destination run_command(*args) end def current_branch run_command_in_repo("git rev-parse --abbrev-ref HEAD").stdout.strip end def get_revision(branch) result = run_command_in_repo("git rev-list #{shell_escape(branch)}", error: false) unless result.exit_status == 0 fetch_origin! end run_command_in_repo("git rev-list #{shell_escape(branch)}").stdout.lines.first.strip end def fetch_origin! return if @origin_fetched @origin_fetched = true run_command_in_repo(['git', 'fetch', 'origin']) end end end end itamae-1.9.10/lib/itamae/resource/execute.rb0000644000175000017500000000077413017534453017755 0ustar scottscottrequire 'itamae' module Itamae module Resource class Execute < Base define_attribute :action, default: :run define_attribute :command, type: String, default_name: true def pre_action case @current_action when :run attributes.executed = true end end def set_current_attributes current.executed = false end def action_run(options) run_command(attributes.command) updated! end end end end itamae-1.9.10/lib/itamae/resource/template.rb0000644000175000017500000000176213017534453020124 0ustar scottscottrequire 'itamae' require 'erb' require 'tempfile' module Itamae module Resource class Template < RemoteFile define_attribute :variables, type: Hash, default: {} def pre_action attributes.content = RenderContext.new(self).render_file(source_file) super end private def content_file nil end def source_file_dir "templates" end def source_file_exts [".erb", ""] end class RenderContext def initialize(resource) @resource = resource @resource.attributes.variables.each_pair do |key, value| instance_variable_set("@#{key}".to_sym, value) end end def render_file(src) template = ::File.read(src) ERB.new(template, nil, '-').tap do |erb| erb.filename = src end.result(binding) end def node @resource.recipe.runner.node end end end end end itamae-1.9.10/lib/itamae/resource/gem_package.rb0000644000175000017500000000526313017534453020534 0ustar scottscottrequire 'itamae' module Itamae module Resource class GemPackage < Base define_attribute :action, default: :install define_attribute :package_name, type: String, default_name: true define_attribute :gem_binary, type: [String, Array], default: 'gem' define_attribute :options, type: [String, Array], default: [] define_attribute :version, type: String define_attribute :source, type: String def pre_action case @current_action when :install attributes.installed = true when :uninstall attributes.installed = false end end def set_current_attributes installed = installed_gems.find {|g| g[:name] == attributes.package_name } current.installed = !!installed if current.installed versions = installed[:versions] if versions.include?(attributes.version) current.version = attributes.version else current.version = versions.first end end end def action_install(action_options) if current.installed if attributes.version && current.version != attributes.version install! updated! end else install! updated! end end def action_uninstall(action_options) uninstall! if current.installed end def action_upgrade(action_options) return if current.installed && attributes.version && current.version == attributes.version install! updated! end def installed_gems gems = [] run_command([*Array(attributes.gem_binary), 'list', '-l']).stdout.each_line do |line| if /\A([^ ]+) \(([^\)]+)\)\z/ =~ line.strip name = $1 versions = $2.split(', ') gems << {name: name, versions: versions} end end gems rescue Backend::CommandExecutionError [] end def install! cmd = [*Array(attributes.gem_binary), 'install', *Array(attributes.options)] if attributes.version cmd << '-v' << attributes.version end if attributes.source cmd << '--source' << attributes.source end cmd << attributes.package_name run_command(cmd) end def uninstall! cmd = [*Array(attributes.gem_binary), 'uninstall', '--ignore-dependencies', '--executables', *Array(attributes.options)] if attributes.version cmd << '-v' << attributes.version else cmd << '--all' end cmd << attributes.package_name run_command(cmd) end end end end itamae-1.9.10/lib/itamae/resource/base.rb0000644000175000017500000002470213017534453017222 0ustar scottscottrequire 'itamae' require 'shellwords' require 'hashie' module Itamae module Resource class Base class EvalContext attr_reader :attributes attr_reader :notifications attr_reader :subscriptions attr_reader :verify_commands attr_reader :only_if_command attr_reader :not_if_command def initialize(resource) @resource = resource @attributes = Hashie::Mash.new @notifications = [] @subscriptions = [] @verify_commands = [] end def respond_to_missing?(method, include_private = false) @resource.class.defined_attributes.has_key?(method) || super end def method_missing(method, *args, &block) if @resource.class.defined_attributes[method] if args.size == 1 return @attributes[method] = args.first elsif args.size == 0 && block_given? return @attributes[method] = block elsif args.size == 0 return @attributes[method] end end super end def notifies(action, resource_desc, timing = :delay) @notifications << Notification.create(@resource, action, resource_desc, timing) end def subscribes(action, resource_desc, timing = :delay) @subscriptions << Subscription.create(@resource, action, resource_desc, timing) end def only_if(command) @only_if_command = command end def not_if(command) @not_if_command = command end def node @resource.recipe.runner.node end def run_command(*args) @resource.recipe.runner.backend.run_command(*args) end # Experimental def verify(command) @verify_commands << command end end @defined_attributes ||= {} class << self attr_reader :defined_attributes attr_reader :supported_oses def inherited(subclass) subclass.instance_variable_set( :@defined_attributes, self.defined_attributes.dup ) end def define_attribute(name, options) current = @defined_attributes[name.to_sym] || {} @defined_attributes[name.to_sym] = current.merge(options) end end define_attribute :action, type: [Symbol, Array], required: true define_attribute :user, type: String define_attribute :cwd, type: String attr_reader :recipe attr_reader :resource_name attr_reader :attributes attr_reader :current_attributes attr_reader :subscriptions attr_reader :notifications attr_reader :updated def initialize(recipe, resource_name, &block) clear_current_attributes @recipe = recipe @resource_name = resource_name @updated = false EvalContext.new(self).tap do |context| context.instance_eval(&block) if block @attributes = context.attributes @notifications = context.notifications @subscriptions = context.subscriptions @only_if_command = context.only_if_command @not_if_command = context.not_if_command @verify_commands = context.verify_commands end process_attributes end def run(specific_action = nil) runner.handler.event(:resource, resource_type: resource_type, resource_name: resource_name) do Itamae.logger.debug "#{resource_type}[#{resource_name}]" Itamae.logger.with_indent_if(Itamae.logger.debug?) do if do_not_run_because_of_only_if? Itamae.logger.debug "#{resource_type}[#{resource_name}] Execution skipped because of only_if attribute" return elsif do_not_run_because_of_not_if? Itamae.logger.debug "#{resource_type}[#{resource_name}] Execution skipped because of not_if attribute" return end [specific_action || attributes.action].flatten.each do |action| run_action(action) end verify unless runner.dry_run? if updated? runner.diff_found! notify runner.handler.event(:resource_updated) end end @updated = false end rescue Backend::CommandExecutionError Itamae.logger.error "#{resource_type}[#{resource_name}] Failed." exit 2 end def action_nothing # do nothing end def resource_type self.class.name.split("::").last.scan(/[A-Z][^A-Z]+/).map(&:downcase).join('_') end private alias_method :current, :current_attributes def run_action(action) runner.handler.event(:action, action: action) do original_attributes = @attributes # preserve and restore later @current_action = action clear_current_attributes Itamae.logger.debug "#{resource_type}[#{resource_name}] action: #{action}" return if action == :nothing Itamae.logger.with_indent_if(Itamae.logger.debug?) do Itamae.logger.debug "(in pre_action)" pre_action Itamae.logger.debug "(in set_current_attributes)" set_current_attributes Itamae.logger.debug "(in show_differences)" show_differences method_name = "action_#{action}" if runner.dry_run? unless respond_to?(method_name) Itamae.logger.error "action #{action.inspect} is unavailable" end else args = [method_name] if method(method_name).arity == 1 # for plugin compatibility args << runner.options end public_send(*args) end if different? updated! runner.handler.event(:attribute_changed, from: @current_attributes, to: @attributes) end end @current_action = nil @attributes = original_attributes end end def clear_current_attributes @current_attributes = Hashie::Mash.new end def pre_action # do nothing end def set_current_attributes # do nothing end def different? @current_attributes.each_pair.any? do |key, current_value| !current_value.nil? && !@attributes[key].nil? && current_value != @attributes[key] end end def show_differences @current_attributes.each_pair do |key, current_value| value = @attributes[key] if current_value.nil? && value.nil? # ignore elsif current_value.nil? && !value.nil? Itamae.logger.color :green do Itamae.logger.info "#{resource_type}[#{resource_name}] #{key} will be '#{value}'" end elsif current_value == value || value.nil? Itamae.logger.debug "#{resource_type}[#{resource_name}] #{key} will not change (current value is '#{current_value}')" else Itamae.logger.color :green do Itamae.logger.info "#{resource_type}[#{resource_name}] #{key} will change from '#{current_value}' to '#{value}'" end end end end def process_attributes self.class.defined_attributes.each_pair do |key, details| @attributes[key] ||= @resource_name if details[:default_name] @attributes[key] = details[:default] if details.has_key?(:default) && !@attributes.has_key?(key) if details[:required] && !@attributes[key] raise Resource::AttributeMissingError, "'#{key}' attribute is required but it is not set." end if @attributes[key] && details[:type] valid_type = [details[:type]].flatten.any? do |type| @attributes[key].is_a?(type) end unless valid_type raise Resource::InvalidTypeError, "#{key} attribute should be #{details[:type]}." end end end end def do_not_run_because_of_only_if? @only_if_command && run_command(@only_if_command, error: false).exit_status != 0 end def do_not_run_because_of_not_if? @not_if_command && run_command(@not_if_command, error: false).exit_status == 0 end def backend runner.backend end def runner recipe.runner end def node runner.node end def run_command(*args) unless args.last.is_a?(Hash) args << {} end args.last[:user] ||= attributes.user args.last[:cwd] ||= attributes.cwd backend.run_command(*args) end def check_command(*args) unless args.last.is_a?(Hash) args << {} end args.last[:error] = false run_command(*args).exit_status == 0 end def run_specinfra(type, *args) command = backend.get_command(type, *args) if type.to_s.start_with?("check_") check_command(command) else run_command(command) end end def shell_escape(str) str.shellescape end def updated! Itamae.logger.debug "This resource is updated." @updated = true end def updated? @updated end def notify (notifications + runner.children.subscribing(self)).each do |notification| message = "Notifying #{notification.action} to #{notification.action_resource.resource_type} resource '#{notification.action_resource.resource_name}'" if notification.delayed? message << " (delayed)" elsif notification.immediately? message << " (immediately)" end Itamae.logger.info message if notification.instance_of?(Subscription) Itamae.logger.info "(because it subscribes this resource)" end if notification.delayed? @recipe.delayed_notifications << notification elsif notification.immediately? notification.run end end end def verify return if @verify_commands.empty? Itamae.logger.info "Verifying..." Itamae.logger.with_indent do @verify_commands.each do |command| run_command(command) end end end end end end itamae-1.9.10/lib/itamae/resource/user.rb0000644000175000017500000000632613017534453017270 0ustar scottscottrequire 'itamae' module Itamae module Resource class User < Base define_attribute :action, default: :create define_attribute :username, type: String, default_name: true define_attribute :gid, type: [Integer, String] define_attribute :home, type: String define_attribute :password, type: String define_attribute :system_user, type: [TrueClass, FalseClass] define_attribute :uid, type: Integer define_attribute :shell, type: String define_attribute :create_home, type: [TrueClass, FalseClass], default: false def pre_action case @current_action when :create attributes.exist = true end if attributes.gid.is_a?(String) # convert name to gid attributes.gid = run_specinfra(:get_group_gid, attributes.gid).stdout.to_i end end def set_current_attributes current.exist = exist? if current.exist current.uid = run_specinfra(:get_user_uid, attributes.username).stdout.strip.to_i current.gid = run_specinfra(:get_user_gid, attributes.username).stdout.strip.to_i current.home = run_specinfra(:get_user_home_directory, attributes.username).stdout.strip current.shell = run_specinfra(:get_user_login_shell, attributes.username).stdout.strip current.password = current_password end end def action_create(options) if run_specinfra(:check_user_exists, attributes.username) if attributes.uid && attributes.uid != current.uid run_specinfra(:update_user_uid, attributes.username, attributes.uid) updated! end if attributes.gid && attributes.gid != current.gid run_specinfra(:update_user_gid, attributes.username, attributes.gid) updated! end if attributes.password && attributes.password != current.password run_specinfra(:update_user_encrypted_password, attributes.username, attributes.password) updated! end if attributes.home && attributes.home != current.home run_specinfra(:update_user_home_directory, attributes.username, attributes.home) updated! end if attributes.shell && attributes.shell != current.shell run_specinfra(:update_user_login_shell, attributes.username, attributes.shell) updated! end else options = { gid: attributes.gid, home_directory: attributes.home, password: attributes.password, system_user: attributes.system_user, uid: attributes.uid, shell: attributes.shell, create_home: attributes.create_home, } run_specinfra(:add_user, attributes.username, options) updated! end end private def exist? run_specinfra(:check_user_exists, attributes.username) end def current_password result = run_specinfra(:get_user_encrypted_password, attributes.username) if result.success? result.stdout.strip else nil end end end end end itamae-1.9.10/lib/itamae/resource/remote_file.rb0000644000175000017500000000241213017534453020574 0ustar scottscottrequire 'itamae' module Itamae module Resource class RemoteFile < File SourceNotFoundError = Class.new(StandardError) define_attribute :source, type: [String, Symbol], default: :auto private def content_file source_file end def source_file @source_file ||= find_source_file end def find_source_file if attributes.source == :auto dirs = attributes.path.split(::File::SEPARATOR) dirs.shift if dirs.first == "" searched_paths = [] dirs.size.times do |i| source_file_exts.each do |ext| path = ::File.join(@recipe.dir, source_file_dir, "#{dirs[i..-1].join("/")}#{ext}") if ::File.exist?(path) Itamae.logger.debug "#{path} is used as a source file." return path else searched_paths << path end end end raise SourceNotFoundError, "source file is not found (searched paths: #{searched_paths.join(', ')})" else ::File.expand_path(attributes.source, @recipe.dir) end end def source_file_dir "files" end def source_file_exts [""] end end end end itamae-1.9.10/lib/itamae/generators/0000755000175000017500000000000013017534453016300 5ustar scottscottitamae-1.9.10/lib/itamae/generators/templates/0000755000175000017500000000000013017534453020276 5ustar scottscottitamae-1.9.10/lib/itamae/generators/templates/project/0000755000175000017500000000000013017534453021744 5ustar scottscottitamae-1.9.10/lib/itamae/generators/templates/project/cookbooks/0000755000175000017500000000000013017534453023735 5ustar scottscottitamae-1.9.10/lib/itamae/generators/templates/project/cookbooks/.keep0000644000175000017500000000000013017534453024650 0ustar scottscottitamae-1.9.10/lib/itamae/generators/templates/project/Gemfile0000644000175000017500000000007713017534453023243 0ustar scottscottsource 'https://rubygems.org' gem 'itamae' # gem 'serverspec' itamae-1.9.10/lib/itamae/generators/templates/project/roles/0000755000175000017500000000000013017534453023070 5ustar scottscottitamae-1.9.10/lib/itamae/generators/templates/project/roles/.keep0000644000175000017500000000000013017534453024003 0ustar scottscottitamae-1.9.10/lib/itamae/generators/templates/cookbook/0000755000175000017500000000000013017534453022104 5ustar scottscottitamae-1.9.10/lib/itamae/generators/templates/cookbook/default.rb0000644000175000017500000000000013017534453024043 0ustar scottscottitamae-1.9.10/lib/itamae/generators/templates/cookbook/templates/0000755000175000017500000000000013017534453024102 5ustar scottscottitamae-1.9.10/lib/itamae/generators/templates/cookbook/templates/.keep0000644000175000017500000000000013017534453025015 0ustar scottscottitamae-1.9.10/lib/itamae/generators/templates/cookbook/files/0000755000175000017500000000000013017534453023206 5ustar scottscottitamae-1.9.10/lib/itamae/generators/templates/cookbook/files/.keep0000644000175000017500000000000013017534453024121 0ustar scottscottitamae-1.9.10/lib/itamae/generators/templates/role/0000755000175000017500000000000013017534453021237 5ustar scottscottitamae-1.9.10/lib/itamae/generators/templates/role/default.rb0000644000175000017500000000000013017534453023176 0ustar scottscottitamae-1.9.10/lib/itamae/generators/templates/role/templates/0000755000175000017500000000000013017534453023235 5ustar scottscottitamae-1.9.10/lib/itamae/generators/templates/role/templates/.keep0000644000175000017500000000000013017534453024150 0ustar scottscottitamae-1.9.10/lib/itamae/generators/templates/role/files/0000755000175000017500000000000013017534453022341 5ustar scottscottitamae-1.9.10/lib/itamae/generators/templates/role/files/.keep0000644000175000017500000000000013017534453023254 0ustar scottscottitamae-1.9.10/lib/itamae/generators/project.rb0000644000175000017500000000054013017534453020272 0ustar scottscottrequire 'thor' require 'thor/group' module Itamae module Generators class Project < Thor::Group include Thor::Actions def self.source_root File.dirname(__FILE__) + '/templates/project' end def copy_files directory '.' end def bundle run 'bundle install' end end end end itamae-1.9.10/lib/itamae/generators/role.rb0000644000175000017500000000054013017534453017565 0ustar scottscottrequire 'thor' require 'thor/group' module Itamae module Generators class Role < Thor::Group include Thor::Actions def self.source_root File.expand_path('../templates/role', __FILE__) end def copy_files directory '.' end def remove_files remove_file '.' end end end end itamae-1.9.10/lib/itamae/generators/cookbook.rb0000644000175000017500000000055013017534453020433 0ustar scottscottrequire 'thor' require 'thor/group' module Itamae module Generators class Cookbook < Thor::Group include Thor::Actions def self.source_root File.expand_path('../templates/cookbook', __FILE__) end def copy_files directory '.' end def remove_files remove_file '.' end end end end itamae-1.9.10/lib/itamae/definition.rb0000644000175000017500000000161713017534453016611 0ustar scottscottrequire 'itamae' module Itamae class Definition < Resource::Base class << self attr_accessor :definition_block attr_accessor :defined_in_recipe def create_class(name, params, defined_in_recipe, &block) Class.new(self).tap do |klass| klass.definition_block = block klass.defined_in_recipe = defined_in_recipe klass.define_attribute :action, default: :run params.each_pair do |key, value| klass.define_attribute key.to_sym, type: Object, default: value end end end end def initialize(*args) super r = Recipe::RecipeFromDefinition.new( runner, self.class.defined_in_recipe.path, ) recipe.children << r r.definition = self r.load(params: @attributes.merge(name: resource_name)) end def run(*args) # nothing end end end itamae-1.9.10/lib/itamae/runner.rb0000644000175000017500000000624013017534453015767 0ustar scottscottrequire 'itamae' require 'json' require 'yaml' module Itamae class Runner class << self def run(recipe_files, backend_type, options) Itamae.logger.info "Starting Itamae..." backend = Backend.create(backend_type, options) runner = self.new(backend, options) runner.load_recipes(recipe_files) runner.run runner end end attr_reader :backend attr_reader :options attr_reader :node attr_reader :tmpdir attr_reader :children attr_reader :handler def initialize(backend, options) @backend = backend @options = options prepare_handler @node = create_node @tmpdir = "/tmp/itamae_tmp" @children = RecipeChildren.new @diff = false @backend.run_command(["mkdir", "-p", @tmpdir]) @backend.run_command(["chmod", "777", @tmpdir]) end def load_recipes(paths) paths.each do |path| expanded_path = File.expand_path(path) if path.include?('::') gem_path = Recipe.find_recipe_in_gem(path) expanded_path = gem_path if gem_path end recipe = Recipe.new(self, expanded_path) children << recipe recipe.load end end def run if recipe_graph_file = options[:recipe_graph] save_dependency_graph(recipe_graph_file) end children.run @backend.finalize if profile = options[:profile] save_profile(profile) end end def dry_run? @options[:dry_run] end def save_dependency_graph(path) Itamae.logger.info "Writing recipe dependency graph to #{path}..." open(path, 'w') do |f| f.write(children.dependency_in_dot) end end def save_profile(path) open(path, 'w', 0600) do |f| f.write(@backend.executed_commands.to_json) end end def diff? @diff end def diff_found! @diff = true end private def create_node hash = {} if @options[:ohai] unless @backend.run_command("which ohai", error: false).exit_status == 0 # install Ohai Itamae.logger.info "Installing Chef package... (to use Ohai)" @backend.run_command("curl -L https://www.opscode.com/chef/install.sh | bash") end Itamae.logger.info "Loading node data via ohai..." hash.merge!(JSON.parse(@backend.run_command("ohai").stdout)) end if @options[:node_json] path = File.expand_path(@options[:node_json]) Itamae.logger.info "Loading node data from #{path}..." hash.merge!(JSON.load(open(path))) end if @options[:node_yaml] path = File.expand_path(@options[:node_yaml]) Itamae.logger.info "Loading node data from #{path}..." hash.merge!(YAML.load(open(path)) || {}) end Node.new(hash, @backend) end def prepare_handler @handler = HandlerProxy.new (@options[:handlers] || []).each do |handler| type = handler.delete('type') unless type raise "#{type} field is not set" end @handler.register_instance(Handler.from_type(type).new(handler)) end end end end itamae-1.9.10/lib/itamae/version.rb0000644000175000017500000000004713017534453016142 0ustar scottscottmodule Itamae VERSION = "1.9.10" end