├── spec ├── fixtures │ ├── inventory.yml │ ├── requirements.yml │ ├── other_requirements.yml │ └── ansible.cfg.template ├── commands │ ├── out_spec.rb │ ├── in_spec.rb │ └── check_spec.rb ├── integration │ ├── output_spec.rb │ ├── ansible_cfg_spec.rb │ ├── input_spec.rb │ ├── ssh_spec.rb │ ├── requirements_spec.rb │ ├── git_spec.rb │ └── playbook_spec.rb └── spec_helper.rb ├── assets ├── check ├── in ├── lib │ ├── ssh_askpass.sh │ ├── ansible_galaxy.rb │ ├── input.rb │ ├── git_config.rb │ ├── ssh_config.rb │ ├── ansible_playbook.rb │ └── commands │ │ └── out.rb └── out ├── .rspec ├── LICENSE ├── Dockerfile └── README.md /spec/fixtures/inventory.yml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/fixtures/requirements.yml: -------------------------------------------------------------------------------- 1 | --- 2 | -------------------------------------------------------------------------------- /assets/check: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | printf [] 3 | -------------------------------------------------------------------------------- /spec/fixtures/other_requirements.yml: -------------------------------------------------------------------------------- 1 | --- 2 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --require spec_helper 2 | --force-color 3 | -------------------------------------------------------------------------------- /assets/in: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | printf '{"version":{}}' 3 | -------------------------------------------------------------------------------- /assets/lib/ssh_askpass.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | echo "Private keys with passphrases are not supported." >&2 3 | exit 1 4 | -------------------------------------------------------------------------------- /spec/fixtures/ansible.cfg.template: -------------------------------------------------------------------------------- 1 | [defaults] 2 | other_stuff = good 3 | vault_password_file = bad! 4 | private_key_file = bad! 5 | ask_vault_pass = bad! 6 | become_ask_pass = bad! 7 | become = bad! 8 | become_user = bad! 9 | become_method = bad! 10 | inventory = bad! 11 | remote_user = bad! 12 | verbosity = bad! 13 | -------------------------------------------------------------------------------- /assets/out: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | exec 3>&1 # make stdout available as fd 3 for the result 6 | exec 1>&2 # redirect all output to stderr for logging 7 | 8 | eval $(ssh-agent) > /dev/null 2>&1 9 | trap "kill $SSH_AGENT_PID" 0 10 | 11 | $(dirname $0)/lib/commands/out.rb $1 <&0 12 | 13 | printf '{"version":{}}' >&3 14 | -------------------------------------------------------------------------------- /spec/commands/out_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'open3' 3 | require 'json' 4 | 5 | describe "commands:out" do 6 | 7 | let(:out_file) { '/opt/resource/out' } 8 | 9 | it "should exist" do 10 | expect(File).to exist(out_file) 11 | expect(File.stat(out_file).mode.to_s(8)[3..5]).to eq("755") 12 | end 13 | 14 | end 15 | -------------------------------------------------------------------------------- /spec/commands/in_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "commands:in" do 4 | 5 | let(:in_file) { '/opt/resource/in' } 6 | 7 | it "should exist" do 8 | expect(File).to exist(in_file) 9 | expect(File.stat(in_file).mode.to_s(8)[3..5]).to eq("755") 10 | end 11 | 12 | it "should return an empty version" do 13 | expect(`#{in_file}`).to eq("{\"version\":{}}") 14 | end 15 | 16 | end 17 | -------------------------------------------------------------------------------- /spec/commands/check_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "commands:check" do 4 | 5 | let(:check_file) { '/opt/resource/check' } 6 | 7 | it "should exist" do 8 | expect(File).to exist(check_file) 9 | expect(File.stat(check_file).mode.to_s(8)[3..5]).to eq("755") 10 | end 11 | 12 | it "should return an empty array" do 13 | expect(`#{check_file}`).to eq("[]") 14 | end 15 | 16 | end 17 | -------------------------------------------------------------------------------- /assets/lib/ansible_galaxy.rb: -------------------------------------------------------------------------------- 1 | 2 | class AnsibleGalaxy 3 | 4 | attr_writer :requirements 5 | attr_writer :verbose 6 | 7 | def initialize(echo = false) 8 | @echo = echo 9 | end 10 | 11 | def requirements 12 | @requirements || "requirements.yml" 13 | end 14 | 15 | def verbose 16 | "-#{@verbose}" unless @verbose.nil? 17 | end 18 | 19 | def install_command 20 | "ansible-galaxy install #{verbose} -r #{requirements}" 21 | end 22 | 23 | def install! 24 | return 0 unless File.exists? requirements 25 | 26 | cmd = install_command 27 | STDERR.puts cmd if @echo 28 | system(cmd) 29 | $?.exitstatus 30 | end 31 | 32 | end 33 | -------------------------------------------------------------------------------- /assets/lib/input.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | require 'ostruct' 3 | 4 | class InputError < StandardError 5 | def initialize(msg) 6 | super(msg) 7 | end 8 | end 9 | 10 | class Input 11 | 12 | def self.instance(payload: nil) 13 | @instance = new(payload: payload) if payload 14 | @instance ||= begin 15 | payload = JSON.parse(ARGF.read) 16 | new(payload: payload) 17 | end 18 | end 19 | 20 | def self.reset 21 | @instance = nil 22 | end 23 | 24 | def initialize(payload:) 25 | @payload = payload 26 | end 27 | 28 | def source 29 | @source ||= OpenStruct.new @payload.fetch('source', {}) 30 | end 31 | 32 | def version 33 | @version ||= OpenStruct.new @payload.fetch('version', {}) 34 | end 35 | 36 | def params 37 | @params ||= OpenStruct.new @payload.fetch('params', {}) 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /spec/integration/output_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'open3' 3 | require 'json' 4 | 5 | describe "integration:output" do 6 | 7 | let(:out_file) { '/opt/resource/out' } 8 | let(:mockelton_out) { '/resource/spec/fixtures/mockleton.out' } 9 | 10 | after(:each) do 11 | File.delete mockelton_out if File.exists? mockelton_out 12 | end 13 | 14 | it "should return an empty version" do 15 | stdin = { 16 | "source" => { 17 | "ssh_private_key" => "key" 18 | }, 19 | "params" => { 20 | "path" => "spec/fixtures", 21 | "inventory" => "the_inventory" 22 | } 23 | }.to_json 24 | 25 | stdout, stderr, status = Open3.capture3("#{out_file} .", :stdin_data => stdin) 26 | 27 | expect(status.success?).to be true 28 | expect(stdout).to eq("{\"version\":{}}") 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /assets/lib/git_config.rb: -------------------------------------------------------------------------------- 1 | 2 | class GitConfig 3 | 4 | def initialize(echo = false) 5 | @echo = echo 6 | end 7 | 8 | def skip_ssl_verification!(skip = true) 9 | if skip 10 | ENV['GIT_SSL_NO_VERIFY'] = "true" 11 | end 12 | end 13 | 14 | def configure_https_credentials!(username, password) 15 | if !username.nil? and !password.nil? 16 | netrc_path = File.expand_path "~/.netrc" 17 | File.write netrc_path, "default login #{username} #{password}\n" 18 | return true 19 | end 20 | false 21 | end 22 | 23 | def configure_git_global!(entries) 24 | (entries || {}).each do |key, value| 25 | cmd = "git config --global '#{key}' '#{value}'" 26 | puts cmd if @echo 27 | 28 | stdout, stderr, status = Open3.capture3(cmd) 29 | puts stdout 30 | STDERR.puts stderr 31 | 32 | raise "git config failed" unless status.success? 33 | end 34 | end 35 | 36 | end 37 | -------------------------------------------------------------------------------- /assets/lib/ssh_config.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'open3' 3 | 4 | class SSHConfig 5 | 6 | def initialize(echo = false) 7 | @echo = echo 8 | end 9 | 10 | def create_key_file!(key_path, key) 11 | unless key.end_with?("\n") 12 | key << "\n" 13 | end 14 | File.write key_path, key 15 | FileUtils.chmod 0600, key_path 16 | end 17 | 18 | def ssh_add_key!(key_path) 19 | cmd = "SSH_ASKPASS=/opt/resource/lib/ssh_askpass.sh DISPLAY= ssh-add #{key_path}" 20 | puts cmd if @echo 21 | 22 | stdout, stderr, status = Open3.capture3(ENV, cmd) 23 | puts(stdout) if @echo 24 | STDERR.puts stderr 25 | 26 | raise "ssh-add failed" unless status.success? 27 | end 28 | 29 | def configure! 30 | ssh_dir = File.expand_path "~/.ssh" 31 | ssh_config_path = File.join(ssh_dir, "config") 32 | 33 | FileUtils.mkdir_p ssh_dir 34 | File.write ssh_config_path, <<~EOF 35 | StrictHostKeyChecking no 36 | LogLevel quiet 37 | EOF 38 | 39 | FileUtils.chmod 0600, ssh_config_path 40 | end 41 | 42 | end 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Troy Kinsella 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /spec/integration/ansible_cfg_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'open3' 3 | require 'json' 4 | 5 | describe "integration:ansible.cfg" do 6 | 7 | let(:out_file) { '/opt/resource/out' } 8 | let(:ansible_cfg_file) { 'spec/fixtures/ansible.cfg' } 9 | 10 | before(:each) do 11 | `cp #{ansible_cfg_file}.template #{ansible_cfg_file}` 12 | end 13 | 14 | after(:each) do 15 | File.delete ansible_cfg_file 16 | end 17 | 18 | it "gets sanitized" do 19 | stdin = { 20 | "source" => { 21 | "ssh_private_key" => "key", 22 | "user" => "foo", 23 | "verbose" => "vv" 24 | }, 25 | "params" => { 26 | "become" => true, 27 | "become_user" => "foo", 28 | "become_method" => "sudo", 29 | "path" => "spec/fixtures", 30 | "inventory" => "the_inventory" 31 | } 32 | }.to_json 33 | 34 | stdout, stderr, status = Open3.capture3("#{out_file} .", :stdin_data => stdin) 35 | 36 | expect(status.success?).to be true 37 | 38 | cfg = File.read ansible_cfg_file 39 | expect(cfg).to_not include("bad!") 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:latest as main 2 | 3 | RUN set -eux; \ 4 | apk --update add bash openssh-client ruby git ruby-json python3 py3-pip openssl ca-certificates; \ 5 | apk --update add --virtual \ 6 | build-dependencies \ 7 | build-base \ 8 | python3-dev \ 9 | libffi-dev \ 10 | openssl-dev \ 11 | musl-dev \ 12 | cargo; \ 13 | pip3 install --upgrade pip cffi; \ 14 | pip3 install ansible boto pywinrm; \ 15 | apk del build-dependencies; \ 16 | rm -rf /var/cache/apk/*; \ 17 | mkdir -p /etc/ansible; \ 18 | echo -e "[local]\nlocalhost ansible_connection=local" > /etc/ansible/hosts 19 | 20 | COPY assets/ /opt/resource/ 21 | 22 | FROM main as testing 23 | 24 | RUN set -eux; \ 25 | gem install rspec; \ 26 | wget -q -O - https://raw.githubusercontent.com/troykinsella/mockleton/master/install.sh | bash; \ 27 | cp /usr/local/bin/mockleton /usr/local/bin/ansible-galaxy; \ 28 | cp /usr/local/bin/mockleton /usr/local/bin/ansible-playbook; \ 29 | cp /usr/local/bin/mockleton /usr/bin/ssh-add; 30 | 31 | COPY . /resource/ 32 | 33 | WORKDIR /resource 34 | RUN rspec 35 | 36 | FROM main 37 | -------------------------------------------------------------------------------- /spec/integration/input_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'open3' 3 | require 'json' 4 | 5 | describe "integration:input" do 6 | 7 | let(:out_file) { '/opt/resource/out' } 8 | let(:mockelton_out) { '/resource/spec/fixtures/mockleton.out' } 9 | 10 | after(:each) do 11 | File.delete mockelton_out if File.exists? mockelton_out 12 | end 13 | 14 | it "requires params.path" do 15 | stdout, stderr, status = Open3.capture3("#{out_file} .", :stdin_data => "{}") 16 | 17 | expect(status.success?).to be false 18 | expect(stderr).to eq %("params.path" must be defined\n) 19 | 20 | end 21 | 22 | it "requires params.path to exist" do 23 | stdin = { 24 | "params" => { 25 | "path" => "definitely_doesn't exist" 26 | } 27 | }.to_json 28 | 29 | stdout, stderr, status = Open3.capture3("#{out_file} .", :stdin_data => stdin) 30 | 31 | expect(status.success?).to be false 32 | expect(stderr).to eq %(params.path: "definitely_doesn't exist" does not exist\n) 33 | 34 | end 35 | 36 | it "does not require source.ssh_private_key" do 37 | stdin = { 38 | "params" => { 39 | "path" => "spec/fixtures", 40 | "inventory" => "inventory.yml" 41 | } 42 | }.to_json 43 | 44 | stdout, stderr, status = Open3.capture3("#{out_file} .", :stdin_data => stdin) 45 | 46 | expect(status.success?).to be true 47 | 48 | end 49 | 50 | it "requires params.inventory" do 51 | stdin = { 52 | "source" => { 53 | "ssh_private_key" => "key" 54 | }, 55 | "params" => { 56 | "path" => "spec/fixtures" 57 | } 58 | }.to_json 59 | 60 | stdout, stderr, status = Open3.capture3("#{out_file} .", :stdin_data => stdin) 61 | 62 | expect(status.success?).to be false 63 | expect(stderr).to eq %("params.inventory" must be defined\n) 64 | 65 | end 66 | 67 | end 68 | -------------------------------------------------------------------------------- /spec/integration/ssh_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'open3' 3 | require 'json' 4 | 5 | describe "integration:ssh" do 6 | 7 | let(:out_file) { '/opt/resource/out' } 8 | let(:mockelton_out) { '/resource/spec/fixtures/mockleton.out' } 9 | let(:ssh_private_key_file) { '/tmp/ansible-playbook-resource-ssh-private-key' } 10 | let(:ssh_config) { '/root/.ssh/config' } 11 | 12 | after(:each) do 13 | File.delete mockelton_out if File.exists? mockelton_out 14 | end 15 | 16 | it "creates private key file" do 17 | stdin = { 18 | "source" => { 19 | "ssh_private_key" => "key\n", 20 | "debug" => true 21 | }, 22 | "params" => { 23 | "path" => "spec/fixtures", 24 | "inventory" => "the_inventory" 25 | } 26 | }.to_json 27 | 28 | stdout, stderr, status = Open3.capture3("#{out_file} .", :stdin_data => stdin) 29 | 30 | expect(status.success?).to be true 31 | expect(File).to exist(ssh_private_key_file) 32 | 33 | ssh_private_key_contents = File.read ssh_private_key_file 34 | expect(ssh_private_key_contents).to eq("key\n") 35 | end 36 | 37 | it "adds trailing newline to private key file" do 38 | stdin = { 39 | "source" => { 40 | "ssh_private_key" => "key", 41 | "debug" => true 42 | }, 43 | "params" => { 44 | "path" => "spec/fixtures", 45 | "inventory" => "the_inventory" 46 | } 47 | }.to_json 48 | 49 | stdout, stderr, status = Open3.capture3("#{out_file} .", :stdin_data => stdin) 50 | 51 | expect(status.success?).to be true 52 | expect(File).to exist(ssh_private_key_file) 53 | 54 | ssh_private_key_contents = File.read ssh_private_key_file 55 | expect(ssh_private_key_contents).to eq("key\n") 56 | end 57 | 58 | it "should create ssh config" do 59 | stdin = { 60 | "source" => { 61 | "ssh_private_key" => "key\n" 62 | }, 63 | "params" => { 64 | "path" => "spec/fixtures", 65 | "inventory" => "the_inventory" 66 | } 67 | }.to_json 68 | 69 | stdout, stderr, status = Open3.capture3("#{out_file} .", :stdin_data => stdin) 70 | 71 | expect(status.success?).to be true 72 | expect(File).to exist(ssh_config) 73 | 74 | ssh_config_contents = File.read ssh_config 75 | expect(ssh_config_contents).to include("StrictHostKeyChecking no\n") 76 | expect(ssh_config_contents).to include("LogLevel quiet\n") 77 | end 78 | 79 | end 80 | -------------------------------------------------------------------------------- /assets/lib/ansible_playbook.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'json' 3 | 4 | class AnsiblePlaybook 5 | 6 | attr_writer :become 7 | attr_writer :become_user 8 | attr_writer :become_method 9 | attr_writer :check 10 | attr_writer :diff 11 | attr_writer :env 12 | attr_writer :extra_vars 13 | attr_writer :inventory 14 | attr_writer :limit 15 | attr_writer :playbook 16 | attr_writer :private_key 17 | attr_writer :skip_tags 18 | attr_writer :ssh_common_args 19 | attr_writer :tags 20 | attr_writer :user 21 | attr_writer :vault_password_file 22 | attr_writer :verbose 23 | 24 | def initialize(echo = false) 25 | @echo = echo 26 | end 27 | 28 | def become 29 | "--become" unless @become.nil? 30 | end 31 | 32 | def become_user 33 | "--become-user #{@become_user}" unless @become_user.nil? 34 | end 35 | 36 | def become_method 37 | "--become-method #{@become_method}" unless @become_method.nil? 38 | end 39 | 40 | def check 41 | "--check" if @check 42 | end 43 | 44 | def diff 45 | "--diff" if @diff 46 | end 47 | 48 | def env 49 | @env || {} 50 | end 51 | 52 | def extra_vars 53 | "--extra-vars '#{@extra_vars.to_json}'" unless @extra_vars.nil? 54 | end 55 | 56 | def limit 57 | "--limit '#{@limit}'" unless @limit.nil? 58 | end 59 | 60 | def inventory 61 | raise "inventory required" if @inventory.nil? 62 | "-i #{@inventory}" 63 | end 64 | 65 | def playbook 66 | @playbook || "site.yml" 67 | end 68 | 69 | def private_key 70 | "--private-key #{@private_key}" unless @private_key.nil? 71 | end 72 | 73 | def tags 74 | (@tags || []).map{|t| "-t #{t}"} 75 | end 76 | 77 | def user 78 | "--user #{@user}" unless @user.nil? 79 | end 80 | 81 | def skip_tags 82 | (@skip_tags || []).map{|t| "--skip-tags #{t}"} 83 | end 84 | 85 | def ssh_common_args 86 | "--ssh-common-args #{@ssh_common_args.to_json}" unless @ssh_common_args.nil? 87 | end 88 | 89 | def vault_password_file 90 | "--vault-password-file #{@vault_password_file}" unless @vault_password_file.nil? 91 | end 92 | 93 | def verbose 94 | "-#{@verbose}" unless @verbose.nil? 95 | end 96 | 97 | def command 98 | [ 99 | "ansible-playbook", 100 | become, 101 | become_user, 102 | become_method, 103 | check, 104 | diff, 105 | extra_vars, 106 | inventory, 107 | limit, 108 | private_key, 109 | skip_tags, 110 | ssh_common_args, 111 | user, 112 | tags, 113 | vault_password_file, 114 | verbose, 115 | playbook 116 | ].join(" ") 117 | end 118 | 119 | def execute! 120 | cmd = command 121 | STDERR.puts cmd if @echo 122 | exec(env, cmd) 123 | end 124 | 125 | end 126 | -------------------------------------------------------------------------------- /spec/integration/requirements_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'open3' 3 | require 'json' 4 | 5 | describe "integration:requirments" do 6 | 7 | let(:out_file) { '/opt/resource/out' } 8 | let(:mockelton_out) { '/resource/spec/fixtures/mockleton.out' } 9 | 10 | after(:each) do 11 | File.delete mockelton_out if File.exists? mockelton_out 12 | end 13 | 14 | it "installs default requirements" do 15 | stdin = { 16 | "source" => { 17 | "ssh_private_key" => "key" 18 | }, 19 | "params" => { 20 | "path" => "spec/fixtures", 21 | "inventory" => "the_inventory" 22 | } 23 | }.to_json 24 | 25 | stdout, stderr, status = Open3.capture3("#{out_file} .", :stdin_data => stdin) 26 | 27 | expect(status.success?).to be true 28 | 29 | out = JSON.parse(File.read(mockelton_out)) 30 | 31 | expect(out["sequence"].size).to be 2 32 | expect(out["sequence"][0]["exec-spec"]["args"]).to eq [ "ansible-galaxy", "install", "-r", "requirements.yml" ] 33 | end 34 | 35 | it "installs specified requirements" do 36 | stdin = { 37 | "source" => { 38 | "ssh_private_key" => "key", 39 | "requirements" => "other_requirements.yml" 40 | }, 41 | "params" => { 42 | "path" => "spec/fixtures", 43 | "inventory" => "the_inventory" 44 | } 45 | }.to_json 46 | 47 | stdout, stderr, status = Open3.capture3("#{out_file} .", :stdin_data => stdin) 48 | 49 | expect(status.success?).to be true 50 | 51 | out = JSON.parse(File.read(mockelton_out)) 52 | 53 | expect(out["sequence"].size).to be 2 54 | expect(out["sequence"][0]["exec-spec"]["args"]).to eq [ "ansible-galaxy", "install", "-r", "other_requirements.yml" ] 55 | end 56 | 57 | it "installs requirements with specified verbosity" do 58 | stdin = { 59 | "source" => { 60 | "ssh_private_key" => "key", 61 | "requirements" => "other_requirements.yml", 62 | "verbose" => "vvv" 63 | }, 64 | "params" => { 65 | "path" => "spec/fixtures", 66 | "inventory" => "the_inventory" 67 | } 68 | }.to_json 69 | 70 | stdout, stderr, status = Open3.capture3("#{out_file} .", :stdin_data => stdin) 71 | 72 | expect(status.success?).to be true 73 | 74 | out = JSON.parse(File.read(mockelton_out)) 75 | 76 | expect(out["sequence"].size).to be 2 77 | expect(out["sequence"][0]["exec-spec"]["args"]).to eq [ "ansible-galaxy", "install", "-vvv", "-r", "other_requirements.yml" ] 78 | end 79 | 80 | it "fails when requirements not found" do 81 | stdin = { 82 | "source" => { 83 | "ssh_private_key" => "key", 84 | "requirements" => "bogus_requirements.yml" 85 | }, 86 | "params" => { 87 | "path" => "spec/fixtures", 88 | "inventory" => "the_inventory" 89 | } 90 | }.to_json 91 | 92 | stdout, stderr, status = Open3.capture3("#{out_file} .", :stdin_data => stdin) 93 | 94 | expect(status.success?).to be false 95 | expect(stderr).to eq %(source.requirements: "bogus_requirements.yml" does not exist\n) 96 | end 97 | 98 | 99 | end 100 | -------------------------------------------------------------------------------- /spec/integration/git_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'open3' 3 | require 'json' 4 | 5 | describe "integration:git" do 6 | 7 | let(:out_file) { '/opt/resource/out' } 8 | let(:mockelton_out) { '/resource/spec/fixtures/mockleton.out' } 9 | let(:git_private_key_file) { '/tmp/ansible-playbook-resource-git-private-key' } 10 | let(:netrc_file) { '/root/.netrc' } 11 | let(:gitconfig_file) { '/root/.gitconfig' } 12 | 13 | after(:each) do 14 | File.delete mockelton_out if File.exists? mockelton_out 15 | end 16 | 17 | it "creates private key file" do 18 | stdin = { 19 | "source" => { 20 | "ssh_private_key" => "ssh_key", 21 | "git_private_key" => "git_key\n" 22 | }, 23 | "params" => { 24 | "path" => "spec/fixtures", 25 | "inventory" => "the_inventory" 26 | } 27 | }.to_json 28 | 29 | stdout, stderr, status = Open3.capture3("#{out_file} .", :stdin_data => stdin) 30 | 31 | expect(status.success?).to be true 32 | expect(File).to exist(git_private_key_file) 33 | 34 | git_private_key_contents = File.read git_private_key_file 35 | expect(git_private_key_contents).to eq("git_key\n") 36 | end 37 | 38 | it "configures ssl verification" do 39 | stdin = { 40 | "source" => { 41 | "ssh_private_key" => "key", 42 | "git_skip_ssl_verification" => true 43 | }, 44 | "params" => { 45 | "path" => "spec/fixtures", 46 | "inventory" => "the_inventory" 47 | } 48 | }.to_json 49 | 50 | stdout, stderr, status = Open3.capture3("#{out_file} .", :stdin_data => stdin) 51 | 52 | expect(status.success?).to be true 53 | 54 | out = JSON.parse(File.read(mockelton_out)) 55 | 56 | expect(out["sequence"].size).to be 2 57 | expect(out["sequence"][1]["exec-spec"]["env"]).to include("GIT_SSL_NO_VERIFY" => "true") 58 | end 59 | 60 | it "configures https credentials" do 61 | stdin = { 62 | "source" => { 63 | "ssh_private_key" => "key", 64 | "git_https_username" => "foo", 65 | "git_https_password" => "bar" 66 | }, 67 | "params" => { 68 | "path" => "spec/fixtures", 69 | "inventory" => "the_inventory" 70 | } 71 | }.to_json 72 | 73 | stdout, stderr, status = Open3.capture3("#{out_file} .", :stdin_data => stdin) 74 | 75 | expect(status.success?).to be true 76 | expect(File).to exist(netrc_file) 77 | 78 | netrc_contents = File.read netrc_file 79 | expect(netrc_contents).to include("default login foo bar\n") 80 | end 81 | 82 | it "configures git globally" do 83 | stdin = { 84 | "source" => { 85 | "ssh_private_key" => "key", 86 | "git_global_config" => { 87 | "user.name" => "foo", 88 | "user.email" => "foo@bar.com" 89 | } 90 | }, 91 | "params" => { 92 | "path" => "spec/fixtures", 93 | "inventory" => "the_inventory" 94 | } 95 | }.to_json 96 | 97 | stdout, stderr, status = Open3.capture3("#{out_file} .", :stdin_data => stdin) 98 | 99 | expect(status.success?).to be true 100 | expect(File).to exist(gitconfig_file) 101 | 102 | gitconfig_contents = File.read gitconfig_file 103 | expect(gitconfig_contents).to include("[user]\n\tname = foo\n\temail = foo@bar.com\n") 104 | end 105 | 106 | it "adds git key to ssh agent" do 107 | stdin = { 108 | "source" => { 109 | "ssh_private_key" => "ssh_key", 110 | "git_private_key" => "git_key", 111 | "debug" => true 112 | }, 113 | "params" => { 114 | "path" => "spec/fixtures", 115 | "inventory" => "the_inventory" 116 | } 117 | }.to_json 118 | 119 | stdout, stderr, status = Open3.capture3("#{out_file} .", :stdin_data => stdin) 120 | 121 | expect(status.success?).to be true 122 | out = JSON.parse(File.read(mockelton_out)) 123 | 124 | expect(out["sequence"].size).to be 3 125 | expect(out["sequence"][0]["exec-spec"]["args"]).to eq [ "ssh-add", git_private_key_file ] 126 | end 127 | 128 | end 129 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # This file was generated by the `rspec --init` command. Conventionally, all 2 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. 3 | # The generated `.rspec` file contains `--require spec_helper` which will cause 4 | # this file to always be loaded, without a need to explicitly require it in any 5 | # files. 6 | # 7 | # Given that it is always loaded, you are encouraged to keep this file as 8 | # light-weight as possible. Requiring heavyweight dependencies from this file 9 | # will add to the boot time of your test suite on EVERY test run, even for an 10 | # individual file that may not need all of that loaded. Instead, consider making 11 | # a separate helper file that requires the additional dependencies and performs 12 | # the additional setup, and require it from the spec files that actually need 13 | # it. 14 | # 15 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 16 | RSpec.configure do |config| 17 | # rspec-expectations config goes here. You can use an alternate 18 | # assertion/expectation library such as wrong or the stdlib/minitest 19 | # assertions if you prefer. 20 | config.expect_with :rspec do |expectations| 21 | # This option will default to `true` in RSpec 4. It makes the `description` 22 | # and `failure_message` of custom matchers include text for helper methods 23 | # defined using `chain`, e.g.: 24 | # be_bigger_than(2).and_smaller_than(4).description 25 | # # => "be bigger than 2 and smaller than 4" 26 | # ...rather than: 27 | # # => "be bigger than 2" 28 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 29 | end 30 | 31 | # rspec-mocks config goes here. You can use an alternate test double 32 | # library (such as bogus or mocha) by changing the `mock_with` option here. 33 | config.mock_with :rspec do |mocks| 34 | # Prevents you from mocking or stubbing a method that does not exist on 35 | # a real object. This is generally recommended, and will default to 36 | # `true` in RSpec 4. 37 | mocks.verify_partial_doubles = true 38 | end 39 | 40 | # This option will default to `:apply_to_host_groups` in RSpec 4 (and will 41 | # have no way to turn it off -- the option exists only for backwards 42 | # compatibility in RSpec 3). It causes shared context metadata to be 43 | # inherited by the metadata hash of host groups and examples, rather than 44 | # triggering implicit auto-inclusion in groups with matching metadata. 45 | config.shared_context_metadata_behavior = :apply_to_host_groups 46 | 47 | # The settings below are suggested to provide a good initial experience 48 | # with RSpec, but feel free to customize to your heart's content. 49 | =begin 50 | # This allows you to limit a spec run to individual examples or groups 51 | # you care about by tagging them with `:focus` metadata. When nothing 52 | # is tagged with `:focus`, all examples get run. RSpec also provides 53 | # aliases for `it`, `describe`, and `context` that include `:focus` 54 | # metadata: `fit`, `fdescribe` and `fcontext`, respectively. 55 | config.filter_run_when_matching :focus 56 | 57 | # Allows RSpec to persist some state between runs in order to support 58 | # the `--only-failures` and `--next-failure` CLI options. We recommend 59 | # you configure your source control system to ignore this file. 60 | config.example_status_persistence_file_path = "spec/examples.txt" 61 | 62 | # Limits the available syntax to the non-monkey patched syntax that is 63 | # recommended. For more details, see: 64 | # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ 65 | # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ 66 | # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode 67 | config.disable_monkey_patching! 68 | 69 | # This setting enables warnings. It's recommended, but in some cases may 70 | # be too noisy due to issues in dependencies. 71 | config.warnings = true 72 | 73 | # Many RSpec users commonly either run the entire suite or an individual 74 | # file, and it's useful to allow more verbose output when running an 75 | # individual spec file. 76 | if config.files_to_run.one? 77 | # Use the documentation formatter for detailed output, 78 | # unless a formatter has already been configured 79 | # (e.g. via a command-line flag). 80 | config.default_formatter = "doc" 81 | end 82 | 83 | # Print the 10 slowest examples and example groups at the 84 | # end of the spec run, to help surface which specs are running 85 | # particularly slow. 86 | config.profile_examples = 10 87 | 88 | # Run specs in random order to surface order dependencies. If you find an 89 | # order dependency and want to debug it, you can fix the order by providing 90 | # the seed, which is printed after each run. 91 | # --seed 1234 92 | config.order = :random 93 | 94 | # Seed global randomization in this process using the `--seed` CLI option. 95 | # Setting this allows you to use `--seed` to deterministically reproduce 96 | # test failures related to randomization by passing the same `--seed` value 97 | # as the one that triggered the failure. 98 | Kernel.srand config.seed 99 | =end 100 | end 101 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Concourse Ansible Playbook Resource 2 | 3 | A [Concourse CI](https://concourse-ci.org) resource for running Ansible playbooks. 4 | 5 | The resource image contains the latest version of ansible, installed by pip, 6 | as of when the image was created. It runs ansible with python 3. 7 | See the `Dockerfile` for other supplied system and pip packages. 8 | 9 | See [Docker Hub](https://cloud.docker.com/repository/docker/troykinsella/concourse-ansible-playbook-resource) 10 | for tagged image versions available. 11 | 12 | ## Source Configuration 13 | 14 | Most source attributes map directly to `ansible-playbook` options. See the 15 | `ansible-playbook --help` for further details. 16 | 17 | The `git_*` attributes are relevant to referencing git repositories in the `requirements.yml` file 18 | which are pulled from during `ansible-galaxy install`. 19 | 20 | * `debug`: Optional. Boolean. Default `false`. Echo commands and other normally-hidden outputs useful for troubleshooting. 21 | * `env`: Optional. A list of environment variables to apply. 22 | Useful for supplying task configuration dependencies like `AWS_ACCESS_KEY_ID`, for example, or specifying 23 | [ansible configuration](https://docs.ansible.com/ansible/latest/reference_appendices/config.html) options 24 | that are unsupported by this resource. Note: Unsupported ansible configurations can also be applied in `ansible.cfg` 25 | in the playbook source. 26 | * `git_global_config`: Optional. A list of git global configurations to apply (with `git config --global`). 27 | * `git_https_username`: Optional. The username for git http/s access. 28 | * `git_https_password`: Optional. The password for git http/s access. 29 | * `git_private_key`: Optional. The git ssh private key. 30 | * `git_skip_ssl_verification`: Optional. Boolean. Default `false`. Don't verify TLS certificates. 31 | * `user`: Optional. Connect to the remote system with this user. 32 | * `requirements`: Optional. Default `requirements.yml`. If this file is present in the 33 | playbook source directory, it is used with `ansible-galaxy --install` before running the playbook. 34 | * `ssh_common_args`: Optional. Specify options to pass to `ssh`. 35 | * `ssh_private_key`: Optional. The `ssh` private key with which to connect to the remote system. 36 | * `vault_password`: Optional. The value of the `ansible-vault` password. 37 | * `verbose`: Optional. Specify, `v`, `vv`, etc., to increase the verbosity of the 38 | `ansible-playbook` execution. 39 | 40 | ### Example 41 | 42 | ```yaml 43 | resource_types: 44 | - name: ansible-playbook 45 | type: docker-image 46 | source: 47 | repository: troykinsella/concourse-ansible-playbook-resource 48 | tag: latest 49 | 50 | resources: 51 | - name: ansible 52 | type: ansible-playbook 53 | source: 54 | debug: false 55 | user: ubuntu 56 | ssh_private_key: ((ansible_ssh_private_key)) 57 | vault_password: ((ansible_vault_password)) 58 | verbose: v 59 | ``` 60 | 61 | ## Behaviour 62 | 63 | ### `check`: No Op 64 | 65 | ### `in`: No Op 66 | 67 | ### `out`: Execute `ansible` Playbook 68 | 69 | Execute `ansible-playbook` against a given playbook and inventory file, 70 | firstly installing dependencies with `ansible-galaxy install -r requirements.yml` if necessary. 71 | 72 | Prior to running `ansible-playbook`, if an `ansible.cfg` file is present in the 73 | `path` directory, it is sanitized by removing entries for which the equivalent 74 | command line options are managed by this resource. The result of this sanitization 75 | can be seen by setting `source.debug: true`. 76 | 77 | #### Parameters 78 | 79 | Most parameters map directly to `ansible-playbook` options. See the 80 | `ansible-playbook --help` for further details. 81 | 82 | * `become`: Optional. Boolean. Default `false`. Run operations as `become` (privilege escalation). 83 | * `become_user`: Optional. Run operations with this user. 84 | * `become_method`: Optional. Privilege escalation method to use. 85 | * `check`: Optional. Boolean. Default `false`. Don't make any changes; 86 | instead, try to predict some of the changes that may occur. 87 | * `diff`: Optional. Boolean. Default `false`. When changing (small) files and 88 | templates, show the differences in those files; works great with `check: true`. 89 | * `inventory`: Required. The path to the inventory file to use, relative 90 | to `path`. 91 | * `limit`: Optional. Limit the playbook run to provided hosts/groups. 92 | * `playbook`: Optional. Default `site.yml`. The path to the playbook file to run, 93 | relative to `path`. 94 | * `skip_tags`: Optional. Only run plays and tasks not tagged with this list of values. 95 | * `setup_commands`: Optional. A list of shell commands to run before executing the playbook. 96 | See the `Custom Setup Commands` section for explanation. 97 | * `tags`: Optional. Only run plays and tasks tagged with this list of values. 98 | * `vars`: Optional. An object of extra variables to pass to `ansible-playbook`. 99 | Mutually exclusive with `vars_file`. 100 | * `vars_file`: Optional. A file containing a JSON object of extra variables 101 | to pass to `ansible-playbook`. Mutually exclusive with `vars`. 102 | * `path`: Required. The path to the directory containing playbook sources. This typically 103 | will point to a resource pulled from source control. 104 | 105 | #### Custom Setup Commands 106 | 107 | As there are a myriad of Ansible modules, each of which having specific system dependencies, 108 | it becomes untenable for all of them to be supported by this resource Docker image. 109 | The `setup_commands` parameter of the `put` operation allows the pipeline manager to 110 | install system packages known to be depended upon by the particular playbooks being executed. 111 | Of course, this flexibility comes at the cost of having to execute the commands upon 112 | every `put`. That said, this Concourse resource does intend to supply a set of dependencies out 113 | of the box to support the most common or basic Ansible modules. Please open a ticket 114 | requesting the addition of a system package if it can be rationalized that it will benefit 115 | a wide variety of use cases. 116 | 117 | #### Example 118 | 119 | ```yaml 120 | # Extends example in Source Configuration 121 | 122 | jobs: 123 | - name: provision-frontend 124 | plan: 125 | - get: master # git resource 126 | - put: ansible 127 | params: 128 | check: true 129 | diff: true 130 | inventory: inventory/some-hosts.yml 131 | playbook: site.yml 132 | path: master 133 | ``` 134 | 135 | ## Testing 136 | 137 | ```bash 138 | docker build . 139 | ``` 140 | 141 | ## License 142 | 143 | MIT © Troy Kinsella 144 | -------------------------------------------------------------------------------- /assets/lib/commands/out.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require_relative '../ansible_galaxy' 4 | require_relative '../ansible_playbook' 5 | require_relative '../git_config' 6 | require_relative '../input' 7 | require_relative '../ssh_config' 8 | 9 | require 'fileutils' 10 | 11 | module Commands 12 | 13 | class Out 14 | 15 | SSH_KEY_PATH = "/tmp/ansible-playbook-resource-ssh-private-key" 16 | GIT_KEY_PATH = "/tmp/ansible-playbook-resource-git-private-key" 17 | 18 | attr_reader :destination 19 | attr_reader :input 20 | 21 | def initialize(destination:, input: Input.instance) 22 | @destination = destination 23 | @input = input 24 | 25 | @ssh_config = SSHConfig.new source.debug 26 | end 27 | 28 | def source 29 | input.source 30 | end 31 | 32 | def require_source(name) 33 | v = source[name] 34 | raise InputError, %("source.#{name}" must be defined) if v.nil? 35 | v 36 | end 37 | 38 | def params 39 | input.params 40 | end 41 | 42 | def require_param(name) 43 | v = params[name] 44 | raise InputError, %("params.#{name}" must be defined) if v.nil? 45 | v 46 | end 47 | 48 | def path 49 | @path ||= begin 50 | p = File.join(destination, require_param('path')) 51 | raise InputError, %(params.path: "#{params.path}" does not exist) unless File.exist?(p) 52 | p 53 | end 54 | end 55 | 56 | def configure_ssh! 57 | debug "Configuring ssh..." 58 | key = source.ssh_private_key 59 | if !key.nil? 60 | @ssh_config.create_key_file! SSH_KEY_PATH, key 61 | end 62 | @ssh_config.configure! 63 | end 64 | 65 | def configure_git! 66 | debug "Configuring git..." 67 | key = source.git_private_key 68 | if !key.nil? 69 | @ssh_config.create_key_file! GIT_KEY_PATH, key 70 | @ssh_config.ssh_add_key! GIT_KEY_PATH 71 | end 72 | 73 | git_config = GitConfig.new source.debug 74 | git_config.skip_ssl_verification! source.git_skip_ssl_verification 75 | git_config.configure_https_credentials! source.git_https_username, source.git_https_password 76 | git_config.configure_git_global! source.git_global_config 77 | end 78 | 79 | def configure_ansible! 80 | # Sanitize ansible.cfg 81 | ansible_cfg_path = "ansible.cfg" 82 | if File.exists? ansible_cfg_path 83 | debug "Sanitizing ansible.cfg..." 84 | 85 | # Never allow a vault password file that may have come from source control :P 86 | `sed -i '/vault_password_file[[:space:]]*=/d' #{ansible_cfg_path}` 87 | 88 | # Never allow a private key file that may have come from source control :P 89 | `sed -i '/private_key_file[[:space:]]*=/d' #{ansible_cfg_path}` 90 | 91 | # Never prompt for a vault password 92 | `sed -i '/ask_vault_pass[[:space:]]*=/d' #{ansible_cfg_path}` 93 | 94 | # Never prompt for a become password 95 | `sed -i '/become_ask_pass[[:space:]]*=/d' #{ansible_cfg_path}` 96 | 97 | # Force certain ansible-playbook command line options to take 98 | # precedence over ansible.cfg entries 99 | if params.become 100 | `sed -i '/become[[:space:]]*=/d' #{ansible_cfg_path}` 101 | end 102 | if !params.become_method.nil? 103 | `sed -i '/become_method[[:space:]]*=/d' #{ansible_cfg_path}` 104 | end 105 | if !params.become_user.nil? 106 | `sed -i '/become_user[[:space:]]*=/d' #{ansible_cfg_path}` 107 | end 108 | if !params.inventory.nil? 109 | `sed -i '/inventory[[:space:]]*=/d' #{ansible_cfg_path}` 110 | end 111 | if !source.user.nil? 112 | `sed -i '/remote_user[[:space:]]*=/d' #{ansible_cfg_path}` 113 | end 114 | if !source.verbose.nil? 115 | `sed -i '/verbosity[[:space:]]*=/d' #{ansible_cfg_path}` 116 | end 117 | 118 | if source.debug 119 | puts "Sanitized ansible.cfg:" 120 | puts File.read(ansible_cfg_path) 121 | puts 122 | end 123 | end 124 | end 125 | 126 | def create_vault_password_file! 127 | vp = source.vault_password 128 | if !vp.nil? 129 | vp_path = "/tmp/ansible-playbook-resource-ansible-vault-password" 130 | File.write vp_path, vp 131 | 132 | debug "Wrote vault password file: #{vp_path}" 133 | 134 | vp_path 135 | end 136 | end 137 | 138 | def install_requirements! 139 | ag = AnsibleGalaxy.new source.debug 140 | 141 | req = source.requirements 142 | if !req.nil? 143 | raise InputError, %(source.requirements: "#{source.requirements}" does not exist) unless File.exists? source.requirements 144 | end 145 | 146 | ag.requirements = req 147 | ag.verbose = source.verbose 148 | code = ag.install! 149 | exit(code) unless code == 0 150 | end 151 | 152 | def run_setup_commands! 153 | (params.setup_commands || []).each do |setup_command| 154 | debug "Running setup command: #{setup_command}" 155 | 156 | system setup_command 157 | end 158 | end 159 | 160 | def run_playbook! 161 | debug "Executing ansible-playbook..." 162 | 163 | ap = AnsiblePlaybook.new source.debug 164 | 165 | ap.become = params.become 166 | ap.become_user = params.become_user 167 | ap.become_method = params.become_method 168 | ap.check = params.check 169 | ap.diff = params.diff 170 | ap.env = ENV.to_hash.merge(source.env || {}) 171 | ap.extra_vars = params.vars 172 | ap.limit = params.limit 173 | ap.inventory = require_param 'inventory' 174 | ap.playbook = params.playbook 175 | ap.private_key = source.ssh_private_key ? SSH_KEY_PATH : nil 176 | ap.tags = params.tags 177 | ap.user = source.user 178 | ap.skip_tags = params.skip_tags 179 | ap.ssh_common_args = source.ssh_common_args 180 | ap.vault_password_file = create_vault_password_file! 181 | ap.verbose = source.verbose 182 | 183 | ap.execute! 184 | end 185 | 186 | def debug(msg) 187 | puts(msg) if source.debug 188 | end 189 | 190 | def run! 191 | Dir.chdir path 192 | 193 | configure_ssh! 194 | configure_git! 195 | configure_ansible! 196 | run_setup_commands! 197 | install_requirements! 198 | run_playbook! 199 | end 200 | 201 | end 202 | 203 | end 204 | 205 | if $PROGRAM_NAME == __FILE__ 206 | command = Commands::Out.new(destination: ARGV.shift) 207 | begin 208 | command.run! 209 | rescue InputError => e 210 | STDERR.puts e.message 211 | exit(1) 212 | end 213 | end 214 | -------------------------------------------------------------------------------- /spec/integration/playbook_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'open3' 3 | require 'json' 4 | 5 | describe "integration:playbook" do 6 | 7 | let(:out_file) { '/opt/resource/out' } 8 | let(:mockelton_out) { '/resource/spec/fixtures/mockleton.out' } 9 | let(:ssh_private_key_file) { '/tmp/ansible-playbook-resource-ssh-private-key' } 10 | 11 | after(:each) do 12 | File.delete mockelton_out if File.exists? mockelton_out 13 | end 14 | 15 | it "calls ansible-playbook with minimal arguments" do 16 | stdin = { 17 | "source" => { 18 | "ssh_private_key" => "key" 19 | }, 20 | "params" => { 21 | "path" => "spec/fixtures", 22 | "inventory" => "the_inventory" 23 | } 24 | }.to_json 25 | 26 | stdout, stderr, status = Open3.capture3("#{out_file} .", :stdin_data => stdin) 27 | 28 | expect(status.success?).to be true 29 | 30 | out = JSON.parse(File.read(mockelton_out)) 31 | 32 | expect(out["sequence"].size).to be 2 33 | expect(out["sequence"][1]["exec-spec"]["args"]).to eq [ 34 | "ansible-playbook", 35 | "-i", 36 | "the_inventory", 37 | "--private-key", 38 | ssh_private_key_file, 39 | "site.yml" 40 | ] 41 | end 42 | 43 | it "calls ansible-playbook with param.become" do 44 | stdin = { 45 | "source" => { 46 | "ssh_private_key" => "key" 47 | }, 48 | "params" => { 49 | "path" => "spec/fixtures", 50 | "inventory" => "the_inventory", 51 | "become" => true 52 | } 53 | }.to_json 54 | 55 | stdout, stderr, status = Open3.capture3("#{out_file} .", :stdin_data => stdin) 56 | 57 | expect(status.success?).to be true 58 | 59 | out = JSON.parse(File.read(mockelton_out)) 60 | 61 | expect(out["sequence"].size).to be 2 62 | expect(out["sequence"][1]["exec-spec"]["args"]).to eq [ 63 | "ansible-playbook", 64 | "--become", 65 | "-i", 66 | "the_inventory", 67 | "--private-key", 68 | ssh_private_key_file, 69 | "site.yml" 70 | ] 71 | end 72 | 73 | it "calls ansible-playbook with param.become_user" do 74 | stdin = { 75 | "source" => { 76 | "ssh_private_key" => "key" 77 | }, 78 | "params" => { 79 | "path" => "spec/fixtures", 80 | "inventory" => "the_inventory", 81 | "become_user" => "ted" 82 | } 83 | }.to_json 84 | 85 | stdout, stderr, status = Open3.capture3("#{out_file} .", :stdin_data => stdin) 86 | 87 | expect(status.success?).to be true 88 | 89 | out = JSON.parse(File.read(mockelton_out)) 90 | 91 | expect(out["sequence"].size).to be 2 92 | expect(out["sequence"][1]["exec-spec"]["args"]).to eq [ 93 | "ansible-playbook", 94 | "--become-user", 95 | "ted", 96 | "-i", 97 | "the_inventory", 98 | "--private-key", 99 | ssh_private_key_file, 100 | "site.yml" 101 | ] 102 | end 103 | 104 | it "calls ansible-playbook with param.become_user" do 105 | stdin = { 106 | "source" => { 107 | "ssh_private_key" => "key" 108 | }, 109 | "params" => { 110 | "path" => "spec/fixtures", 111 | "inventory" => "the_inventory", 112 | "become_method" => "sudo" 113 | } 114 | }.to_json 115 | 116 | stdout, stderr, status = Open3.capture3("#{out_file} .", :stdin_data => stdin) 117 | 118 | expect(status.success?).to be true 119 | 120 | out = JSON.parse(File.read(mockelton_out)) 121 | 122 | expect(out["sequence"].size).to be 2 123 | expect(out["sequence"][1]["exec-spec"]["args"]).to eq [ 124 | "ansible-playbook", 125 | "--become-method", 126 | "sudo", 127 | "-i", 128 | "the_inventory", 129 | "--private-key", 130 | ssh_private_key_file, 131 | "site.yml" 132 | ] 133 | end 134 | 135 | it "calls ansible-playbook with param.check" do 136 | stdin = { 137 | "source" => { 138 | "ssh_private_key" => "key" 139 | }, 140 | "params" => { 141 | "path" => "spec/fixtures", 142 | "inventory" => "the_inventory", 143 | "check" => true 144 | } 145 | }.to_json 146 | 147 | stdout, stderr, status = Open3.capture3("#{out_file} .", :stdin_data => stdin) 148 | 149 | expect(status.success?).to be true 150 | 151 | out = JSON.parse(File.read(mockelton_out)) 152 | 153 | expect(out["sequence"].size).to be 2 154 | expect(out["sequence"][1]["exec-spec"]["args"]).to eq [ 155 | "ansible-playbook", 156 | "--check", 157 | "-i", 158 | "the_inventory", 159 | "--private-key", 160 | ssh_private_key_file, 161 | "site.yml" 162 | ] 163 | end 164 | 165 | it "calls ansible-playbook with param.diff" do 166 | stdin = { 167 | "source" => { 168 | "ssh_private_key" => "key" 169 | }, 170 | "params" => { 171 | "path" => "spec/fixtures", 172 | "inventory" => "the_inventory", 173 | "diff" => true 174 | } 175 | }.to_json 176 | 177 | stdout, stderr, status = Open3.capture3("#{out_file} .", :stdin_data => stdin) 178 | 179 | expect(status.success?).to be true 180 | 181 | out = JSON.parse(File.read(mockelton_out)) 182 | 183 | expect(out["sequence"].size).to be 2 184 | expect(out["sequence"][1]["exec-spec"]["args"]).to eq [ 185 | "ansible-playbook", 186 | "--diff", 187 | "-i", 188 | "the_inventory", 189 | "--private-key", 190 | ssh_private_key_file, 191 | "site.yml" 192 | ] 193 | end 194 | 195 | it "calls ansible-playbook with source.user" do 196 | stdin = { 197 | "source" => { 198 | "ssh_private_key" => "key", 199 | "user" => "reggie" 200 | }, 201 | "params" => { 202 | "path" => "spec/fixtures", 203 | "inventory" => "the_inventory", 204 | "diff" => true 205 | } 206 | }.to_json 207 | 208 | stdout, stderr, status = Open3.capture3("#{out_file} .", :stdin_data => stdin) 209 | 210 | expect(status.success?).to be true 211 | 212 | out = JSON.parse(File.read(mockelton_out)) 213 | 214 | expect(out["sequence"].size).to be 2 215 | expect(out["sequence"][1]["exec-spec"]["args"]).to eq [ 216 | "ansible-playbook", 217 | "--diff", 218 | "-i", 219 | "the_inventory", 220 | "--private-key", 221 | ssh_private_key_file, 222 | "--user", 223 | "reggie", 224 | "site.yml" 225 | ] 226 | end 227 | 228 | it "calls ansible-playbook with source.ssh_common_args" do 229 | stdin = { 230 | "source" => { 231 | "ssh_private_key" => "key", 232 | "ssh_common_args" => "-o foo" 233 | }, 234 | "params" => { 235 | "path" => "spec/fixtures", 236 | "inventory" => "the_inventory", 237 | "diff" => true 238 | } 239 | }.to_json 240 | 241 | stdout, stderr, status = Open3.capture3("#{out_file} .", :stdin_data => stdin) 242 | 243 | expect(status.success?).to be true 244 | 245 | out = JSON.parse(File.read(mockelton_out)) 246 | 247 | expect(out["sequence"].size).to be 2 248 | expect(out["sequence"][1]["exec-spec"]["args"]).to eq [ 249 | "ansible-playbook", 250 | "--diff", 251 | "-i", 252 | "the_inventory", 253 | "--private-key", 254 | ssh_private_key_file, 255 | "--ssh-common-args", 256 | "-o foo", 257 | "site.yml" 258 | ] 259 | end 260 | 261 | it "calls ansible-playbook with params.tags" do 262 | stdin = { 263 | "source" => { 264 | "ssh_private_key" => "key" 265 | }, 266 | "params" => { 267 | "path" => "spec/fixtures", 268 | "inventory" => "the_inventory", 269 | "diff" => true, 270 | "tags" => [ "foo", "bar" ] 271 | } 272 | }.to_json 273 | 274 | stdout, stderr, status = Open3.capture3("#{out_file} .", :stdin_data => stdin) 275 | 276 | expect(status.success?).to be true 277 | 278 | out = JSON.parse(File.read(mockelton_out)) 279 | 280 | expect(out["sequence"].size).to be 2 281 | expect(out["sequence"][1]["exec-spec"]["args"]).to eq [ 282 | "ansible-playbook", 283 | "--diff", 284 | "-i", 285 | "the_inventory", 286 | "--private-key", 287 | ssh_private_key_file, 288 | "-t", 289 | "foo", 290 | "-t", 291 | "bar", 292 | "site.yml" 293 | ] 294 | end 295 | 296 | it "calls ansible-playbook with params.skip_tags" do 297 | stdin = { 298 | "source" => { 299 | "ssh_private_key" => "key" 300 | }, 301 | "params" => { 302 | "path" => "spec/fixtures", 303 | "inventory" => "the_inventory", 304 | "diff" => true, 305 | "skip_tags" => [ "foo", "bar" ] 306 | } 307 | }.to_json 308 | 309 | stdout, stderr, status = Open3.capture3("#{out_file} .", :stdin_data => stdin) 310 | 311 | expect(status.success?).to be true 312 | 313 | out = JSON.parse(File.read(mockelton_out)) 314 | 315 | expect(out["sequence"].size).to be 2 316 | expect(out["sequence"][1]["exec-spec"]["args"]).to eq [ 317 | "ansible-playbook", 318 | "--diff", 319 | "-i", 320 | "the_inventory", 321 | "--private-key", 322 | ssh_private_key_file, 323 | "--skip-tags", 324 | "foo", 325 | "--skip-tags", 326 | "bar", 327 | "site.yml" 328 | ] 329 | end 330 | 331 | it "calls ansible-playbook with source.vault_password" do 332 | stdin = { 333 | "source" => { 334 | "ssh_private_key" => "key", 335 | "vault_password" => "asdf" 336 | }, 337 | "params" => { 338 | "path" => "spec/fixtures", 339 | "inventory" => "the_inventory", 340 | "diff" => true 341 | } 342 | }.to_json 343 | 344 | stdout, stderr, status = Open3.capture3("#{out_file} .", :stdin_data => stdin) 345 | 346 | expect(status.success?).to be true 347 | 348 | out = JSON.parse(File.read(mockelton_out)) 349 | 350 | expect(out["sequence"].size).to be 2 351 | expect(out["sequence"][1]["exec-spec"]["args"]).to eq [ 352 | "ansible-playbook", 353 | "--diff", 354 | "-i", 355 | "the_inventory", 356 | "--private-key", 357 | ssh_private_key_file, 358 | "--vault-password-file", 359 | "/tmp/ansible-playbook-resource-ansible-vault-password", 360 | "site.yml" 361 | ] 362 | 363 | vp = File.read("/tmp/ansible-playbook-resource-ansible-vault-password") 364 | expect(vp).to eq "asdf" 365 | end 366 | 367 | it "calls ansible-playbook with param.vars" do 368 | stdin = { 369 | "source" => { 370 | "ssh_private_key" => "key" 371 | }, 372 | "params" => { 373 | "path" => "spec/fixtures", 374 | "inventory" => "the_inventory", 375 | "vars" => { 376 | "foo" => "bar", 377 | "baz" => [ "wokka" ], 378 | "biz" => { 379 | "booze" => "yes please" 380 | } 381 | } 382 | } 383 | }.to_json 384 | 385 | stdout, stderr, status = Open3.capture3("#{out_file} .", :stdin_data => stdin) 386 | 387 | expect(status.success?).to be true 388 | 389 | out = JSON.parse(File.read(mockelton_out)) 390 | 391 | expect(out["sequence"].size).to be 2 392 | expect(out["sequence"][1]["exec-spec"]["args"]).to eq [ 393 | "ansible-playbook", 394 | "--extra-vars", 395 | '{"foo":"bar","baz":["wokka"],"biz":{"booze":"yes please"}}', 396 | "-i", 397 | "the_inventory", 398 | "--private-key", 399 | ssh_private_key_file, 400 | "site.yml" 401 | ] 402 | end 403 | 404 | it "calls ansible-playbook with source.verbose" do 405 | stdin = { 406 | "source" => { 407 | "ssh_private_key" => "key", 408 | "verbose" => "vv" 409 | }, 410 | "params" => { 411 | "path" => "spec/fixtures", 412 | "inventory" => "the_inventory" 413 | } 414 | }.to_json 415 | 416 | stdout, stderr, status = Open3.capture3("#{out_file} .", :stdin_data => stdin) 417 | 418 | expect(status.success?).to be true 419 | 420 | out = JSON.parse(File.read(mockelton_out)) 421 | 422 | expect(out["sequence"].size).to be 2 423 | expect(out["sequence"][1]["exec-spec"]["args"]).to eq [ 424 | "ansible-playbook", 425 | "-i", 426 | "the_inventory", 427 | "--private-key", 428 | ssh_private_key_file, 429 | "-vv", 430 | "site.yml" 431 | ] 432 | end 433 | 434 | it "calls ansible-playbook with params.limit" do 435 | stdin = { 436 | "source" => { 437 | "ssh_private_key" => "key", 438 | "verbose" => "vv" 439 | }, 440 | "params" => { 441 | "path" => "spec/fixtures", 442 | "inventory" => "the_inventory", 443 | "limit" => "foobar" 444 | } 445 | }.to_json 446 | 447 | stdout, stderr, status = Open3.capture3("#{out_file} .", :stdin_data => stdin) 448 | 449 | expect(status.success?).to be true 450 | 451 | out = JSON.parse(File.read(mockelton_out)) 452 | 453 | expect(out["sequence"].size).to be 2 454 | expect(out["sequence"][1]["exec-spec"]["args"]).to eq [ 455 | "ansible-playbook", 456 | "-i", 457 | "the_inventory", 458 | "--limit", 459 | "foobar", 460 | "--private-key", 461 | ssh_private_key_file, 462 | "-vv", 463 | "site.yml" 464 | ] 465 | end 466 | 467 | it "runs setup_commands" do 468 | stdin = { 469 | "source" => { 470 | "ssh_private_key" => "key" 471 | }, 472 | "params" => { 473 | "path" => "spec/fixtures", 474 | "inventory" => "the_inventory", 475 | "setup_commands" => [ 476 | "touch /foo_burger", 477 | ] 478 | } 479 | }.to_json 480 | 481 | stdout, stderr, status = Open3.capture3("#{out_file} .", :stdin_data => stdin) 482 | 483 | expect(status.success?).to be true 484 | 485 | out = JSON.parse(File.read(mockelton_out)) 486 | 487 | expect(out["sequence"].size).to be 2 488 | expect(out["sequence"][1]["exec-spec"]["args"]).to eq [ 489 | "ansible-playbook", 490 | "-i", 491 | "the_inventory", 492 | "--private-key", 493 | ssh_private_key_file, 494 | "site.yml" 495 | ] 496 | 497 | system("test -f /foo_burger") 498 | expect($?.exitstatus).to be 0 499 | end 500 | 501 | end 502 | --------------------------------------------------------------------------------