├── .gitignore ├── .rspec ├── .travis.yml ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── TODO.md ├── infra_operator.gemspec ├── lib ├── infra_operator.rb └── infra_operator │ ├── backends │ ├── base.rb │ ├── exec.rb │ └── native.rb │ ├── command_result.rb │ ├── commands │ ├── base.rb │ ├── ruby.rb │ └── shell.rb │ ├── host.rb │ ├── platforms │ ├── base.rb │ └── osx │ │ └── common.rb │ ├── providers │ ├── base.rb │ ├── cron │ │ └── common.rb │ └── file │ │ └── bsd.rb │ ├── service_proxy.rb │ ├── specinfra1_compat │ └── command_result.rb │ ├── utils │ └── shell_builder.rb │ └── version.rb ├── script ├── console └── setup └── spec ├── backends └── exec_spec.rb ├── host_spec.rb ├── platforms └── base_spec.rb ├── service_proxy_spec.rb ├── spec_helper.rb └── utils └── shell_builder_spec.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 1.8.7 4 | - 1.9.3 5 | - 2.0 6 | - 2.1 7 | - 2.2 8 | before_install: gem install bundler -v 1.10.3 9 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in infra_operator.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Shota Fukumori (sora_h) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # InfraOperator 2 | 3 | (important note: still in early development phase) 4 | 5 | InfraOperator provides unified interface to do something on servers, with various platform. It provides the following: 6 | 7 | - __backends:__ Execute commands and return results. Execution method may vary (via SSH, via exec(3), etc...). But provides same interface for all. 8 | - __command generators:__ Generate suitable shell scripts for target to do something. 9 | - __inventory:__ Collects host's informations and metrics. It may use command generator (described above) to collect informations. 10 | 11 | Also, InfraOperator provides compatible API for [SpecInfra](https://github.com/serverspec/specinfra). My goal is to replace SpecInfra with InfraOperator implementation. 12 | 13 | ## Usage 14 | 15 | TBD 16 | 17 | ## Installation 18 | 19 | Add this line to your application's Gemfile: 20 | 21 | ```ruby 22 | gem 'infra_operator' 23 | ``` 24 | 25 | And then execute: 26 | 27 | $ bundle 28 | 29 | Or install it yourself as: 30 | 31 | $ gem install infra_operator 32 | 33 | ## Development 34 | 35 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake rspec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 36 | 37 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 38 | 39 | ### Terminologies 40 | 41 | - detector: 42 | - command: represents command or something like shell script. may transform into single String, or may be executed directly. 43 | - action: 44 | - command generator: 45 | - environment: where target host runs (e.g. virtualized, on some IaaS, or baremetal ...) 46 | - platform: what target host runs (e.g. OS, Distributon, ...); has many providers 47 | - platform variant: variant of platform (e.g. same platform but based on systemd, or upstart / differ on version) 48 | - provider: provides actions. 49 | 50 | ## Contributing 51 | 52 | Bug reports and pull requests are welcome on GitHub at https://github.com/sorah/infra_operator. 53 | 54 | ## License 55 | 56 | The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). 57 | 58 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :default => :spec 7 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | ## backends 2 | 3 | - [ ] base 4 | - [ ] cmd 5 | - [ ] docker 6 | - [ ] dockerfile 7 | - [x] exec 8 | - [x] native 9 | - [ ] lxc 10 | - [ ] powershell 11 | - [ ] shell_script 12 | - [ ] ssh 13 | - [ ] telnet 14 | - [ ] winrm 15 | 16 | ## providers 17 | 18 | - [ ] file: 19 | - [ ] native (ruby) 20 | - [ ] cron: 21 | - [ ] bond: 22 | - [ ] bridge: 23 | - [ ] user: 24 | - [ ] native (ruby) 25 | - [ ] group: 26 | - [ ] native (ruby) 27 | - [ ] service: 28 | - [ ] sysv 29 | - [ ] upstart 30 | - [ ] systemd 31 | - [ ] openrc 32 | - [ ] package: 33 | - [ ] firewall: 34 | - [ ] iptables, ip6tables 35 | - [ ] ipfilter 36 | - [ ] ipfw 37 | - [ ] route: 38 | - [ ] process: 39 | - [ ] port: 40 | - [ ] network_interfaces: 41 | - [ ] host: 42 | - [ ] mount: 43 | 44 | ---- 45 | 46 | misc 47 | 48 | - [ ] ipnat 49 | - [ ] selinux 50 | - [ ] selinux_module 51 | - [ ] zfs 52 | - [ ] yumrepo 53 | - [ ] ppa 54 | - [ ] mail_alias 55 | - [ ] lxc_container 56 | - [ ] localhost (?) 57 | - [ ] kernel_module 58 | - [ ] fstab 59 | 60 | ## platform 61 | 62 | - [ ] linux 63 | - [ ] alpine 64 | - [ ] arch 65 | - [ ] coreos 66 | - [ ] cumulus 67 | - [ ] debian 68 | - [ ] upstart 69 | - [ ] systemd 70 | - [ ] ubuntu 71 | - [ ] upstart 72 | - [ ] systemd 73 | - [ ] suse 74 | - [ ] opensuse 75 | - [ ] redhat 76 | - [ ] centos 77 | - [ ] amazon 78 | - [ ] v5 79 | - [ ] v7 80 | - [ ] fedora 81 | - [ ] sysv 82 | - [ ] systemd 83 | - [ ] gentoo 84 | - [ ] sysv 85 | - [ ] systemd 86 | - [ ] plamo 87 | - [ ] nixos 88 | - [ ] bsd 89 | - [ ] freebsd 90 | - [ ] v10 91 | - [ ] v6 92 | - [ ] darwin 93 | - [ ] osx 94 | - [ ] openbsd 95 | - [ ] solaris 96 | - [ ] smartos 97 | - [ ] windows 98 | - [ ] aix 99 | - [ ] esxi 100 | 101 | ## environment 102 | 103 | - [ ] amazon ec2 104 | - [ ] google compute engine 105 | - [ ] azure vm 106 | - [ ] xen 107 | - [ ] lxc 108 | - [ ] docker 109 | - [ ] vmware 110 | - [ ] kvm 111 | - [ ] baremetal 112 | 113 | ## inventory 114 | 115 | - [ ] base 116 | - [ ] cpu 117 | - [ ] domain 118 | - [ ] ec2 119 | - [ ] filesystem 120 | - [ ] fqdn 121 | - [ ] hostname 122 | - [ ] kernel 123 | - [ ] memory 124 | - [ ] platform 125 | - [ ] platform_version 126 | - [ ] virtualization 127 | 128 | ## misc 129 | 130 | - [ ] config 131 | - [ ] pre_command 132 | - [ ] shell 133 | - [ ] path 134 | - [ ] stdout_handler, stderr_handler 135 | -------------------------------------------------------------------------------- /infra_operator.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'infra_operator/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "infra_operator" 8 | spec.version = InfraOperator::VERSION 9 | spec.authors = ["Shota Fukumori (sora_h)"] 10 | spec.email = ["her@sorah.jp"] 11 | 12 | spec.summary = %q{Operator} 13 | spec.homepage = "https://github.com/sorah/infra_operator" 14 | spec.license = "MIT" 15 | 16 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 17 | spec.bindir = "bin" 18 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 19 | spec.require_paths = ["lib"] 20 | 21 | spec.add_dependency 'sfl' # spawn-for-legacy 22 | 23 | spec.add_development_dependency "bundler" 24 | spec.add_development_dependency "rake" 25 | spec.add_development_dependency "rspec" 26 | end 27 | -------------------------------------------------------------------------------- /lib/infra_operator.rb: -------------------------------------------------------------------------------- 1 | require "infra_operator/version" 2 | 3 | module InfraOperator 4 | # Your code goes here... 5 | end 6 | -------------------------------------------------------------------------------- /lib/infra_operator/backends/base.rb: -------------------------------------------------------------------------------- 1 | module InfraOperator 2 | module Backends 3 | class Base 4 | def initialize(options = {}) 5 | @options = {} 6 | end 7 | 8 | def self.native? 9 | raise NotImplementedError 10 | end 11 | 12 | def execute_script!(script) 13 | raise NotImplementedError 14 | end 15 | 16 | def upload(src, dest) 17 | raise NotImplementedError 18 | end 19 | 20 | def upload_directory(src, dest) 21 | raise NotImplementedError 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/infra_operator/backends/exec.rb: -------------------------------------------------------------------------------- 1 | require 'infra_operator/backends/base' 2 | require 'infra_operator/command_result' 3 | 4 | require 'fileutils' 5 | require 'sfl' 6 | 7 | module InfraOperator 8 | module Backends 9 | class Exec < Base 10 | READ_CANCEL_EXCEPTIONS = if defined? IO::ReadWaitable 11 | [EOFError, IO::ReadWaitable] 12 | else 13 | [EOFError, Errno::EWOULDBLOCK, Errno::EAGAIN, Errno::EINTR] 14 | end 15 | 16 | def self.native? 17 | # Native flag is turned on on Native backend 18 | false 19 | end 20 | 21 | def self.shell? 22 | true 23 | end 24 | 25 | def execute_script!(script) 26 | # Assume you're surprised why this method is doing complex stuff -- 27 | # we may execute scripts that starts daemon, and they may not close 28 | # stdout, stderr. In such situations, execute_script! will be blocked 29 | # forever because we read output using simple IO#read. The following 30 | # lines waits single process we spawned, and read output progressively 31 | # while the process alives. 32 | 33 | stdout, stderr = '', '' 34 | 35 | quit_r, quit_w = IO.pipe 36 | out_r, out_w = IO.pipe 37 | err_r, err_w = IO.pipe 38 | 39 | th = Thread.new do 40 | begin 41 | terminate = false 42 | 43 | loop do 44 | break if terminate 45 | readable_ios, = IO.select([quit_r, out_r, err_r]) 46 | 47 | if readable_ios.include?(quit_r) 48 | terminate = true 49 | end 50 | 51 | if readable_ios.include?(out_r) 52 | begin 53 | while out = out_r.read_nonblock(4096) 54 | stdout += out 55 | end 56 | rescue *READ_CANCEL_EXCEPTIONS 57 | end 58 | end 59 | 60 | if readable_ios.include?(err_r) 61 | begin 62 | while err = err_r.read_nonblock(4096) 63 | stderr += err 64 | end 65 | rescue *READ_CANCEL_EXCEPTIONS 66 | end 67 | end 68 | end 69 | ensure 70 | quit_r.close unless quit_r.closed? 71 | out_r.close unless out_r.closed? 72 | err_r.close unless err_r.closed? 73 | end 74 | end 75 | 76 | th.abort_on_exception = true 77 | 78 | pid = spawn(env, script, :out => out_w, :err => err_w, :unsetenv_others => true) 79 | 80 | out_w.close 81 | err_w.close 82 | 83 | pid, stat = Process.waitpid2(pid) 84 | 85 | begin 86 | quit_w.syswrite 1 87 | rescue Errno::EPIPE 88 | end 89 | 90 | th.join(2) 91 | th.kill if th.alive? 92 | 93 | CommandResult.new(:status => stat, :stdout => stdout, :stderr => stderr) 94 | ensure 95 | quit_w.close unless quit_w.closed? 96 | end 97 | 98 | def upload(src, dest) 99 | FileUtils.cp(src, dest) 100 | end 101 | 102 | def upload_directory(src, dest) 103 | FileUtils.cp_r(src, dest) 104 | end 105 | 106 | private 107 | 108 | ENV_NAMES_TO_EXCLUDE = %w[BUNDLER_EDITOR BUNDLE_BIN_PATH BUNDLE_GEMFILE RUBYOPT GEM_HOME GEM_PATH GEM_CACHE] 109 | 110 | def env 111 | env = {} 112 | ENV.each do |k,v| 113 | next if ENV_NAMES_TO_EXCLUDE.include?(k) 114 | env[k] = v 115 | end 116 | env 117 | end 118 | end 119 | end 120 | end 121 | -------------------------------------------------------------------------------- /lib/infra_operator/backends/native.rb: -------------------------------------------------------------------------------- 1 | require 'infra_operator/backends/exec' 2 | 3 | module InfraOperator 4 | module Backends 5 | class Native < Exec 6 | def self.native? 7 | true 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/infra_operator/command_result.rb: -------------------------------------------------------------------------------- 1 | module InfraOperator 2 | class CommandResult 3 | class UnsuccessfulError < StandardError; end 4 | 5 | def initialize(properties = {}) 6 | @properties = properties 7 | end 8 | 9 | def output 10 | @properties[:output].to_s || stdout 11 | end 12 | 13 | def stdout 14 | @properties[:stdout] 15 | end 16 | 17 | def stderr 18 | @properties[:stderr] 19 | end 20 | 21 | def status 22 | @properties[:status] 23 | end 24 | 25 | def pid 26 | refer_stat_or_property(:pid) 27 | end 28 | 29 | def exitstatus 30 | refer_stat_or_property(:exitstatus) 31 | end 32 | 33 | def signal 34 | @properties[:signal] || termsig || stopsig 35 | end 36 | 37 | def stopsig 38 | refer_stat_or_property(:stopsig) 39 | end 40 | 41 | def termsig 42 | refer_stat_or_property(:termsig) 43 | end 44 | 45 | def success? 46 | !error && (@properties[:success] || (exitstatus == 0)) 47 | end 48 | 49 | def signaled? 50 | status ? status.signaled? : !!signal 51 | end 52 | 53 | def exited? 54 | status ? status.exited? : @properties[:exited] 55 | end 56 | 57 | def coredump? 58 | status ? status.coredump? : @properties[:coredump] 59 | end 60 | 61 | def error 62 | @properties[:error] 63 | end 64 | 65 | def value 66 | case 67 | when error 68 | raise error 69 | when !success? 70 | raise InfraOperator::CommandResult::UnsuccessfulError 71 | end 72 | 73 | self 74 | end 75 | 76 | private 77 | 78 | def refer_stat_or_property(name) 79 | status ? status.__send__(name) : @properties[name] 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /lib/infra_operator/commands/base.rb: -------------------------------------------------------------------------------- 1 | require 'infra_operator/specinfra1_compat/command_result' 2 | 3 | module InfraOperator 4 | module Commands 5 | class Base 6 | class BackendIncompatibleError < StandardError; end 7 | 8 | def initialize(options = {}, &block) 9 | @options = options 10 | @block = block 11 | end 12 | 13 | def compatible?(backend) 14 | raise NotImplementedError 15 | end 16 | 17 | # Compile command to string. May be unsupported on some subclass 18 | def to_s 19 | raise NotImplementedError 20 | end 21 | 22 | # Specify processor block to tranform raw CommandResult. Passed block will be used on execute method 23 | def process(&block) 24 | @processor = block 25 | self 26 | end 27 | 28 | # Specify block to transform processor result for specinfra v1 API. Block should return CommandResult. 29 | def process_specinfra1(&block) 30 | @specinfra1_processor = block 31 | self 32 | end 33 | 34 | def execute_specinfra1(backend) 35 | if @specinfra1_processor 36 | Specinfra1Compat::CommandResult.new @specinfra1_processor.call(execute(backend)) 37 | else 38 | Specinfra1Compat::CommandResult.new execute(backend, :raw => true) 39 | end 40 | end 41 | 42 | def execute(backend, options = {}) 43 | unless self.compatible?(backend) 44 | raise BackendIncompatibleError 45 | end 46 | 47 | command_result = execute!(backend) 48 | if @processor && !options[:raw] 49 | @processor.call(command_result) 50 | else 51 | command_result 52 | end 53 | end 54 | 55 | # Execute command. 56 | def execute!(backend) 57 | raise NotImplementedError 58 | end 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/infra_operator/commands/ruby.rb: -------------------------------------------------------------------------------- 1 | require 'infra_operator/commands/base' 2 | require 'infra_operator/command_result' 3 | 4 | module InfraOperator 5 | module Commands 6 | class Ruby < Base 7 | def compatible?(backend) 8 | backend.native? 9 | end 10 | 11 | def execute!(backend) 12 | begin 13 | output = @block.call(backend) 14 | rescue Exception => e 15 | return CommandResult.new(:error => e) 16 | end 17 | 18 | CommandResult.new(:output => output) 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/infra_operator/commands/shell.rb: -------------------------------------------------------------------------------- 1 | require 'infra_operator/commands/base' 2 | require 'infra_operator/utils/shell_builder' 3 | 4 | module InfraOperator 5 | module Commands 6 | class Shell < Base 7 | def initialize(*) 8 | super 9 | raise ArgumentError, 'block must be given' unless block_given? 10 | @script = Utils::ShellBuilder.new(&@block) 11 | end 12 | 13 | def compatible?(backend) 14 | backend.class.shell? 15 | end 16 | 17 | def to_s 18 | @script.to_s 19 | end 20 | 21 | def execute!(backend) 22 | backend.execute_script!(self.to_s) 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/infra_operator/host.rb: -------------------------------------------------------------------------------- 1 | require 'infra_operator/backends/native' 2 | require 'infra_operator/platforms/base' 3 | require 'infra_operator/service_proxy' 4 | 5 | module InfraOperator 6 | class Host 7 | def initialize(options = {}) 8 | @platform = options[:platform] 9 | @backend = options[:backend] 10 | 11 | # instantiate 12 | @platform = @platform.new if @platform.kind_of?(Class) 13 | @backend = @backend.new if @backend.kind_of?(Class) 14 | end 15 | 16 | def platform 17 | @platform ||= nil # TODO: 18 | end 19 | 20 | def backend 21 | @backend ||= InfraOperator::Backends::Native.new 22 | end 23 | 24 | def service(id) 25 | retried = false 26 | begin 27 | svc = @platform.service(id) 28 | rescue InfraOperator::Platforms::Base::NotYetDetermined 29 | raise if retried 30 | 31 | @platform.determine_provider!(id, backend) 32 | 33 | retried = true 34 | retry 35 | end 36 | 37 | if svc 38 | ServiceProxy.new(backend, svc) 39 | else 40 | nil 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/infra_operator/platforms/base.rb: -------------------------------------------------------------------------------- 1 | module InfraOperator 2 | module Platforms 3 | class Base 4 | class BackendRequiredToDetermine < StandardError; end 5 | class NotYetDetermined < StandardError; end 6 | 7 | def initialize(options={}) 8 | @options = options 9 | @services = {} 10 | @service_classes = self.class.services.dup 11 | 12 | override_services! 13 | end 14 | 15 | attr_reader :options 16 | 17 | def self.services 18 | @services ||= {} 19 | end 20 | 21 | def self.provides(service, provider) 22 | services[service] = self.resolve_provider_class(service, provider) 23 | end 24 | 25 | def self.resolve_provider_class(service, provider) 26 | case provider 27 | when Class 28 | provider 29 | when Symbol, String 30 | retried = false 31 | begin 32 | provider_const_name = provider.to_s.capitalize.gsub(/_./) { |_| _[1].upcase } 33 | service_const_name = service.to_s.capitalize.gsub(/_./) { |_| _[1].upcase } 34 | InfraOperator::Providers.const_get(service_const_name).const_get(provider_const_name) 35 | rescue NameError 36 | raise if retried 37 | require "infra_operator/providers/#{service}/#{provider}" 38 | retried = true 39 | retry 40 | end 41 | when Array 42 | provider.map { |_| resolve_provider_class(service, _) } 43 | end 44 | end 45 | 46 | def service(id) 47 | case 48 | when @services.key?(id) 49 | return @services[id] 50 | when @service_classes.key?(id) 51 | begin 52 | determine_provider!(id) 53 | @services[id] 54 | rescue BackendRequiredToDetermine 55 | raise NotYetDetermined 56 | end 57 | else 58 | nil 59 | end 60 | end 61 | 62 | def provides?(id) 63 | @service_classes.key? id 64 | end 65 | 66 | def determine_providers!(backend) 67 | @service_classes.each_key do |id| 68 | determine_provider! id, backend 69 | end 70 | end 71 | 72 | def determine_provider!(id, backend = nil) 73 | return if @services[id] 74 | return unless @service_classes[id] 75 | 76 | candidate = @service_classes[id] 77 | case candidate 78 | when Class 79 | @services[id] = candidate.new 80 | when Proc 81 | raise BackendRequiredToDetermine unless backend 82 | @services[id] = candidate.call(self, backend) 83 | when Array 84 | raise BackendRequiredToDetermine unless backend 85 | candidate.each do |_| 86 | if _.suitable?(backend) 87 | @services[id] = _.new 88 | break 89 | end 90 | end 91 | else 92 | raise TypeError 93 | end 94 | end 95 | 96 | private 97 | 98 | def override_services! 99 | spec = options[:services] || {} 100 | 101 | spec.each do |k, v| 102 | @service_classes[k] = self.class.resolve_provider_class(k, v) 103 | end 104 | end 105 | end 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /lib/infra_operator/platforms/osx/common.rb: -------------------------------------------------------------------------------- 1 | require 'infra_operator/platforms/base' 2 | 3 | module InfraOperator 4 | module Platforms 5 | module Osx 6 | class Common < Base 7 | provides :file, :bsd 8 | provides :cron, :common 9 | # provides :user, :bsd 10 | # provides :group, :bsd 11 | # provides :service, :launchd 12 | # provides :package, :osx 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/infra_operator/providers/base.rb: -------------------------------------------------------------------------------- 1 | module InfraOperator 2 | module Providers 3 | class Base 4 | def initialize(options={}) 5 | @options = options 6 | end 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/infra_operator/providers/cron/common.rb: -------------------------------------------------------------------------------- 1 | require 'infra_operator/providers/base' 2 | require 'infra_operator/commands/shell' 3 | require 'infra_operator/command_result' 4 | 5 | module InfraOperator 6 | module Providers 7 | module Cron 8 | class Common < Base 9 | def entry_defined?(entry, options = {}) 10 | user = options[:user] 11 | user_opt = user ? ['-u', user] : [] 12 | 13 | Commands::Shell.new do 14 | run "crontab", "-l", *user_opt 15 | end.process do |stat| 16 | stat.value.stdout.each_line.map(&:chomp).any? { |e| e == entry.chomp } 17 | end.process_specinfra1 do |stat| 18 | stat 19 | end 20 | end 21 | 22 | def check_has_entry(user, entry) # specinfra1 compat 23 | entry_defined?(entry, :user => user) 24 | end 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/infra_operator/providers/file/bsd.rb: -------------------------------------------------------------------------------- 1 | require 'infra_operator/providers/base' 2 | require 'infra_operator/commands/shell' 3 | require 'infra_operator/command_result' 4 | 5 | module InfraOperator 6 | module Providers 7 | module File 8 | class Bsd < Base 9 | def is_accessible_by_user?(file, user, access) 10 | Commands::Shell.new do 11 | run 'sudo', '-u', user, '-s', '/bin/test', "-#{access}", file 12 | end.process do |stat| 13 | stat.success? 14 | end 15 | end 16 | 17 | alias check_is_accessible_by_user is_accessible_by_user? 18 | 19 | def md5sum(file) 20 | Commands::Shell.new do 21 | pipe do 22 | run 'openssl', 'md5', file 23 | end 24 | end.process do |stat| 25 | stat.value.stdout.chomp[-32..-1] 26 | end.process_specinfra1 do |sum| 27 | sum 28 | end 29 | end 30 | 31 | alias get_md5sum md5sum 32 | 33 | def sha256sum(file) 34 | Commands::Shell.new do 35 | pipe do 36 | run 'openssl', 'dgst', '-sha256', file 37 | end 38 | end.process do |stat| 39 | stat.value.stdout.chomp[-64..-1] 40 | end.process_specinfra1 do |sum| 41 | sum 42 | end 43 | end 44 | 45 | alias get_sha256sum sha256sum 46 | 47 | def is_linked_to?(link, target) 48 | Commands::Shell.new do 49 | run "stat", "-f", "%Y", link 50 | end.process do |stat| 51 | stat.value.stdout.chomp == target 52 | end.process_specinfra1 do |result| 53 | result 54 | end 55 | end 56 | 57 | alias check_is_linked_to is_linked_to? 58 | 59 | def has_mode?(file, mode) 60 | Commands::Shell.new do 61 | run "stat", "-f", "%p", file 62 | end.process do |stat| 63 | stat.value.stdout.chomp[-4..-1] == mode.rjust(4, '0') 64 | end.process_specinfra1 do |result| 65 | result 66 | end 67 | end 68 | 69 | alias check_has_mode has_mode? 70 | 71 | def is_owned_by?(file, owner) 72 | Commands::Shell.new do 73 | run "stat", "-f", "%Su", file 74 | end.process do |stat| 75 | stat.value.stdout.chomp == owner 76 | end.process_specinfra1 do |result| 77 | result 78 | end 79 | end 80 | 81 | alias check_is_owned_by is_owned_by? 82 | 83 | def is_owned_by_group?(file, owner) # XXX: 84 | Commands::Shell.new do 85 | run "stat", "-f", "%Sg", file 86 | end.process do |stat| 87 | stat.value.stdout.chomp == owner 88 | end.process_specinfra1 do |result| 89 | result 90 | end 91 | end 92 | 93 | alias check_is_owned_by is_owned_by_group? 94 | 95 | def mode(file) 96 | Commands::Shell.new do 97 | run "stat", "-f", "%p", file 98 | end.process do |stat| 99 | stat.value.stdout.chomp[-4..-1] 100 | end.process_specinfra1 do |fourdigit_mode| 101 | fourdigit_mode[-3..-1] 102 | end 103 | end 104 | 105 | alias get_mode mode 106 | 107 | def owner(file) 108 | Commands::Shell.new do 109 | pipe do 110 | run "stat", "-f", "%Su", file 111 | end 112 | end.process do |stat| 113 | stat.value.stdout.chomp 114 | end 115 | end 116 | 117 | alias get_owner owner 118 | 119 | def group(file) 120 | Commands::Shell.new do 121 | pipe do 122 | run "stat", "-f", "%Sg", file 123 | end 124 | end.process do |stat| 125 | stat.value.stdout.chomp 126 | end 127 | end 128 | 129 | alias get_group owner 130 | end 131 | end 132 | end 133 | end 134 | -------------------------------------------------------------------------------- /lib/infra_operator/service_proxy.rb: -------------------------------------------------------------------------------- 1 | module InfraOperator 2 | class ServiceProxy 3 | def initialize(backend, service) 4 | @backend = backend 5 | @service = service 6 | end 7 | 8 | attr_reader :backend, :service 9 | 10 | def method_missing(meth, *args) 11 | command = service.__send__(meth, *args) 12 | command.execute backend 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/infra_operator/specinfra1_compat/command_result.rb: -------------------------------------------------------------------------------- 1 | require 'infra_operator/command_result' 2 | module InfraOperator 3 | module Specinfra1Compat 4 | class CommandResult 5 | attr_reader :stdout, :stderr, :exit_status, :exit_signal 6 | def initialize(arg= {}) 7 | case arg 8 | when InfraOperator::CommandResult 9 | options = arg 10 | 11 | @stdout = options.stdout 12 | @stderr = options.stderr 13 | @exit_status = options.exitstatus 14 | @exit_signal = options.signal 15 | when String 16 | @stdout = arg 17 | @stderr = '' 18 | @exit_status = 0 19 | @exit_signal = nil 20 | when TrueClass, FalseClass 21 | @stdout = '' 22 | @stderr = '' 23 | @exit_status = arg ? 0 : 1 24 | @exit_signal = nil 25 | when Hash 26 | @stdout = options[:stdout] || '' 27 | @stderr = options[:stderr] || '' 28 | @exit_status = options[:exitstatus] || 0 29 | @exit_signal = options[:exitsignal] 30 | else 31 | raise ArgumentError 32 | end 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/infra_operator/utils/shell_builder.rb: -------------------------------------------------------------------------------- 1 | require 'shellwords' 2 | 3 | module InfraOperator 4 | module Utils 5 | class ShellBuilder 6 | def initialize(&block) 7 | @block = block 8 | end 9 | 10 | def to_s 11 | Context.new(&@block).to_s 12 | end 13 | 14 | class Context 15 | def initialize(&block) 16 | @block = block 17 | @list = [] 18 | instance_eval &@block 19 | end 20 | 21 | attr_reader :list 22 | 23 | def to_s 24 | @list.map(&:to_s).join("; ") 25 | end 26 | 27 | def var(*args) 28 | @list << Variable.new(*args) 29 | end 30 | 31 | def export(*args) 32 | @list << Variable.new(*args).export 33 | end 34 | 35 | def cd(*args) 36 | @list << Chdir.new(*args) 37 | end 38 | 39 | def run(*args) 40 | @list << Run.new(*args) 41 | end 42 | 43 | def pipe(&block) 44 | @list << Context.new(&block).list.map(&:to_s).join(' | ') 45 | end 46 | 47 | def subshell(&block) 48 | @list << [ 49 | '(', 50 | Context.new(&block).list.map(&:to_s).join('; '), 51 | ')', 52 | ].join(' ') 53 | end 54 | 55 | def with_and(&block) 56 | @list << Context.new(&block).list.map(&:to_s).join(' && ') 57 | end 58 | 59 | def with_or(&block) 60 | @list << Context.new(&block).list.map(&:to_s).join(' || ') 61 | end 62 | end 63 | 64 | class Run 65 | def initialize(*args) 66 | @args = args 67 | @options = args.last.kind_of?(Hash) ? args.pop : {} 68 | @args.flatten! 69 | end 70 | 71 | def command 72 | @args.map { |_| Shellwords.escape(_) } 73 | end 74 | 75 | def redirect 76 | redirects = @options.map do |k,v| 77 | next unless k.kind_of?(Integer) || k == :out || k == :err || k == :in 78 | make_redirection(k, v) 79 | end.compact 80 | 81 | redirects.empty? ? nil : redirects.join(' ') 82 | end 83 | 84 | def to_s 85 | [*command, *redirect].join(" ") 86 | end 87 | 88 | private 89 | 90 | def make_redirection(k, v) 91 | fd = {:in => '', :out => '', :err => 2}[k] || k 92 | 93 | direction = k == :in ? '<' : '>' 94 | 95 | if v.kind_of?(Array) 96 | v, orig_v = v.dup, v 97 | if v.first == :read 98 | v.shift 99 | direction = '<' 100 | end 101 | 102 | if v.first == :rw 103 | v.shift 104 | direction = '<>' 105 | end 106 | 107 | if v.first == :append 108 | v.shift 109 | direction = '>>' 110 | end 111 | 112 | unless v.size == 1 113 | raise ArgumentError, "invalid redirect (#{k.inspect} => #{orig_v.inspect})" 114 | end 115 | 116 | v = v.first 117 | end 118 | 119 | if v.kind_of?(Integer) 120 | dest = "&#{v}" 121 | else 122 | dest = Shellwords.escape(v.to_s) 123 | end 124 | 125 | [fd, direction, dest].join 126 | end 127 | end 128 | 129 | class Chdir 130 | def initialize(destination) 131 | @destination = destination 132 | end 133 | 134 | def to_s 135 | "cd #{Shellwords.escape(@destination)}" 136 | end 137 | end 138 | 139 | class Variable 140 | def initialize(variables = {}) 141 | @variables = variables 142 | @export = variables.delete(:export) 143 | end 144 | 145 | def export 146 | self.class.new(@variables.merge(:export => true)) 147 | end 148 | 149 | def escaped_variable_definitions 150 | @variables.map { |k, v| "#{@export ? 'export ' : nil}#{Shellwords.shellescape(k)}=#{Shellwords.shellescape(v)}" } 151 | end 152 | 153 | def to_s 154 | escaped_variable_definitions.join("; ") 155 | end 156 | end 157 | end 158 | end 159 | end 160 | -------------------------------------------------------------------------------- /lib/infra_operator/version.rb: -------------------------------------------------------------------------------- 1 | module InfraOperator 2 | VERSION = "0.1.0" 3 | end 4 | -------------------------------------------------------------------------------- /script/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "infra_operator" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start 15 | -------------------------------------------------------------------------------- /script/setup: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | 5 | bundle install 6 | 7 | # Do any other automated setup that you need to do here 8 | -------------------------------------------------------------------------------- /spec/backends/exec_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'infra_operator/backends/exec' 3 | require 'infra_operator/command_result' 4 | 5 | RSpec.describe InfraOperator::Backends::Exec do 6 | subject(:backend) { described_class.new } 7 | 8 | describe "#execute_script!" do 9 | context do 10 | subject(:result) { backend.execute_script!('echo hi; echo err 1>&2') } 11 | 12 | it { is_expected.to be_a_kind_of(InfraOperator::CommandResult) } 13 | it { is_expected.to be_success } 14 | 15 | it "passes Process::Status" do 16 | expect(result.status).to be_a_kind_of(Process::Status) 17 | end 18 | 19 | it "captures output" do 20 | expect(result.stdout).to eq("hi\n") 21 | expect(result.stderr).to eq("err\n") 22 | end 23 | end 24 | 25 | context "when command exited with non-zero status" do 26 | subject(:result) { backend.execute_script!('exit 1') } 27 | 28 | it { is_expected.to be_a_kind_of(InfraOperator::CommandResult) } 29 | it { is_expected.not_to be_success } 30 | 31 | it "has exitstatus" do 32 | expect(result.exitstatus).to eq 1 33 | end 34 | end 35 | 36 | context "when executed process launches child process like a daemon, and the daemon doesn't close stdout,err" do 37 | subject(:result) { backend.execute_script!("ruby -e 'pid = fork { sleep 10; puts :bye }; Process.detach(pid); puts pid'") } 38 | 39 | it "doesn't block" do 40 | a = Time.now 41 | result # exec 42 | b = Time.now 43 | expect((b-a) < 3).to be_truthy 44 | 45 | expect(result.stderr).to be_empty 46 | expect(result.stdout.chomp).to match(/\A\d+\z/) 47 | Process.kill :TERM, result.stdout.chomp.to_i 48 | end 49 | end 50 | 51 | context "when parent process (where calls the method), has bundler/ruby environment variables" do 52 | NAMES = %w[BUNDLER_EDITOR BUNDLE_BIN_PATH BUNDLE_GEMFILE RUBYOPT GEM_HOME GEM_PATH GEM_CACHE] 53 | before do 54 | @orig_env = {} 55 | NAMES.each do |name| 56 | @orig_env[name] = ENV[name] 57 | ENV[name] = "exec_spec" 58 | end 59 | end 60 | 61 | after do 62 | NAMES.each do |name| 63 | ENV[name] = @orig_env[name] 64 | end 65 | end 66 | 67 | subject(:result) { backend.execute_script!('env') } 68 | let(:env_lines) { result.stdout.lines.map(&:chomp) } 69 | 70 | it { is_expected.to be_a_kind_of(InfraOperator::CommandResult) } 71 | it { is_expected.to be_success } 72 | 73 | it "starts child process without such environment variable" do 74 | NAMES.each do |name| 75 | expect(env_lines).not_to include(/^#{name}=/) 76 | end 77 | end 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /spec/host_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | require 'infra_operator/service_proxy' 4 | require 'infra_operator/platforms/base' 5 | 6 | require 'infra_operator/host' 7 | 8 | describe InfraOperator::Host do 9 | let(:platform_class) { Class.new { } } 10 | let(:backend_class) { Class.new { } } 11 | 12 | describe ".new" do 13 | context "with objects" do 14 | let(:platform) { platform_class.new } 15 | let(:backend) { backend_class.new } 16 | 17 | subject { described_class.new(:platform => platform, :backend => backend) } 18 | 19 | it "holds given platform and backend" do 20 | expect(subject.platform).to eq platform 21 | expect(subject.backend).to eq backend 22 | end 23 | end 24 | 25 | context "with classes" do 26 | subject { described_class.new(:platform => platform_class, :backend => backend_class) } 27 | 28 | it "instantiates given platform class and backend class" do 29 | expect(subject.platform).to be_a_kind_of(platform_class) 30 | expect(subject.backend).to be_a_kind_of(backend_class) 31 | end 32 | end 33 | end 34 | 35 | describe "#service" do 36 | let(:service) { double('service') } 37 | 38 | let(:platform_class) do 39 | _service = service 40 | Class.new do 41 | def initialize(*) 42 | @determined = nil 43 | end 44 | 45 | define_method(:service) do |id| 46 | case id 47 | when :service 48 | _service 49 | when :not_determined 50 | if @determined 51 | _service 52 | else 53 | raise InfraOperator::Platforms::Base::NotYetDetermined 54 | end 55 | when :undetermineable 56 | raise InfraOperator::Platforms::Base::NotYetDetermined 57 | end 58 | end 59 | 60 | def determine_provider!(id, backend) 61 | @determined = true 62 | end 63 | end 64 | end 65 | 66 | let(:backend_class) do 67 | Class.new do 68 | end 69 | end 70 | 71 | let(:platform) { platform_class.new } 72 | let(:backend) { backend_class.new } 73 | 74 | subject(:host) { described_class.new(:platform => platform, :backend => backend) } 75 | 76 | context "when service is determined" do 77 | subject { host.service(:service) } 78 | 79 | it "returns ServiceProxy" do 80 | expect(subject).to be_a_kind_of(InfraOperator::ServiceProxy) 81 | expect(subject.backend).to eq backend 82 | expect(subject.service).to eq service 83 | end 84 | end 85 | 86 | context "when service hasn't been determined and determining succeeded" do 87 | subject { host.service(:not_determined) } 88 | 89 | it "returns ServiceProxy" do 90 | expect(platform).to receive(:determine_provider!).with(:not_determined, backend).and_call_original 91 | 92 | expect(subject).to be_a_kind_of(InfraOperator::ServiceProxy) 93 | expect(subject.backend).to eq backend 94 | expect(subject.service).to eq service 95 | end 96 | end 97 | 98 | context "when service hasn't been determined and determining failed" do 99 | subject { host.service(:undetermineable) } 100 | 101 | it "returns ServiceProxy" do 102 | expect(platform).to receive(:determine_provider!).with(:undetermineable, backend).and_call_original 103 | 104 | expect { subject }.to raise_error(InfraOperator::Platforms::Base::NotYetDetermined) 105 | end 106 | end 107 | end 108 | end 109 | -------------------------------------------------------------------------------- /spec/platforms/base_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'infra_operator/platforms/base' 3 | 4 | describe InfraOperator::Platforms::Base do 5 | let(:provider) { 6 | Class.new do 7 | def self.suitable?(backend) 8 | true 9 | end 10 | end 11 | } 12 | 13 | subject(:klass) do 14 | _provider = provider 15 | Class.new(described_class) do 16 | provides :drink, _provider 17 | end 18 | end 19 | 20 | subject(:platform) do 21 | klass.new 22 | end 23 | 24 | describe "#service" do 25 | subject { platform.service(:drink) } 26 | 27 | it "returns service provider instance" do 28 | expect(subject).to be_a(provider) 29 | end 30 | end 31 | 32 | describe "#provides?" do 33 | it "returns true for provided service" do 34 | expect(platform.provides?(:drink)).to be_truthy 35 | end 36 | 37 | it "returns false for non-provided service" do 38 | expect(platform.provides?(:pizza)).to be_falsey 39 | end 40 | end 41 | 42 | context "with dynamic provider (Array)" do 43 | let(:service_a) { Class.new { } } 44 | let(:service_b) { Class.new { } } 45 | 46 | let(:backend) { double('backend') } 47 | 48 | subject(:klass) do 49 | _service_a, _service_b = service_a, service_b 50 | Class.new(described_class) do 51 | provides :service, [_service_a, _service_b] 52 | end 53 | end 54 | 55 | context "when not determined" do 56 | describe "#service" do 57 | it "raises NotYetDetermined" do 58 | expect { 59 | platform.service(:service) 60 | }.to raise_error(InfraOperator::Platforms::Base::NotYetDetermined) 61 | end 62 | end 63 | end 64 | 65 | context "when determined" do 66 | before do 67 | expect(service_a).to receive(:suitable?).with(backend).and_return(true) 68 | expect(service_b).not_to receive(:suitable?) 69 | 70 | platform.determine_providers!(backend) 71 | end 72 | 73 | it "returns determined service" do 74 | expect(platform.service(:service)).to be_a(service_a) 75 | end 76 | end 77 | 78 | context "when determined (2)" do 79 | before do 80 | expect(service_a).to receive(:suitable?).with(backend).and_return(false) 81 | expect(service_b).to receive(:suitable?).with(backend).and_return(true) 82 | 83 | platform.determine_providers!(backend) 84 | end 85 | 86 | it "returns determined service" do 87 | expect(platform.service(:service)).to be_a(service_b) 88 | end 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /spec/service_proxy_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'infra_operator/service_proxy' 3 | 4 | describe InfraOperator::ServiceProxy do 5 | let(:command) { double('command') } 6 | let(:backend) { double('backend') } 7 | let(:service) { double('service', :operate => command) } 8 | 9 | subject do 10 | described_class.new(backend, service) 11 | end 12 | 13 | it "retrieves action from given service, then execute on given backend" do 14 | expect(command).to receive(:execute).with(backend).and_return(:result) 15 | 16 | expect(subject.operate).to eq :result 17 | end 18 | 19 | context "with arguments" do 20 | it "retrieves action from given service, then execute on given backend" do 21 | allow(service).to receive(:operate2).with(:arg0, :arg1).and_return(command) 22 | expect(command).to receive(:execute).with(backend).and_return(:result2) 23 | 24 | expect(subject.operate2(:arg0, :arg1)).to eq :result2 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 2 | require 'infra_operator' 3 | -------------------------------------------------------------------------------- /spec/utils/shell_builder_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'infra_operator/utils/shell_builder' 3 | 4 | RSpec.describe InfraOperator::Utils::ShellBuilder do 5 | def build(&block) 6 | described_class.new(&block).to_s 7 | end 8 | 9 | describe "var" do 10 | subject { build { var('foo' => 'bar baz', 'hoge' => 'fuga') }.chomp.split(/; /) } 11 | 12 | it { is_expected.to include("foo=bar\\ baz") } 13 | it { is_expected.to include("hoge=fuga") } 14 | end 15 | 16 | describe "export" do 17 | subject { build { export('foo' => 'bar baz', 'hoge' => 'fuga') }.chomp.split(/; /) } 18 | 19 | it { is_expected.to include("export foo=bar\\ baz") } 20 | it { is_expected.to include("export hoge=fuga") } 21 | end 22 | 23 | describe "cd" do 24 | context "simple" do 25 | subject { build { cd "/tmp" } } 26 | it { is_expected.to eq "cd /tmp" } 27 | end 28 | 29 | context "with spaces" do 30 | subject { build { cd "/tmp tmp" } } 31 | it { is_expected.to eq "cd /tmp\\ tmp" } 32 | end 33 | end 34 | 35 | describe "run" do 36 | context "simple" do 37 | subject { build { run 'foo', 'bar', '1 2' } } 38 | it { is_expected.to eq "foo bar 1\\ 2" } 39 | end 40 | 41 | context "array" do 42 | subject { build { run %w(foo bar) } } 43 | it { is_expected.to eq "foo bar" } 44 | end 45 | 46 | context "array and values" do 47 | subject { build { run %w(foo bar), 'baz' } } 48 | it { is_expected.to eq "foo bar baz" } 49 | end 50 | 51 | context "redirection" do 52 | subject(:script) do 53 | build do 54 | run( 55 | 'foo', 56 | { 57 | :in => 'in.txt', 58 | :out => 'out.txt', 59 | :err => 'err.txt', 60 | 10 => '10.txt', 61 | 21 => [:read, '21.txt'], 62 | 22 => [:rw, '22.txt'], 63 | 23 => [:append, '23.txt'], 64 | 31 => 91, 65 | 32 => [:read, 92], 66 | 33 => [:rw, 93], 67 | 40 => 'foo bar.txt', 68 | } 69 | ) 70 | end 71 | end 72 | 73 | subject(:splitted_subject) { script.shellsplit } 74 | 75 | specify { 76 | expect(splitted_subject.size).to eq 12 77 | expect(splitted_subject.first).to eq 'foo' 78 | expect(splitted_subject).to include('out.txt') 80 | expect(splitted_subject).to include('2>err.txt') 81 | expect(splitted_subject).to include('10>10.txt') 82 | expect(splitted_subject).to include('21<21.txt') 83 | expect(splitted_subject).to include('22<>22.txt') 84 | expect(splitted_subject).to include('23>>23.txt') 85 | expect(splitted_subject).to include('31>&91') 86 | expect(splitted_subject).to include('32<&92') 87 | expect(splitted_subject).to include('33<>&93') 88 | expect(splitted_subject).to include("40>foo bar.txt") 89 | expect(script).to include("40>foo\\ bar.txt") 90 | } 91 | end 92 | end 93 | 94 | describe "pipe" do 95 | subject do 96 | build do 97 | pipe do 98 | run 'a' 99 | run 'b' 100 | end 101 | end 102 | end 103 | 104 | it { is_expected.to eq "a | b" } 105 | end 106 | 107 | describe "subshell" do 108 | subject do 109 | build do 110 | subshell do 111 | run 'a' 112 | run 'b' 113 | end 114 | end 115 | end 116 | 117 | it { is_expected.to eq "( a; b )" } 118 | end 119 | 120 | describe "with_and" do 121 | subject do 122 | build do 123 | with_and do 124 | run 'a' 125 | run 'b' 126 | end 127 | end 128 | end 129 | 130 | it { is_expected.to eq "a && b" } 131 | end 132 | 133 | describe "with_or" do 134 | subject do 135 | build do 136 | with_or do 137 | run 'a' 138 | run 'b' 139 | end 140 | end 141 | end 142 | 143 | it { is_expected.to eq "a || b" } 144 | end 145 | 146 | context do 147 | subject do 148 | build do 149 | cd '/tmp' 150 | export 'foo' => 'bar' 151 | 152 | run 'a', 'hello', 'world' 153 | 154 | subshell do 155 | run 'b.a' 156 | run 'b.b' 157 | 158 | pipe do 159 | run 'b.c.a' 160 | run 'b.c.b' 161 | end 162 | end 163 | 164 | with_and do 165 | run 'c.a' 166 | run 'c.b' 167 | with_or do 168 | run 'c.c.a' 169 | run 'c.c.b' 170 | end 171 | end 172 | end 173 | end 174 | 175 | specify "complex one" do 176 | expect(subject).to eq(<<-EOF.lines.map(&:chomp).join(' ')) 177 | cd /tmp; 178 | export foo=bar; 179 | a hello\ world; 180 | ( 181 | b.a; 182 | b.b; 183 | b.c.a | b.c.b 184 | ); 185 | c.a && c.b && c.c.a || c.c.b 186 | EOF 187 | end 188 | end 189 | end 190 | --------------------------------------------------------------------------------