packer-cn/provisioner/inspec/provisioner.go

573 lines
15 KiB
Go
Raw Normal View History

//go:generate packer-sdc mapstructure-to-hcl2 -type Config
2019-01-20 10:43:47 -05:00
package inspec
import (
"bufio"
"bytes"
"context"
2019-01-20 10:43:47 -05:00
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"net"
"os"
"os/exec"
"os/user"
"regexp"
"strconv"
"strings"
"sync"
"syscall"
2019-01-20 10:43:47 -05:00
"unicode"
"golang.org/x/crypto/ssh"
build using HCL2 (#8423) This follows #8232 which added the code to generate the code required to parse HCL files for each packer component. All old config files of packer will keep on working the same. Packer takes one argument. When a directory is passed, all files in the folder with a name ending with “.pkr.hcl” or “.pkr.json” will be parsed using the HCL2 format. When a file ending with “.pkr.hcl” or “.pkr.json” is passed it will be parsed using the HCL2 format. For every other case; the old packer style will be used. ## 1. the hcl2template pkg can create a packer.Build from a set of HCL (v2) files I had to make the packer.coreBuild (which is our one and only packer.Build ) a public struct with public fields ## 2. Components interfaces get a new ConfigSpec Method to read a file from an HCL file. This is a breaking change for packer plugins. a packer component can be a: builder/provisioner/post-processor each component interface now gets a `ConfigSpec() hcldec.ObjectSpec` which allows packer to tell what is the layout of the hcl2 config meant to configure that specific component. This ObjectSpec is sent through the wire (RPC) and a cty.Value is now sent through the already existing configuration entrypoints: Provisioner.Prepare(raws ...interface{}) error Builder.Prepare(raws ...interface{}) ([]string, error) PostProcessor.Configure(raws ...interface{}) error close #1768 Example hcl files: ```hcl // file amazon-ebs-kms-key/run.pkr.hcl build { sources = [ "source.amazon-ebs.first", ] provisioner "shell" { inline = [ "sleep 5" ] } post-processor "shell-local" { inline = [ "sleep 5" ] } } // amazon-ebs-kms-key/source.pkr.hcl source "amazon-ebs" "first" { ami_name = "hcl2-test" region = "us-east-1" instance_type = "t2.micro" kms_key_id = "c729958f-c6ba-44cd-ab39-35ab68ce0a6c" encrypt_boot = true source_ami_filter { filters { virtualization-type = "hvm" name = "amzn-ami-hvm-????.??.?.????????-x86_64-gp2" root-device-type = "ebs" } most_recent = true owners = ["amazon"] } launch_block_device_mappings { device_name = "/dev/xvda" volume_size = 20 volume_type = "gp2" delete_on_termination = "true" } launch_block_device_mappings { device_name = "/dev/xvdf" volume_size = 500 volume_type = "gp2" delete_on_termination = true encrypted = true } ami_regions = ["eu-central-1"] run_tags { Name = "packer-solr-something" stack-name = "DevOps Tools" } communicator = "ssh" ssh_pty = true ssh_username = "ec2-user" associate_public_ip_address = true } ```
2019-12-17 05:25:56 -05:00
"github.com/hashicorp/hcl/v2/hcldec"
2020-12-17 16:29:25 -05:00
"github.com/hashicorp/packer-plugin-sdk/adapter"
"github.com/hashicorp/packer-plugin-sdk/common"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
"github.com/hashicorp/packer-plugin-sdk/template/config"
"github.com/hashicorp/packer-plugin-sdk/template/interpolate"
2019-01-20 10:43:47 -05:00
)
var SupportedBackends = map[string]bool{"docker": true, "local": true, "ssh": true, "winrm": true}
type Config struct {
common.PackerConfig `mapstructure:",squash"`
ctx interpolate.Context
// The command to invoke InSpec. Defaults to `inspec`.
Command string `mapstructure-to-hcl2:"command"`
SubCommand string `mapstructure-to-hcl2:",skip"`
2019-01-20 10:43:47 -05:00
// Extra options to pass to the inspec command
ExtraArguments []string `mapstructure:"extra_arguments"`
InspecEnvVars []string `mapstructure:"inspec_env_vars"`
// The profile to execute.
Profile string `mapstructure:"profile"`
AttributesDirectory string `mapstructure:"attributes_directory"`
AttributesFiles []string `mapstructure:"attributes"`
Backend string `mapstructure:"backend"`
User string `mapstructure:"user"`
Host string `mapstructure:"host"`
2019-03-19 09:47:21 -04:00
LocalPort int `mapstructure:"local_port"`
2019-01-20 10:43:47 -05:00
SSHHostKeyFile string `mapstructure:"ssh_host_key_file"`
SSHAuthorizedKeyFile string `mapstructure:"ssh_authorized_key_file"`
ValidExitCodes []int `mapstructure:"valid_exit_codes"`
2019-01-20 10:43:47 -05:00
}
type Provisioner struct {
config Config
2019-02-12 02:10:57 -05:00
adapter *adapter.Adapter
2019-01-20 10:43:47 -05:00
done chan struct{}
inspecVersion string
inspecMajVersion uint
}
build using HCL2 (#8423) This follows #8232 which added the code to generate the code required to parse HCL files for each packer component. All old config files of packer will keep on working the same. Packer takes one argument. When a directory is passed, all files in the folder with a name ending with “.pkr.hcl” or “.pkr.json” will be parsed using the HCL2 format. When a file ending with “.pkr.hcl” or “.pkr.json” is passed it will be parsed using the HCL2 format. For every other case; the old packer style will be used. ## 1. the hcl2template pkg can create a packer.Build from a set of HCL (v2) files I had to make the packer.coreBuild (which is our one and only packer.Build ) a public struct with public fields ## 2. Components interfaces get a new ConfigSpec Method to read a file from an HCL file. This is a breaking change for packer plugins. a packer component can be a: builder/provisioner/post-processor each component interface now gets a `ConfigSpec() hcldec.ObjectSpec` which allows packer to tell what is the layout of the hcl2 config meant to configure that specific component. This ObjectSpec is sent through the wire (RPC) and a cty.Value is now sent through the already existing configuration entrypoints: Provisioner.Prepare(raws ...interface{}) error Builder.Prepare(raws ...interface{}) ([]string, error) PostProcessor.Configure(raws ...interface{}) error close #1768 Example hcl files: ```hcl // file amazon-ebs-kms-key/run.pkr.hcl build { sources = [ "source.amazon-ebs.first", ] provisioner "shell" { inline = [ "sleep 5" ] } post-processor "shell-local" { inline = [ "sleep 5" ] } } // amazon-ebs-kms-key/source.pkr.hcl source "amazon-ebs" "first" { ami_name = "hcl2-test" region = "us-east-1" instance_type = "t2.micro" kms_key_id = "c729958f-c6ba-44cd-ab39-35ab68ce0a6c" encrypt_boot = true source_ami_filter { filters { virtualization-type = "hvm" name = "amzn-ami-hvm-????.??.?.????????-x86_64-gp2" root-device-type = "ebs" } most_recent = true owners = ["amazon"] } launch_block_device_mappings { device_name = "/dev/xvda" volume_size = 20 volume_type = "gp2" delete_on_termination = "true" } launch_block_device_mappings { device_name = "/dev/xvdf" volume_size = 500 volume_type = "gp2" delete_on_termination = true encrypted = true } ami_regions = ["eu-central-1"] run_tags { Name = "packer-solr-something" stack-name = "DevOps Tools" } communicator = "ssh" ssh_pty = true ssh_username = "ec2-user" associate_public_ip_address = true } ```
2019-12-17 05:25:56 -05:00
func (p *Provisioner) ConfigSpec() hcldec.ObjectSpec { return p.config.FlatMapstructure().HCL2Spec() }
2019-01-20 10:43:47 -05:00
func (p *Provisioner) Prepare(raws ...interface{}) error {
p.done = make(chan struct{})
err := config.Decode(&p.config, &config.DecodeOpts{
PluginType: "inspec",
2019-01-20 10:43:47 -05:00
Interpolate: true,
InterpolateContext: &p.config.ctx,
InterpolateFilter: &interpolate.RenderFilter{
Exclude: []string{},
},
}, raws...)
if err != nil {
return err
}
// Defaults
if p.config.Command == "" {
p.config.Command = "inspec"
}
if p.config.SubCommand == "" {
p.config.SubCommand = "exec"
}
var errs *packersdk.MultiError
2019-01-20 10:43:47 -05:00
err = validateProfileConfig(p.config.Profile)
if err != nil {
errs = packersdk.MultiErrorAppend(errs, err)
2019-01-20 10:43:47 -05:00
}
// Check that the authorized key file exists
if len(p.config.SSHAuthorizedKeyFile) > 0 {
err = validateFileConfig(p.config.SSHAuthorizedKeyFile, "ssh_authorized_key_file", true)
if err != nil {
log.Println(p.config.SSHAuthorizedKeyFile, "does not exist")
errs = packersdk.MultiErrorAppend(errs, err)
2019-01-20 10:43:47 -05:00
}
}
if len(p.config.SSHHostKeyFile) > 0 {
err = validateFileConfig(p.config.SSHHostKeyFile, "ssh_host_key_file", true)
if err != nil {
log.Println(p.config.SSHHostKeyFile, "does not exist")
errs = packersdk.MultiErrorAppend(errs, err)
2019-01-20 10:43:47 -05:00
}
}
if p.config.Backend == "" {
p.config.Backend = "ssh"
}
if _, ok := SupportedBackends[p.config.Backend]; !ok {
errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("backend: %s must be a valid backend", p.config.Backend))
}
2019-02-12 15:07:13 -05:00
if p.config.Backend == "docker" && p.config.Host == "" {
errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("backend: host must be specified for docker backend"))
2019-02-12 15:07:13 -05:00
}
2019-01-20 10:43:47 -05:00
if p.config.Host == "" {
p.config.Host = "127.0.0.1"
}
if p.config.LocalPort > 65535 {
errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("local_port: %d must be a valid port", p.config.LocalPort))
2019-01-20 10:43:47 -05:00
}
if len(p.config.AttributesDirectory) > 0 {
err = validateDirectoryConfig(p.config.AttributesDirectory, "attrs")
if err != nil {
log.Println(p.config.AttributesDirectory, "does not exist")
errs = packersdk.MultiErrorAppend(errs, err)
2019-01-20 10:43:47 -05:00
}
}
if p.config.User == "" {
usr, err := user.Current()
if err != nil {
errs = packersdk.MultiErrorAppend(errs, err)
2019-01-20 10:43:47 -05:00
} else {
p.config.User = usr.Username
}
}
if p.config.User == "" {
errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("user: could not determine current user from environment."))
2019-01-20 10:43:47 -05:00
}
if p.config.ValidExitCodes == nil {
p.config.ValidExitCodes = []int{0, 101}
}
2019-01-20 10:43:47 -05:00
if errs != nil && len(errs.Errors) > 0 {
return errs
}
return nil
}
func (p *Provisioner) getVersion() error {
out, err := exec.Command(p.config.Command, "version").Output()
if err != nil {
return fmt.Errorf(
"Error running \"%s version\": %s", p.config.Command, err.Error())
}
versionRe := regexp.MustCompile(`(\d+\.\d+[.\d+]*)`)
2019-01-20 10:43:47 -05:00
matches := versionRe.FindStringSubmatch(string(out))
if matches == nil {
return fmt.Errorf(
"Could not find %s version in output:\n%s", p.config.Command, string(out))
}
version := matches[1]
log.Printf("%s version: %s", p.config.Command, version)
p.inspecVersion = version
majVer, err := strconv.ParseUint(strings.Split(version, ".")[0], 10, 0)
if err != nil {
return fmt.Errorf("Could not parse major version from \"%s\".", version)
}
p.inspecMajVersion = uint(majVer)
return nil
}
func (p *Provisioner) Provision(ctx context.Context, ui packersdk.Ui, comm packersdk.Communicator, generatedData map[string]interface{}) error {
2019-01-20 10:43:47 -05:00
ui.Say("Provisioning with Inspec...")
p.config.ctx.Data = generatedData
userp, err := interpolate.Render(p.config.User, &p.config.ctx)
if err != nil {
return fmt.Errorf("Could not interpolate inspec user: %s", err)
}
p.config.User = userp
host, err := interpolate.Render(p.config.Host, &p.config.ctx)
if err != nil {
return fmt.Errorf("Could not interpolate inspec user: %s", err)
}
p.config.Host = host
2019-01-20 10:43:47 -05:00
for i, envVar := range p.config.InspecEnvVars {
envVar, err := interpolate.Render(envVar, &p.config.ctx)
if err != nil {
return fmt.Errorf("Could not interpolate inspec env vars: %s", err)
}
p.config.InspecEnvVars[i] = envVar
}
for i, arg := range p.config.ExtraArguments {
arg, err := interpolate.Render(arg, &p.config.ctx)
if err != nil {
return fmt.Errorf("Could not interpolate inspec extra arguments: %s", err)
}
p.config.ExtraArguments[i] = arg
}
for i, arg := range p.config.AttributesFiles {
arg, err := interpolate.Render(arg, &p.config.ctx)
if err != nil {
return fmt.Errorf("Could not interpolate inspec attributes: %s", err)
}
p.config.AttributesFiles[i] = arg
}
k, err := newUserKey(p.config.SSHAuthorizedKeyFile)
if err != nil {
return err
}
hostSigner, err := newSigner(p.config.SSHHostKeyFile)
2019-08-27 20:01:33 -04:00
if err != nil {
return fmt.Errorf("error creating host signer: %s", err)
}
2019-01-20 10:43:47 -05:00
// Remove the private key file
if len(k.privKeyFile) > 0 {
defer os.Remove(k.privKeyFile)
}
keyChecker := ssh.CertChecker{
UserKeyFallback: func(conn ssh.ConnMetadata, pubKey ssh.PublicKey) (*ssh.Permissions, error) {
if user := conn.User(); user != p.config.User {
return nil, errors.New(fmt.Sprintf("authentication failed: %s is not a valid user", user))
}
if !bytes.Equal(k.Marshal(), pubKey.Marshal()) {
return nil, errors.New("authentication failed: unauthorized key")
}
return nil, nil
},
IsUserAuthority: func(k ssh.PublicKey) bool { return true },
2019-01-20 10:43:47 -05:00
}
config := &ssh.ServerConfig{
AuthLogCallback: func(conn ssh.ConnMetadata, method string, err error) {
log.Printf("authentication attempt from %s to %s as %s using %s", conn.RemoteAddr(), conn.LocalAddr(), conn.User(), method)
},
PublicKeyCallback: keyChecker.Authenticate,
//NoClientAuth: true,
}
config.AddHostKey(hostSigner)
localListener, err := func() (net.Listener, error) {
port := p.config.LocalPort
2019-01-20 10:43:47 -05:00
tries := 1
if port != 0 {
tries = 10
}
for i := 0; i < tries; i++ {
l, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", port))
port++
if err != nil {
ui.Say(err.Error())
continue
}
_, portStr, err := net.SplitHostPort(l.Addr().String())
if err != nil {
ui.Say(err.Error())
continue
}
2019-03-19 09:47:21 -04:00
p.config.LocalPort, err = strconv.Atoi(portStr)
2019-01-20 10:43:47 -05:00
if err != nil {
ui.Say(err.Error())
continue
}
return l, nil
}
return nil, errors.New("Error setting up SSH proxy connection")
}()
if err != nil {
return err
}
ui = &packersdk.SafeUi{
Sem: make(chan int, 1),
Ui: ui,
}
2019-02-12 02:10:57 -05:00
p.adapter = adapter.NewAdapter(p.done, localListener, config, "", ui, comm)
2019-01-20 10:43:47 -05:00
defer func() {
log.Print("shutting down the SSH proxy")
close(p.done)
p.adapter.Shutdown()
}()
go p.adapter.Serve()
tf, err := ioutil.TempFile(p.config.AttributesDirectory, "packer-provisioner-inspec.*.yml")
if err != nil {
return fmt.Errorf("Error preparing packer attributes file: %s", err)
}
defer os.Remove(tf.Name())
w := bufio.NewWriter(tf)
w.WriteString(fmt.Sprintf("packer_build_name: %s\n", p.config.PackerBuildName))
w.WriteString(fmt.Sprintf("packer_builder_type: %s\n", p.config.PackerBuilderType))
if err := w.Flush(); err != nil {
tf.Close()
return fmt.Errorf("Error preparing packer attributes file: %s", err)
}
tf.Close()
p.config.AttributesFiles = append(p.config.AttributesFiles, tf.Name())
if err := p.executeInspec(ui, comm, k.privKeyFile); err != nil {
return fmt.Errorf("Error executing Inspec: %s", err)
}
return nil
}
func (p *Provisioner) executeInspec(ui packersdk.Ui, comm packersdk.Communicator, privKeyFile string) error {
2019-01-20 10:43:47 -05:00
var envvars []string
args := []string{p.config.SubCommand, p.config.Profile}
args = append(args, "--backend", p.config.Backend)
args = append(args, "--host", p.config.Host)
if p.config.User != "" {
args = append(args, "--user", p.config.User)
}
2019-01-20 10:43:47 -05:00
if p.config.Backend == "ssh" {
if len(privKeyFile) > 0 {
args = append(args, "--key-files", privKeyFile)
}
2019-03-19 09:47:21 -04:00
args = append(args, "--port", strconv.Itoa(p.config.LocalPort))
2019-01-20 10:43:47 -05:00
}
args = append(args, "--input-file")
2019-01-20 10:43:47 -05:00
args = append(args, p.config.AttributesFiles...)
args = append(args, p.config.ExtraArguments...)
if len(p.config.InspecEnvVars) > 0 {
envvars = append(envvars, p.config.InspecEnvVars...)
}
cmd := exec.Command(p.config.Command, args...)
cmd.Env = os.Environ()
if len(envvars) > 0 {
cmd.Env = append(cmd.Env, envvars...)
}
stdout, err := cmd.StdoutPipe()
if err != nil {
return err
}
stderr, err := cmd.StderrPipe()
if err != nil {
return err
}
wg := sync.WaitGroup{}
repeat := func(r io.ReadCloser) {
reader := bufio.NewReader(r)
for {
line, err := reader.ReadString('\n')
if line != "" {
line = strings.TrimRightFunc(line, unicode.IsSpace)
ui.Message(line)
}
if err != nil {
if err == io.EOF {
break
} else {
ui.Error(err.Error())
break
}
}
}
wg.Done()
}
wg.Add(2)
go repeat(stdout)
go repeat(stderr)
ui.Say(fmt.Sprintf("Executing Inspec: %s", strings.Join(cmd.Args, " ")))
if err := cmd.Start(); err != nil {
return err
}
wg.Wait()
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)
}
2019-01-20 10:43:47 -05:00
}
return nil
}
func validateFileConfig(name string, config string, req bool) error {
if req {
if name == "" {
return fmt.Errorf("%s must be specified.", config)
}
}
info, err := os.Stat(name)
if err != nil {
return fmt.Errorf("%s: %s is invalid: %s", config, name, err)
} else if info.IsDir() {
return fmt.Errorf("%s: %s must point to a file", config, name)
}
return nil
}
func validateProfileConfig(name string) error {
if name == "" {
return fmt.Errorf("profile must be specified.")
}
return nil
}
func validateDirectoryConfig(name string, config string) error {
info, err := os.Stat(name)
if err != nil {
return fmt.Errorf("%s: %s is invalid: %s", config, name, err)
} else if !info.IsDir() {
return fmt.Errorf("%s: %s must point to a directory", config, name)
}
return nil
}
type userKey struct {
ssh.PublicKey
privKeyFile string
}
func newUserKey(pubKeyFile string) (*userKey, error) {
userKey := new(userKey)
if len(pubKeyFile) > 0 {
pubKeyBytes, err := ioutil.ReadFile(pubKeyFile)
if err != nil {
return nil, errors.New("Failed to read public key")
}
userKey.PublicKey, _, _, _, err = ssh.ParseAuthorizedKey(pubKeyBytes)
if err != nil {
return nil, errors.New("Failed to parse authorized key")
}
return userKey, nil
}
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, errors.New("Failed to generate key pair")
}
userKey.PublicKey, err = ssh.NewPublicKey(key.Public())
if err != nil {
return nil, errors.New("Failed to extract public key from generated key pair")
}
// To support Inspec calling back to us we need to write
// this file down
privateKeyDer := x509.MarshalPKCS1PrivateKey(key)
privateKeyBlock := pem.Block{
Type: "RSA PRIVATE KEY",
Headers: nil,
Bytes: privateKeyDer,
}
tf, err := ioutil.TempFile("", "packer-provisioner-inspec.*.key")
if err != nil {
return nil, errors.New("failed to create temp file for generated key")
}
_, err = tf.Write(pem.EncodeToMemory(&privateKeyBlock))
if err != nil {
return nil, errors.New("failed to write private key to temp file")
}
err = tf.Close()
if err != nil {
return nil, errors.New("failed to close private key temp file")
}
userKey.privKeyFile = tf.Name()
return userKey, nil
}
type signer struct {
ssh.Signer
}
func newSigner(privKeyFile string) (*signer, error) {
signer := new(signer)
if len(privKeyFile) > 0 {
privateBytes, err := ioutil.ReadFile(privKeyFile)
if err != nil {
return nil, errors.New("Failed to load private host key")
}
signer.Signer, err = ssh.ParsePrivateKey(privateBytes)
if err != nil {
return nil, errors.New("Failed to parse private host key")
}
return signer, nil
}
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, errors.New("Failed to generate server key pair")
}
signer.Signer, err = ssh.NewSignerFromKey(key)
if err != nil {
return nil, errors.New("Failed to extract private key from generated key pair")
}
return signer, nil
}