Merge pull request #6636 from hashicorp/fix_6522

Create new template option allowing users to choose to source env vars from a file rather than declaring them inline
This commit is contained in:
Megan Marsh 2018-08-30 12:34:12 -07:00 committed by GitHub
commit 7042d7a3d5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 233 additions and 34 deletions

View File

@ -45,6 +45,10 @@ type Config struct {
// your command(s) are executed.
Vars []string `mapstructure:"environment_vars"`
// Write the Vars to a file and source them from there rather than declaring
// inline
UseEnvVarFile bool `mapstructure:"use_env_var_file"`
// The remote folder where the local shell script will be uploaded to.
// This should be set to a pre-existing directory, it defaults to /tmp
RemoteFolder string `mapstructure:"remote_folder"`
@ -75,6 +79,8 @@ type Config struct {
startRetryTimeout time.Duration
ctx interpolate.Context
// name of the tmp environment variable file, if UseEnvVarFile is true
envVarFile string
}
type Provisioner struct {
@ -82,8 +88,9 @@ type Provisioner struct {
}
type ExecuteCommandTemplate struct {
Vars string
Path string
Vars string
EnvVarFile string
Path string
}
func (p *Provisioner) Prepare(raws ...interface{}) error {
@ -102,6 +109,9 @@ func (p *Provisioner) Prepare(raws ...interface{}) error {
if p.config.ExecuteCommand == "" {
p.config.ExecuteCommand = "chmod +x {{.Path}}; {{.Vars}} {{.Path}}"
if p.config.UseEnvVarFile == true {
p.config.ExecuteCommand = "chmod +x {{.Path}}; . {{.EnvVarFile}} && {{.Path}}"
}
}
if p.config.Inline != nil && len(p.config.Inline) == 0 {
@ -218,6 +228,58 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
tf.Close()
}
if p.config.UseEnvVarFile == true {
tf, err := ioutil.TempFile("", "packer-shell-vars")
if err != nil {
return fmt.Errorf("Error preparing shell script: %s", err)
}
defer os.Remove(tf.Name())
// Write our contents to it
writer := bufio.NewWriter(tf)
if _, err := writer.WriteString(p.createEnvVarFileContent()); err != nil {
return fmt.Errorf("Error preparing shell script: %s", err)
}
if err := writer.Flush(); err != nil {
return fmt.Errorf("Error preparing shell script: %s", err)
}
p.config.envVarFile = tf.Name()
defer os.Remove(p.config.envVarFile)
// upload the var file
var cmd *packer.RemoteCmd
err = p.retryable(func() error {
if _, err := tf.Seek(0, 0); err != nil {
return err
}
var r io.Reader = tf
if !p.config.Binary {
r = &UnixReader{Reader: r}
}
remoteVFName := fmt.Sprintf("%s/%s", p.config.RemoteFolder,
fmt.Sprintf("varfile_%d.sh", rand.Intn(9999)))
if err := comm.Upload(remoteVFName, r, nil); err != nil {
return fmt.Errorf("Error uploading envVarFile: %s", err)
}
tf.Close()
cmd = &packer.RemoteCmd{
Command: fmt.Sprintf("chmod 0600 %s", remoteVFName),
}
if err := comm.Start(cmd); err != nil {
return fmt.Errorf(
"Error chmodding script file to 0755 in remote "+
"machine: %s", err)
}
cmd.Wait()
p.config.envVarFile = remoteVFName
return nil
})
}
// Create environment variables to set before executing the command
flattenedEnvVars := p.createFlattenedEnvVars()
@ -233,8 +295,9 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
// Compile the command
p.config.ctx.Data = &ExecuteCommandTemplate{
Vars: flattenedEnvVars,
Path: p.config.RemotePath,
Vars: flattenedEnvVars,
EnvVarFile: p.config.envVarFile,
Path: p.config.RemotePath,
}
command, err := interpolate.Render(p.config.ExecuteCommand, &p.config.ctx)
if err != nil {
@ -297,30 +360,13 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
// Delete the temporary file we created. We retry this a few times
// since if the above rebooted we have to wait until the reboot
// completes.
err = p.retryable(func() error {
cmd = &packer.RemoteCmd{
Command: fmt.Sprintf("rm -f %s", p.config.RemotePath),
}
if err := comm.Start(cmd); err != nil {
return fmt.Errorf(
"Error removing temporary script at %s: %s",
p.config.RemotePath, err)
}
cmd.Wait()
// treat disconnects as retryable by returning an error
if cmd.ExitStatus == packer.CmdDisconnect {
return fmt.Errorf("Disconnect while removing temporary script.")
}
return nil
})
err = p.cleanupRemoteFile(p.config.RemotePath, comm)
if err != nil {
return err
}
if cmd.ExitStatus != 0 {
return fmt.Errorf(
"Error removing temporary script at %s!",
p.config.RemotePath)
err = p.cleanupRemoteFile(p.config.envVarFile, comm)
if err != nil {
return err
}
}
}
@ -328,6 +374,36 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
return nil
}
func (p *Provisioner) cleanupRemoteFile(path string, comm packer.Communicator) error {
err := p.retryable(func() error {
cmd := &packer.RemoteCmd{
Command: fmt.Sprintf("rm -f %s", path),
}
if err := comm.Start(cmd); err != nil {
return fmt.Errorf(
"Error removing temporary script at %s: %s",
path, err)
}
cmd.Wait()
// treat disconnects as retryable by returning an error
if cmd.ExitStatus == packer.CmdDisconnect {
return fmt.Errorf("Disconnect while removing temporary script.")
}
if cmd.ExitStatus != 0 {
return fmt.Errorf(
"Error removing temporary script at %s!",
path)
}
return nil
})
if err != nil {
return err
}
return nil
}
func (p *Provisioner) Cancel() {
// Just hard quit. It isn't a big deal if what we're doing keeps
// running on the other side.
@ -360,8 +436,7 @@ func (p *Provisioner) retryable(f func() error) error {
}
}
func (p *Provisioner) createFlattenedEnvVars() (flattened string) {
flattened = ""
func (p *Provisioner) escapeEnvVars() ([]string, map[string]string) {
envVars := make(map[string]string)
// Always available Packer provided env vars
@ -387,6 +462,24 @@ func (p *Provisioner) createFlattenedEnvVars() (flattened string) {
}
sort.Strings(keys)
return keys, envVars
}
func (p *Provisioner) createEnvVarFileContent() string {
keys, envVars := p.escapeEnvVars()
flattened := ""
// Re-assemble vars surrounding value with single quotes and flatten
for _, key := range keys {
flattened += fmt.Sprintf("export %s='%s'\n", key, envVars[key])
}
return flattened
}
func (p *Provisioner) createFlattenedEnvVars() (flattened string) {
keys, envVars := p.escapeEnvVars()
// Re-assemble vars surrounding value with single quotes and flatten
for _, key := range keys {
flattened += fmt.Sprintf("%s='%s' ", key, envVars[key])

View File

@ -286,6 +286,61 @@ func TestProvisioner_createFlattenedEnvVars(t *testing.T) {
}
}
func TestProvisioner_createEnvVarFileContent(t *testing.T) {
var flattenedEnvVars string
config := testConfig()
userEnvVarTests := [][]string{
{}, // No user env var
{"FOO=bar"}, // Single user env var
{"FOO=bar's"}, // User env var with single quote in value
{"FOO=bar", "BAZ=qux"}, // Multiple user env vars
{"FOO=bar=baz"}, // User env var with value containing equals
{"FOO==bar"}, // User env var with value starting with equals
}
expected := []string{
`export PACKER_BUILDER_TYPE='iso'
export PACKER_BUILD_NAME='vmware'
`,
`export FOO='bar'
export PACKER_BUILDER_TYPE='iso'
export PACKER_BUILD_NAME='vmware'
`,
`export FOO='bar'"'"'s'
export PACKER_BUILDER_TYPE='iso'
export PACKER_BUILD_NAME='vmware'
`,
`export BAZ='qux'
export FOO='bar'
export PACKER_BUILDER_TYPE='iso'
export PACKER_BUILD_NAME='vmware'
`,
`export FOO='bar=baz'
export PACKER_BUILDER_TYPE='iso'
export PACKER_BUILD_NAME='vmware'
`,
`export FOO='=bar'
export PACKER_BUILDER_TYPE='iso'
export PACKER_BUILD_NAME='vmware'
`,
}
p := new(Provisioner)
p.Prepare(config)
// Defaults provided by Packer
p.config.PackerBuildName = "vmware"
p.config.PackerBuilderType = "iso"
for i, expectedValue := range expected {
p.config.Vars = userEnvVarTests[i]
flattenedEnvVars = p.createEnvVarFileContent()
if flattenedEnvVars != expectedValue {
t.Fatalf("expected flattened env vars to be: %s, got %s.", expectedValue, flattenedEnvVars)
}
}
}
func TestProvisioner_RemoteFolderSetSuccessfully(t *testing.T) {
config := testConfig()

View File

@ -65,13 +65,25 @@ Optional parameters:
Packer injects some environmental variables by default into the environment,
as well, which are covered in the section below.
- `execute_command` (string) - The command to use to execute the script. By
default this is `chmod +x {{ .Path }}; {{ .Vars }} {{ .Path }}`. The value
of this is treated as [configuration
template](/docs/templates/engine.html). There are two
available variables: `Path`, which is the path to the script to run, and
`Vars`, which is the list of `environment_vars`, if configured.
- `use_env_var_file` (boolean) - If true, Packer will write your environment
variables to a tempfile and source them from that file, rather than
declaring them inline in our execute_command. The default `execute_command`
will be `chmod +x {{.Path}}; . {{.EnvVarFile}} && {{.Path}}`. This option is
unnecessary for most cases, but if you have extra quoting in your custom
`execute_command`, then this may be neccecary for proper script execution.
Default: false.
- `execute_command` (string) - The command to use to execute the script. By
default this is `chmod +x {{ .Path }}; {{ .Vars }} {{ .Path }}`, unless the
user has set `"use_env_var_file": true` -- in that case, the default
`execute_command` is `chmod +x {{.Path}}; . {{.EnvVarFile}} && {{.Path}}`.
The value of this is treated as a
[configuration template](/docs/templates/engine.html). There are three
available variables:
* `Path` is the path to the script to run
* `Vars` is the list of `environment_vars`, if configured.
* `EnvVarFile` is the path to the file containing env vars, if
`use_env_var_file` is true.
- `expect_disconnect` (boolean) - Defaults to `false`. Whether to error if the
server disconnects us. A disconnect might happen if you restart the ssh
server or reboot the host.
@ -256,9 +268,48 @@ would be:
create race conditions. Your first provisioner can tell the machine to wait
until it completely boots.
``` json
```json
{
"type": "shell",
"inline": [ "sleep 10" ]
}
```
## Quoting Environment Variables
Packer manages quoting for you, so you should't have to worry about it.
Below is an example of packer template inputs and what you should expect to get
out:
```json
"provisioners": [
{
"type": "shell",
"environment_vars": ["FOO=foo",
"BAR=bar's",
"BAZ=baz=baz",
"QUX==qux",
"FOOBAR=foo bar",
"FOOBARBAZ='foo bar baz'",
"QUX2=\"qux\""],
"inline": ["echo \"FOO is $FOO\"",
"echo \"BAR is $BAR\"",
"echo \"BAZ is $BAZ\"",
"echo \"QUX is $QUX\"",
"echo \"FOOBAR is $FOOBAR\"",
"echo \"FOOBARBAZ is $FOOBARBAZ\"",
"echo \"QUX2 is $QUX2\""]
}
```
Output:
```
docker: FOO is foo
docker: BAR is bar's
docker: BAZ is baz=baz
docker: QUX is =qux
docker: FOOBAR is foo bar
docker: FOOBARBAZ is 'foo bar baz'
docker: QUX2 is "qux"
```