Merge pull request #5443 from inspec/cw/timeouts

Add timeout option to command resource
This commit is contained in:
Clinton Wolfe 2021-04-04 22:25:09 -04:00 committed by GitHub
commit 8286ec8072
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 108 additions and 1 deletions

View file

@ -258,6 +258,8 @@ This subcommand has additional options:
Specifies the bastion port if applicable
* ``--bastion-user=BASTION_USER``
Specifies the bastion user if applicable
* ``--command-timeout=SECONDS``
Maximum seconds to allow a command to run. Default 3600.
* ``--config=CONFIG``
Read configuration from JSON file (`-` reads from stdin).
* ``--controls=one two three``
@ -422,6 +424,8 @@ This subcommand has additional options:
Specifies the bastion user if applicable
* ``-c``, ``--command=COMMAND``
A single command string to run instead of launching the shell
* ``--command-timeout=SECONDS``
Maximum seconds to allow a command to run. Default 3600.
* ``--config=CONFIG``
Read configuration from JSON file (`-` reads from stdin).
* ``--depends=one two three``

View file

@ -136,6 +136,30 @@ Wix includes several tools -- such as `candle` (preprocesses and compiles source
end
end
### Timing Out Long-Running Commands
On target platforms that support the feature, the command resource takes an optional `timeout:` parameter which specifies how long the command may run in seconds before erroring out and failing the control.
```ruby
describe command("find / -owner badguy", timeout: 300) do
its("stdout") { should be_empty }
end
```
This example would run the `find` command for up to 300 seconds, then give up and fail the control if it exceeded that time.
On supported target platforms, the default timeout is 3600 seconds (one hour).
Aside from setting the value on a per-resource basis, you may also use the `--command-timeout` CLI option to globally set a command timeout. The CLI option takes precedence over any per-resource `timeout:` options.
Currently supported target platforms include:
* Local Unix-like OSes, including macOS
* SSH targets
* Windows targets via WinRM
Any target platforms not listed are not supported at this time.
On unsupported platforms, the timeout value is ignored and the command will run indefinitely.
### Redacting Sensitive Commands
By default the command that is ran is shown in the Chef InSpec output. This can be problematic if the command contains sensitive arguments such as a password. These sensitive parts can be redacted by passing in `redact_regex` and a regular expression to redact. Optionally, you can use 2 capture groups to fine tune what is redacted.

View file

@ -166,6 +166,9 @@ module Inspec
desc: "After normal execution order, results are sorted by control ID, or by file (default), or randomly. None uses legacy unsorted mode."
option :filter_empty_profiles, type: :boolean, default: false,
desc: "Filter empty profiles (profiles without controls) from the report."
option :command_timeout, type: :numeric, default: 3600,
desc: "Maximum seconds to allow commands to run during execution. Default 3600.",
long_desc: "Maximum seconds to allow commands to run during execution. Default 3600. A timed out command is considered an error."
end
def self.help(*args)

View file

@ -321,6 +321,9 @@ class Inspec::InspecCLI < Inspec::BaseCLI
desc: "A space-delimited list of local folders containing profiles whose libraries and resources will be loaded into the new shell"
option :distinct_exit, type: :boolean, default: true,
desc: "Exit with code 100 if any tests fail, and 101 if any are skipped but none failed (default). If disabled, exit 0 on skips and 1 for failures."
option :command_timeout, type: :numeric, default: 3600,
desc: "Maximum seconds to allow a command to run. Default 3600.",
long_desc: "Maximum seconds to allow commands to run. Default 3600. A timed out command is considered an error."
option :inspect, type: :boolean, default: false, desc: "Use verbose/debugging output for resources."
def shell_func
o = config

View file

@ -32,6 +32,16 @@ module Inspec::Resources
@command = cmd
cli_timeout = Inspec::Config.cached["command_timeout"].to_i
# Can access this via Inspec::InspecCLI.commands["exec"].options[:command_timeout].default,
# but that may not be loaded for kitchen-inspec and other pure gem consumers
default_cli_timeout = 3600
if cli_timeout != default_cli_timeout
@timeout = cli_timeout
else
@timeout = options[:timeout]&.to_i || default_cli_timeout
end
if options[:redact_regex]
unless options[:redact_regex].is_a?(Regexp)
# Make sure command is replaced so sensitive output isn't shown
@ -44,7 +54,15 @@ module Inspec::Resources
end
def result
@result ||= inspec.backend.run_command(@command)
@result ||= begin
inspec.backend.run_command(@command, timeout: @timeout)
rescue Train::CommandTimeoutReached
# Without a small sleep, the train connection gets broken
# We've already timed out, so a small sleep is not likely to be painful here.
sleep 0.1
raise Inspec::Exceptions::ResourceFailed,
"Command `#{@command}` timed out after #{@timeout} seconds"
end
end
def stdout

View file

@ -0,0 +1,11 @@
control "timeout-test-01" do
# Do something that will timeout, with inline timeout option
describe command("sleep 10; echo oops", timeout: 2) do
its("stdout") { should be_empty }
end
# Validate that the connection still works after a timeout
describe command("echo hello") do
its("exit_status") { should cmp 0 }
end
end

View file

@ -0,0 +1,10 @@
name: sleepy
title: InSpec Profile
maintainer: The Authors
copyright: The Authors
copyright_email: you@example.com
license: Apache-2.0
summary: A profile that contains tests that take a long time to execute, to test the command timeout feature
version: 0.1.0
supports:
platform: os

View file

@ -1051,4 +1051,37 @@ Test Summary: 2 successful, 0 failures, 0 skipped\n"
end
end
end
describe "when running a profile using timeouts on a command resource" do
let(:profile) { "#{profile_path}/timeouts" }
describe "when using the DSL command resource option" do
let(:run_result) { run_inspec_process("exec #{profile}") }
it "properly timesout an inlined command resource" do
# Command timeout not available on local windows pipe train transports
skip if windows?
_(run_result.stderr).must_be_empty
# Control with inline timeout should be interrupted correctly
_(run_result.stdout).must_include "Command `sleep 10; echo oops` timed out after 2 seconds"
# Subsequent control must still run correctly
_(run_result.stdout).must_include "Command: `echo hello` exit_status is expected to cmp == 0"
end
end
describe "when using the CLI option to override the command timeout" do
let(:run_result) { run_inspec_process("exec #{profile} --command-timeout 1") }
it "properly overrides the DSL setting with the CLI timeout option" do
# Command timeout not available on local windows pipe train transports
skip if windows?
_(run_result.stderr).must_be_empty
# Command timeout should be interrupted correctly, with CLI timeout applied
_(run_result.stdout).must_include "Command `sleep 10; echo oops` timed out after 1 seconds"
# Subsequent control must still run correctly
_(run_result.stdout).must_include "Command: `echo hello` exit_status is expected to cmp == 0"
end
end
end
end

View file

@ -95,6 +95,7 @@ describe "Inspec::Resources::JSON" do
# stdout:empty, stderr:empty
def run_json_cmd(cmd)
Inspec::Config.cached["command_timeout"] = 3600 # Reset to default
quick_resource("json", :linux, command: cmd)
end