diff options
Diffstat (limited to 'spec/acceptance')
2 files changed, 229 insertions, 0 deletions
diff --git a/spec/acceptance/tests/resource/cron/should_not_overwrite_crontab_file_on_file_read_error_spec.rb b/spec/acceptance/tests/resource/cron/should_not_overwrite_crontab_file_on_file_read_error_spec.rb new file mode 100644 index 0000000..08e8bb8 --- /dev/null +++ b/spec/acceptance/tests/resource/cron/should_not_overwrite_crontab_file_on_file_read_error_spec.rb @@ -0,0 +1,114 @@ +require 'spec_helper_acceptance' + +RSpec.context 'when Puppet cannot read a crontab file' do + let(:username) { "pl#{rand(999_999).to_i}" } + let(:crontab_contents) { '6 6 6 6 6 /usr/bin/true' } + + before(:each) do + step 'Create the user on the agents' do + compatible_agents.each do |agent| + user_present(agent, username) + end + end + + step "Set the user's crontab" do + compatible_agents.each do |agent| + run_cron_on(agent, :add, username, crontab_contents) + assert_matching_arrays([crontab_contents], crontab_entries_of(agent, username), "Could not set the user's crontab for testing") + end + end + end + + after(:each) do + step 'Teardown -- Erase the user on the agents' do + compatible_agents.each do |agent| + run_cron_on(agent, :remove, username) + user_absent(agent, username) + end + end + end + + compatible_agents.each do |agent| + it "should not overwrite it on #{agent}" do + crontab_exe = nil + step 'Find the crontab executable' do + crontab_exe = on(agent, 'which crontab').stdout.chomp + end + + stub_crontab_bin_dir = nil + stub_crontab_exe = nil + step 'Create the stub crontab executable that triggers the read error' do + stub_crontab_bin_dir = agent.tmpdir('stub_crontab_bin_dir') + on(agent, "chown #{username} #{stub_crontab_bin_dir}") + + stub_crontab_exe = "#{stub_crontab_bin_dir}/crontab" + stub_crontab_exe_script = <<-SCRIPT + #!/usr/bin/env bash + exit 1 + SCRIPT + create_remote_file(agent, stub_crontab_exe, stub_crontab_exe_script) + on(agent, "chown #{username} #{stub_crontab_exe}") + on(agent, "chmod a+x #{stub_crontab_exe}") + end + + path_env_var = nil + step 'Get the value of the PATH environment variable' do + path_env_var = on(agent, 'echo $PATH').stdout.chomp + end + + step "(PUP-9217) Attempt to overwrite the user's crontab file" do + # This manifest reproduces the issue in PUP-9217. Here's how: + # 1. When Puppet attempts to realize Cron['first_entry'], it will prefetch + # all of the present cron entries on the system. To do this, it will execute + # the crontab command. Since we prepend stub_crontab_bindir to PATH prior to + # executing Puppet, executing the crontab command will really execute + # stub_crontab_exe, which returns an exit code of 1. This triggers our + # read error. + # + # 2. Puppet will attempt to write Cron['first_entry'] onto disk. However, it will + # fail to do so because it will still execute stub_crontab_exe to perform the + # write. Thus, Cron['first_entry'] will fail its evaluation. + # + # 3. Next, Puppet will modify stub_crontab_exe to now execute the actual crontab + # command so that any subsequent calls to crontab will succeed. Note that under + # the hood, Puppet will still be executing stub_crontab_exe. + # + # 4. Finally, Puppet will attempt to realize Cron['second_entry']. It will skip + # the prefetch step, instead proceeding to directly write Cron['second_entry'] + # to disk. But note that since the prefetch failed in (1), Puppet will proceed + # to overwrite our user's crontab file so that it will contain Cron['first_entry'] + # and Cron['second_entry'] (Cron['first_entry'] is there because Puppet maintains + # each crontab file's entries in memory so that when it writes one entry to disk, + # it will write all of them). + manifest = [ + cron_manifest('first_entry', command: 'ls', user: username), + file_manifest(stub_crontab_exe, content: "#!/usr/bin/env bash\n#{crontab_exe} $@"), + cron_manifest('second_entry', command: 'ls', user: username), + ].join("\n\n") + manifest_file = agent.tmpfile('crontab_overwrite_manifest') + create_remote_file(agent, manifest_file, manifest) + + # We need to run a script here instead of a command because: + # * We need to cd into a directory owned by our user. Otherwise, bash will + # fail to execute stub_crontab_exe on AIX and Solaris because we run crontab + # as the given user, and the given user does not have access to Puppet's cwd. + # + # * We also need to pass-in our PATH to Puppet since it contains stub_crontab_bin_dir. + apply_crontab_overwrite_manifest = agent.tmpfile('apply_crontab_overwrite_manifest') + script = <<-SCRIPT + #!/usr/bin/env bash + + cd #{stub_crontab_bin_dir} && puppet apply #{manifest_file} + SCRIPT + create_remote_file(agent, apply_crontab_overwrite_manifest, script) + on(agent, "chmod a+x #{apply_crontab_overwrite_manifest}") + + on(agent, "bash #{apply_crontab_overwrite_manifest}", environment: { PATH: "#{stub_crontab_bin_dir}:#{path_env_var}" }) + end + + step "(PUP-9217) Verify that Puppet does not overwrite the user's crontab file when it fails to read it" do + assert_matching_arrays([crontab_contents], crontab_entries_of(agent, username), "Puppet overwrote the user's crontab file even though it failed to read it") + end + end + end +end diff --git a/spec/acceptance/tests/resource/cron/should_only_fail_associated_resources_on_file_read_error_spec.rb b/spec/acceptance/tests/resource/cron/should_only_fail_associated_resources_on_file_read_error_spec.rb new file mode 100644 index 0000000..3e06d0c --- /dev/null +++ b/spec/acceptance/tests/resource/cron/should_only_fail_associated_resources_on_file_read_error_spec.rb @@ -0,0 +1,115 @@ +require 'spec_helper_acceptance' + +RSpec.context 'when Puppet cannot read a crontab file' do + let(:username) { "pl#{rand(999_999).to_i}" } + let(:failed_username) { "pl#{rand(999_999).to_i}" } + + before(:each) do + step 'Create the users' do + compatible_agents.each do |agent| + user_present(agent, username) + user_present(agent, failed_username) + end + end + end + + after(:each) do + step 'Teardown -- Erase the users' do + compatible_agents.each do |agent| + run_cron_on(agent, :remove, username) + user_absent(agent, username) + + user_absent(agent, failed_username) + end + end + end + + compatible_agents.each do |agent| + it "should only fail the associated resources on #{agent}" do + crontab_exe = nil + step 'Find the crontab executable' do + crontab_exe = on(agent, 'which crontab').stdout.chomp + end + + stub_crontab_bin_dir = nil + stub_crontab_exe = nil + step 'Create the stub crontab executable that triggers the read error for the failed user' do + stub_crontab_bin_dir = agent.tmpdir('stub_crontab_bin_dir') + stub_crontab_exe = "#{stub_crontab_bin_dir}/crontab" + + # On Linux and OSX, we read a user's crontab by running crontab -u <username>, + # where the crontab command is run as root. However on AIX/Solaris, we read a + # user's crontab by running the crontab command as that user. Thus our mock + # crontab executable needs to check if we're reading our failed user's crontab + # (Linux and OSX) OR running crontab as our failed user (AIX and Solaris) before + # triggering the FileReadError + stub_crontab_exe_script = <<-SCRIPT +#!/usr/bin/env bash + if [[ "$@" =~ #{failed_username} || "`id`" =~ #{failed_username} ]]; then + echo "Mocking a FileReadError for the #{failed_username} user's crontab!" + exit 1 +fi + #{crontab_exe} $@ +SCRIPT + + create_remote_file(agent, stub_crontab_exe, stub_crontab_exe_script) + on(agent, "chmod 777 #{stub_crontab_bin_dir}") + on(agent, "chmod 777 #{stub_crontab_exe}") + end + + path_env_var = nil + step 'Get the value of the PATH environment variable' do + path_env_var = on(agent, 'echo $PATH').stdout.chomp + end + + puppet_result = nil + step 'Add some cron entries with Puppet' do + # We delete our mock crontab executable here to ensure that Cron[second_entry]'s + # evaluation fails because of the FileReadError raised in the prefetch + # step. Otherwise, Cron[second_entry]'s evaluation will fail at the write step + # because Puppet would still be invoking our mock crontab executable, which would + # pass the test on an agent that swallows FileReadErrors in the cron provider's + # prefetch step. + manifest = [ + cron_manifest('first_entry', command: 'ls', user: username), + file_manifest(stub_crontab_exe, ensure: :absent), + cron_manifest('second_entry', command: 'ls', user: failed_username), + ].join("\n\n") + manifest_file = agent.tmpfile('crontab_overwrite_manifest') + create_remote_file(agent, manifest_file, manifest) + + # We need to run a script here instead of a command because: + # * We need to cd into a directory that our user can access. Otherwise, bash will + # fail to execute stub_crontab_exe on AIX and Solaris because we run crontab + # as the given user, and the given user does not have access to Puppet's cwd. + # + # * We also need to pass-in our PATH to Puppet since it contains stub_crontab_bin_dir. + apply_crontab_overwrite_manifest = agent.tmpfile('apply_crontab_overwrite_manifest') + script = <<-SCRIPT + #!/usr/bin/env bash + cd #{stub_crontab_bin_dir} && puppet apply #{manifest_file} + SCRIPT + create_remote_file(agent, apply_crontab_overwrite_manifest, script) + on(agent, "chmod a+x #{apply_crontab_overwrite_manifest}") + + puppet_result = on(agent, "bash #{apply_crontab_overwrite_manifest}", environment: { PATH: "#{stub_crontab_bin_dir}:#{path_env_var}" }) + end + + step 'Verify that Puppet fails a Cron resource associated with an unreadable crontab file' do + assert_match(%r{Cron.*second_entry}, puppet_result.stderr, 'Puppet does not fail a Cron resource associated with an unreadable crontab file') + end + + step 'Verify that Puppet does not fail a Cron resource associated with a readable crontab file' do + assert_no_match(%r{Cron.*first_entry}, puppet_result.stderr, 'Puppet fails a Cron resource associated with a readable crontab file') + end + + step 'Verify that Puppet successfully evaluates a Cron resource associated with a readable crontab file' do + assert_match(%r{Cron.*first_entry}, puppet_result.stdout, 'Puppet fails to evaluate a Cron resource associated with a readable crontab file') + end + + step 'Verify that Puppet did update the readable crontab file with the Cron resource' do + assert_matching_arrays(['* * * * * ls'], crontab_entries_of(agent, username), 'Puppet fails to update a readable crontab file with the specified Cron entry') + end + end + end +end |