Fix for issue #7413 - Allow non-zero exit codes for inspec provisioner (#10723)

This commit is contained in:
finchr 2021-03-12 07:47:21 -08:00 committed by GitHub
parent cf65b7b494
commit d1254a5e48
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 118 additions and 8 deletions

View File

@ -23,6 +23,7 @@ import (
"strconv"
"strings"
"sync"
"syscall"
"unicode"
"golang.org/x/crypto/ssh"
@ -59,6 +60,7 @@ type Config struct {
LocalPort int `mapstructure:"local_port"`
SSHHostKeyFile string `mapstructure:"ssh_host_key_file"`
SSHAuthorizedKeyFile string `mapstructure:"ssh_authorized_key_file"`
ValidExitCodes []int `mapstructure:"valid_exit_codes"`
}
type Provisioner struct {
@ -157,6 +159,10 @@ func (p *Provisioner) Prepare(raws ...interface{}) error {
errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("user: could not determine current user from environment."))
}
if p.config.ValidExitCodes == nil {
p.config.ValidExitCodes = []int{0, 101}
}
if errs != nil && len(errs.Errors) > 0 {
return errs
}
@ -170,7 +176,7 @@ func (p *Provisioner) getVersion() error {
"Error running \"%s version\": %s", p.config.Command, err.Error())
}
versionRe := regexp.MustCompile(`\w (\d+\.\d+[.\d+]*)`)
versionRe := regexp.MustCompile(`(\d+\.\d+[.\d+]*)`)
matches := versionRe.FindStringSubmatch(string(out))
if matches == nil {
return fmt.Errorf(
@ -412,9 +418,33 @@ func (p *Provisioner) executeInspec(ui packersdk.Ui, comm packersdk.Communicator
return err
}
wg.Wait()
err = cmd.Wait()
if err != nil {
return fmt.Errorf("Non-zero exit status: %s", err)
if err := cmd.Wait(); err != nil {
if exiterr, ok := err.(*exec.ExitError); ok {
// The program has exited with an exit code != 0
// This works on both Unix and Windows. Although package
// syscall is generally platform dependent, WaitStatus is
// defined for both Unix and Windows and in both cases has
// an ExitStatus() method with the same signature.
if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
exitStatus := status.ExitStatus()
// Check exit code against allowed codes (likely just 0)
validExitCode := false
for _, v := range p.config.ValidExitCodes {
if exitStatus == v {
validExitCode = true
}
}
if !validExitCode {
return fmt.Errorf(
"Inspec exited with unexpected exit status: %d. Expected exit codes are: %v",
exitStatus, p.config.ValidExitCodes)
}
}
} else {
return fmt.Errorf("Unable to get exit status: %s", err)
}
}
return nil

View File

@ -30,6 +30,7 @@ type FlatConfig struct {
LocalPort *int `mapstructure:"local_port" cty:"local_port" hcl:"local_port"`
SSHHostKeyFile *string `mapstructure:"ssh_host_key_file" cty:"ssh_host_key_file" hcl:"ssh_host_key_file"`
SSHAuthorizedKeyFile *string `mapstructure:"ssh_authorized_key_file" cty:"ssh_authorized_key_file" hcl:"ssh_authorized_key_file"`
ValidExitCodes []int `mapstructure:"valid_exit_codes" cty:"valid_exit_codes" hcl:"valid_exit_codes"`
}
// FlatMapstructure returns a new FlatConfig.
@ -64,6 +65,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
"local_port": &hcldec.AttrSpec{Name: "local_port", Type: cty.Number, Required: false},
"ssh_host_key_file": &hcldec.AttrSpec{Name: "ssh_host_key_file", Type: cty.String, Required: false},
"ssh_authorized_key_file": &hcldec.AttrSpec{Name: "ssh_authorized_key_file", Type: cty.String, Required: false},
"valid_exit_codes": &hcldec.AttrSpec{Name: "valid_exit_codes", Type: cty.List(cty.Number), Required: false},
}
return s
}

View File

@ -1,12 +1,15 @@
package inspec
import (
"bytes"
"context"
"crypto/rand"
"fmt"
"io"
"io/ioutil"
"os"
"path"
"runtime"
"strings"
"testing"
@ -268,12 +271,17 @@ func TestProvisionerPrepare_LocalPort(t *testing.T) {
}
func TestInspecGetVersion(t *testing.T) {
if os.Getenv("PACKER_ACC") == "" {
t.Skip("This test is only run with PACKER_ACC=1 and it requires InSpec to be installed")
var p Provisioner
if os.Getenv("PACKER_ACC") == "1" {
p.config.Command = "inspec"
} else {
p.config.Command = "./test-fixtures/inspec_version.sh"
if runtime.GOOS == "windows" {
t.Skip("This test is only run with PACKER_ACC=1 and it requires InSpec to be installed")
}
}
var p Provisioner
p.config.Command = "inspec exec"
p.config.Backend = "local"
err := p.getVersion()
if err != nil {
t.Fatalf("err: %s", err)
@ -291,3 +299,32 @@ func TestInspecGetVersionError(t *testing.T) {
t.Fatal("Error message should include command name")
}
}
func TestInspecValidExitCodes(t *testing.T) {
var p Provisioner
if os.Getenv("PACKER_ACC") == "1" {
p.config.Command = "inspec"
} else {
p.config.Command = "./test-fixtures/valid_exit_codes.sh"
if runtime.GOOS == "windows" {
t.Skip("This test is only run with PACKER_ACC=1 and it requires InSpec to be installed")
}
}
p.config.Backend = "local"
p.config.Profile = "test-fixtures/skip_control.rb"
err := p.Prepare()
if err != nil {
t.Fatalf("err: %s", err)
}
comm := &packersdk.MockCommunicator{}
ui := &packersdk.BasicUi{
Reader: new(bytes.Buffer),
Writer: new(bytes.Buffer),
}
err = p.Provision(context.Background(), ui, comm, make(map[string]interface{}))
if err != nil {
t.Fatalf("err: %s", err)
}
}

View File

@ -0,0 +1,7 @@
#!/bin/sh
cat <<EOB
4.26.13
EOB
exit 0

View File

@ -0,0 +1,13 @@
# copyright: 2018, The Authors
title "skip control"
control "skip-1.0" do
impact 1.0
title "skip control"
desc "skip control to generate a 101 return code"
only_if { 1 == 2 }
describe file("/tmp") do
it { should be_directory }
end
end

View File

@ -0,0 +1,16 @@
#!/bin/sh
cat <<EOB
Profile: tests from test-fixtures/skip_control.rb (tests from test-fixtures/valid_exit_codes.sh)
Version: (not specified)
Target: local://
 ↺ skip-1.0: skip control
 ↺ Skipped control due to only_if condition.
Profile Summary: 0 successful controls, 0 control failures, 1 control skipped
Test Summary: 0 successful, 0 failures, 1 skipped
EOB
exit 101

View File

@ -159,6 +159,11 @@ Optional Parameters:
- `user` (string) - The `--user` to use. Defaults to the user running Packer.
- `valid_exit_codes` (list of ints) - A list of valid exit codes returned by
inspec to be accepted by the provisioner. The default is (0,101) (accept
skipped tests). See [inspec exit codes](https://docs.chef.io/inspec/cli/#exec)
for a list and explanation of inspec exit codes.
@include 'provisioners/common-config.mdx'
## Accepting the InSpec license