move proxy behind feature flag

This commit is contained in:
Megan Marsh 2020-02-04 10:42:55 -08:00
parent 1963f3aa6f
commit ca5814ab74
5 changed files with 419 additions and 61 deletions

View File

@ -63,6 +63,7 @@ func PopulateProvisionHookData(state multistep.StateBag) map[string]interface{}
hookData["ConnType"] = commConf.Type
hookData["SSHPublicKey"] = string(commConf.SSHPublicKey)
hookData["SSHPrivateKey"] = string(commConf.SSHPrivateKey)
hookData["SSHPrivateKeyFile"] = commConf.SSHPrivateKeyFile
// Backwards compatibility; in practice, WinRMPassword is fulfilled by
// Password.

View File

@ -67,6 +67,9 @@ type Config struct {
GalaxyCommand string `mapstructure:"galaxy_command"`
GalaxyForceInstall bool `mapstructure:"galaxy_force_install"`
RolesPath string `mapstructure:"roles_path"`
//TODO: change default to false in v1.6.0.
UseProxy config.Trilean `mapstructure:"use_proxy"`
userWasEmpty bool
}
type Provisioner struct {
@ -76,6 +79,9 @@ type Provisioner struct {
ansibleVersion string
ansibleMajVersion uint
generatedData map[string]interface{}
setupAdapterFunc func(ui packer.Ui, comm packer.Communicator) (string, error)
executeAnsibleFunc func(ui packer.Ui, comm packer.Communicator, privKeyFile string) error
}
func (p *Provisioner) ConfigSpec() hcldec.ObjectSpec { return p.config.FlatMapstructure().HCL2Spec() }
@ -163,6 +169,7 @@ func (p *Provisioner) Prepare(raws ...interface{}) error {
}
if p.config.User == "" {
p.config.userWasEmpty = true
usr, err := user.Current()
if err != nil {
errs = packer.MultiErrorAppend(errs, err)
@ -174,6 +181,16 @@ func (p *Provisioner) Prepare(raws ...interface{}) error {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("user: could not determine current user from environment."))
}
// These fields exist so that we can replace the functions for testing
// logic inside of the Provision func; in actual use, these don't ever
// need to get set.
if p.setupAdapterFunc == nil {
p.setupAdapterFunc = p.setupAdapter
}
if p.executeAnsibleFunc == nil {
p.executeAnsibleFunc = p.executeAnsible
}
if errs != nil && len(errs.Errors) > 0 {
return errs
}
@ -207,40 +224,17 @@ func (p *Provisioner) getVersion() error {
return nil
}
func (p *Provisioner) Provision(ctx context.Context, ui packer.Ui, comm packer.Communicator, generatedData map[string]interface{}) error {
ui.Say("Provisioning with Ansible...")
// Interpolate env vars to check for generated values like password and port
p.generatedData = generatedData
p.config.ctx.Data = generatedData
for i, envVar := range p.config.AnsibleEnvVars {
envVar, err := interpolate.Render(envVar, &p.config.ctx)
if err != nil {
return fmt.Errorf("Could not interpolate ansible env vars: %s", err)
}
p.config.AnsibleEnvVars[i] = envVar
}
// Interpolate extra vars to check for generated values like password and port
for i, arg := range p.config.ExtraArguments {
arg, err := interpolate.Render(arg, &p.config.ctx)
if err != nil {
return fmt.Errorf("Could not interpolate ansible env vars: %s", err)
}
p.config.ExtraArguments[i] = arg
}
func (p *Provisioner) setupAdapter(ui packer.Ui, comm packer.Communicator) (string, error) {
ui.Message("Setting up proxy adapter for Ansible....")
k, err := newUserKey(p.config.SSHAuthorizedKeyFile)
if err != nil {
return err
return "", err
}
hostSigner, err := newSigner(p.config.SSHHostKeyFile)
if err != nil {
return fmt.Errorf("error creating host signer: %s", err)
}
// Remove the private key file
if len(k.privKeyFile) > 0 {
defer os.Remove(k.privKeyFile)
return "", fmt.Errorf("error creating host signer: %s", err)
}
keyChecker := ssh.CertChecker{
@ -298,7 +292,7 @@ func (p *Provisioner) Provision(ctx context.Context, ui packer.Ui, comm packer.C
}()
if err != nil {
return err
return "", err
}
ui = &packer.SafeUi{
@ -307,50 +301,155 @@ func (p *Provisioner) Provision(ctx context.Context, ui packer.Ui, comm packer.C
}
p.adapter = adapter.NewAdapter(p.done, localListener, config, p.config.SFTPCmd, ui, comm)
defer func() {
log.Print("shutting down the SSH proxy")
close(p.done)
p.adapter.Shutdown()
}()
return k.privKeyFile, nil
}
go p.adapter.Serve()
func (p *Provisioner) createInventoryFile(hostIP string, hostPort int) error {
log.Printf("Creating inventory file for Ansible run...")
tf, err := ioutil.TempFile(p.config.InventoryDirectory, "packer-provisioner-ansible")
if err != nil {
return fmt.Errorf("Error preparing inventory file: %s", err)
}
host := fmt.Sprintf("%s ansible_host=%s ansible_user=%s ansible_port=%d\n",
p.config.HostAlias, hostIP, p.config.User, hostPort)
if p.ansibleMajVersion < 2 {
host = fmt.Sprintf("%s ansible_ssh_host=%s ansible_ssh_user=%s ansible_ssh_port=%d\n",
p.config.HostAlias, hostIP, p.config.User, hostPort)
}
w := bufio.NewWriter(tf)
w.WriteString(host)
for _, group := range p.config.Groups {
fmt.Fprintf(w, "[%s]\n%s", group, host)
}
for _, group := range p.config.EmptyGroups {
fmt.Fprintf(w, "[%s]\n", group)
}
if err := w.Flush(); err != nil {
tf.Close()
os.Remove(tf.Name())
return fmt.Errorf("Error preparing inventory file: %s", err)
}
tf.Close()
p.config.InventoryFile = tf.Name()
return nil
}
func (p *Provisioner) Provision(ctx context.Context, ui packer.Ui, comm packer.Communicator, generatedData map[string]interface{}) error {
ui.Say("Provisioning with Ansible...")
// Interpolate env vars to check for generated values like password and port
p.generatedData = generatedData
p.config.ctx.Data = generatedData
for i, envVar := range p.config.AnsibleEnvVars {
envVar, err := interpolate.Render(envVar, &p.config.ctx)
if err != nil {
return fmt.Errorf("Could not interpolate ansible env vars: %s", err)
}
p.config.AnsibleEnvVars[i] = envVar
}
// Interpolate extra vars to check for generated values like password and port
for i, arg := range p.config.ExtraArguments {
arg, err := interpolate.Render(arg, &p.config.ctx)
if err != nil {
return fmt.Errorf("Could not interpolate ansible env vars: %s", err)
}
p.config.ExtraArguments[i] = arg
}
// Set up proxy if there's no host IP to access, regardless of user config.
hostIP := generatedData["Host"].(string)
if hostIP == "" && p.config.UseProxy.False() {
ui.Error("Warning: use_proxy is false, but instance does" +
" not have an IP address to give to Ansible. Falling back" +
" to use localhost proxy.")
p.config.UseProxy = config.TriTrue
}
privKeyFile := ""
if !p.config.UseProxy.False() {
// We set up the proxy if useProxy is either true or unset.
pkf, err := p.setupAdapterFunc(ui, comm)
if err != nil {
return err
}
// This is necessary to avoid accidentally redeclaring
// privKeyFile in the scope of this if statement.
privKeyFile = pkf
defer func() {
log.Print("shutting down the SSH proxy")
close(p.done)
p.adapter.Shutdown()
}()
go p.adapter.Serve()
// Remove the private key file
if len(privKeyFile) > 0 {
defer os.Remove(privKeyFile)
}
} else {
ui.Message("Not using Proxy adapter for Ansible run:\n" +
"\tUsing ssh keys from Packer communicator...")
// In this situation, we need to make sure we have the
// private key we actually use to access the instance.
SSHPrivateKeyFile := generatedData["SSHPrivateKeyFile"].(string)
if SSHPrivateKeyFile != "" {
privKeyFile = SSHPrivateKeyFile
} else {
// See if we can get a private key and write that to a tmpfile
SSHPrivateKey := generatedData["SSHPrivateKey"].([]byte)
tmpSSHPrivateKey, err := tmp.File("ansible-key")
if err != nil {
return fmt.Errorf("Error writing private key to temp file for"+
"ansible connection: %v", err)
}
_, err = tmpSSHPrivateKey.Write(SSHPrivateKey)
if err != nil {
return errors.New("failed to write private key to temp file")
}
err = tmpSSHPrivateKey.Close()
if err != nil {
return errors.New("failed to close private key temp file")
}
privKeyFile = tmpSSHPrivateKey.Name()
}
// Also make sure that the username matches the SSH keys given.
if p.config.userWasEmpty {
p.config.User = generatedData["User"].(string)
}
}
if len(p.config.InventoryFile) == 0 {
tf, err := ioutil.TempFile(p.config.InventoryDirectory, "packer-provisioner-ansible")
hostIP = "127.0.0.1"
hostPort := p.config.LocalPort
if p.config.UseProxy.False() {
// We aren't using a proxy, so we need to retrieve the
// host IP and port from generated data.
hostIP = generatedData["Host"].(string)
hostPort = int(generatedData["Port"].(int64))
}
// Create the inventory file
err := p.createInventoryFile(hostIP, hostPort)
if err != nil {
return fmt.Errorf("Error preparing inventory file: %s", err)
}
defer os.Remove(tf.Name())
host := fmt.Sprintf("%s ansible_host=127.0.0.1 ansible_user=%s ansible_port=%d\n",
p.config.HostAlias, p.config.User, p.config.LocalPort)
if p.ansibleMajVersion < 2 {
host = fmt.Sprintf("%s ansible_ssh_host=127.0.0.1 ansible_ssh_user=%s ansible_ssh_port=%d\n",
p.config.HostAlias, p.config.User, p.config.LocalPort)
return err
}
w := bufio.NewWriter(tf)
w.WriteString(host)
for _, group := range p.config.Groups {
fmt.Fprintf(w, "[%s]\n%s", group, host)
}
for _, group := range p.config.EmptyGroups {
fmt.Fprintf(w, "[%s]\n", group)
}
if err := w.Flush(); err != nil {
tf.Close()
return fmt.Errorf("Error preparing inventory file: %s", err)
}
tf.Close()
p.config.InventoryFile = tf.Name()
// Delete the generated inventory file
defer func() {
os.Remove(p.config.InventoryFile)
p.config.InventoryFile = ""
}()
}
if err := p.executeAnsible(ui, comm, k.privKeyFile); err != nil {
if err := p.executeAnsibleFunc(ui, comm, privKeyFile); err != nil {
return fmt.Errorf("Error executing Ansible: %s", err)
}

View File

@ -36,6 +36,7 @@ type FlatConfig struct {
GalaxyCommand *string `mapstructure:"galaxy_command" cty:"galaxy_command"`
GalaxyForceInstall *bool `mapstructure:"galaxy_force_install" cty:"galaxy_force_install"`
RolesPath *string `mapstructure:"roles_path" cty:"roles_path"`
UseProxy *bool `mapstructure:"use_proxy" cty:"use_proxy"`
}
// FlatMapstructure returns a new FlatConfig.
@ -77,6 +78,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
"galaxy_command": &hcldec.AttrSpec{Name: "galaxy_command", Type: cty.String, Required: false},
"galaxy_force_install": &hcldec.AttrSpec{Name: "galaxy_force_install", Type: cty.Bool, Required: false},
"roles_path": &hcldec.AttrSpec{Name: "roles_path", Type: cty.String, Required: false},
"use_proxy": &hcldec.AttrSpec{Name: "use_proxy", Type: cty.Bool, Required: false},
}
return s
}

View File

@ -14,9 +14,32 @@ import (
"strings"
"testing"
confighelper "github.com/hashicorp/packer/helper/config"
"github.com/hashicorp/packer/packer"
)
type provisionLogicTracker struct {
setupAdapterCalled bool
executeAnsibleCalled bool
happyPath bool
}
func (l *provisionLogicTracker) setupAdapter(ui packer.Ui, comm packer.Communicator) (string, error) {
l.setupAdapterCalled = true
if l.happyPath {
return "fakeKeyString", nil
}
return "", fmt.Errorf("chose sadpath")
}
func (l *provisionLogicTracker) executeAnsible(ui packer.Ui, comm packer.Communicator, privKeyFile string) error {
l.executeAnsibleCalled = true
if l.happyPath {
return fmt.Errorf("Chose sadpath")
}
return nil
}
// Be sure to remove the Ansible stub file in each test with:
// defer os.Remove(config["command"].(string))
func testConfig(t *testing.T) map[string]interface{} {
@ -354,3 +377,220 @@ func TestAnsibleLongMessages(t *testing.T) {
t.Fatalf("err: %s", err)
}
}
func TestCreateInventoryFile_vers1(t *testing.T) {
var p Provisioner
p.Prepare(testConfig(t))
defer os.Remove(p.config.Command)
p.ansibleMajVersion = 1
p.config.User = "testuser"
err := p.createInventoryFile("123.45.67.89", 2222)
if err != nil {
t.Fatalf("error creating config using localhost and local port proxy")
}
if p.config.InventoryFile == "" {
t.Fatalf("No inventory file was created")
}
defer os.Remove(p.config.InventoryFile)
f, err := ioutil.ReadFile(p.config.InventoryFile)
if err != nil {
t.Fatalf("couldn't read created inventoryfile: %s", err)
}
expected := "default ansible_ssh_host=123.45.67.89 ansible_ssh_user=testuser ansible_ssh_port=2222\n"
if fmt.Sprintf("%s", f) != expected {
t.Fatalf("File didn't match expected:\n\n expected: \n%s\n; recieved: \n%s\n", expected, f)
}
}
func TestCreateInventoryFile_vers2(t *testing.T) {
var p Provisioner
p.Prepare(testConfig(t))
defer os.Remove(p.config.Command)
p.ansibleMajVersion = 2
p.config.User = "testuser"
err := p.createInventoryFile("123.45.67.89", 1234)
if err != nil {
t.Fatalf("error creating config using localhost and local port proxy")
}
if p.config.InventoryFile == "" {
t.Fatalf("No inventory file was created")
}
defer os.Remove(p.config.InventoryFile)
f, err := ioutil.ReadFile(p.config.InventoryFile)
if err != nil {
t.Fatalf("couldn't read created inventoryfile: %s", err)
}
expected := "default ansible_host=123.45.67.89 ansible_user=testuser ansible_port=1234\n"
if fmt.Sprintf("%s", f) != expected {
t.Fatalf("File didn't match expected:\n\n expected: \n%s\n; recieved: \n%s\n", expected, f)
}
}
func TestCreateInventoryFile_Groups(t *testing.T) {
var p Provisioner
p.Prepare(testConfig(t))
defer os.Remove(p.config.Command)
p.ansibleMajVersion = 1
p.config.User = "testuser"
p.config.Groups = []string{"Group1", "Group2"}
err := p.createInventoryFile("123.45.67.89", 1234)
if err != nil {
t.Fatalf("error creating config using localhost and local port proxy")
}
if p.config.InventoryFile == "" {
t.Fatalf("No inventory file was created")
}
defer os.Remove(p.config.InventoryFile)
f, err := ioutil.ReadFile(p.config.InventoryFile)
if err != nil {
t.Fatalf("couldn't read created inventoryfile: %s", err)
}
expected := `default ansible_ssh_host=123.45.67.89 ansible_ssh_user=testuser ansible_ssh_port=1234
[Group1]
default ansible_ssh_host=123.45.67.89 ansible_ssh_user=testuser ansible_ssh_port=1234
[Group2]
default ansible_ssh_host=123.45.67.89 ansible_ssh_user=testuser ansible_ssh_port=1234
`
if fmt.Sprintf("%s", f) != expected {
t.Fatalf("File didn't match expected:\n\n expected: \n%s\n; recieved: \n%s\n", expected, f)
}
}
func TestCreateInventoryFile_EmptyGroups(t *testing.T) {
var p Provisioner
p.Prepare(testConfig(t))
defer os.Remove(p.config.Command)
p.ansibleMajVersion = 1
p.config.User = "testuser"
p.config.EmptyGroups = []string{"Group1", "Group2"}
err := p.createInventoryFile("123.45.67.89", 1234)
if err != nil {
t.Fatalf("error creating config using localhost and local port proxy")
}
if p.config.InventoryFile == "" {
t.Fatalf("No inventory file was created")
}
defer os.Remove(p.config.InventoryFile)
f, err := ioutil.ReadFile(p.config.InventoryFile)
if err != nil {
t.Fatalf("couldn't read created inventoryfile: %s", err)
}
expected := `default ansible_ssh_host=123.45.67.89 ansible_ssh_user=testuser ansible_ssh_port=1234
[Group1]
[Group2]
`
if fmt.Sprintf("%s", f) != expected {
t.Fatalf("File didn't match expected:\n\n expected: \n%s\n; recieved: \n%s\n", expected, f)
}
}
func TestCreateInventoryFile_GroupsAndEmptyGroups(t *testing.T) {
var p Provisioner
p.Prepare(testConfig(t))
defer os.Remove(p.config.Command)
p.ansibleMajVersion = 1
p.config.User = "testuser"
p.config.Groups = []string{"Group1", "Group2"}
p.config.EmptyGroups = []string{"Group3"}
err := p.createInventoryFile("123.45.67.89", 1234)
if err != nil {
t.Fatalf("error creating config using localhost and local port proxy")
}
if p.config.InventoryFile == "" {
t.Fatalf("No inventory file was created")
}
defer os.Remove(p.config.InventoryFile)
f, err := ioutil.ReadFile(p.config.InventoryFile)
if err != nil {
t.Fatalf("couldn't read created inventoryfile: %s", err)
}
expected := `default ansible_ssh_host=123.45.67.89 ansible_ssh_user=testuser ansible_ssh_port=1234
[Group1]
default ansible_ssh_host=123.45.67.89 ansible_ssh_user=testuser ansible_ssh_port=1234
[Group2]
default ansible_ssh_host=123.45.67.89 ansible_ssh_user=testuser ansible_ssh_port=1234
[Group3]
`
if fmt.Sprintf("%s", f) != expected {
t.Fatalf("File didn't match expected:\n\n file is \n\n %s", f)
}
}
func TestUseProxy(t *testing.T) {
type testcase struct {
UseProxy confighelper.Trilean
generatedData map[string]interface{}
expectedSetupAdapterCalled bool
explanation string
}
tcs := []testcase{
{
explanation: "use_proxy is true; we should set up adapter",
UseProxy: confighelper.TriTrue,
generatedData: map[string]interface{}{
"Host": "123.45.67.8",
"Port": int64(1234),
},
expectedSetupAdapterCalled: true,
},
{
explanation: "use_proxy is false but no IP addr is available; we should set up adapter anyway.",
UseProxy: confighelper.TriFalse,
generatedData: map[string]interface{}{
"Host": "",
"Port": nil,
},
expectedSetupAdapterCalled: true,
},
{
explanation: "use_proxy is false; we shouldn't set up adapter.",
UseProxy: confighelper.TriFalse,
generatedData: map[string]interface{}{
"Host": "123.45.67.8",
"Port": int64(1234),
},
expectedSetupAdapterCalled: false,
},
{
explanation: "use_proxy is unset; we should default to setting up the adapter (for now).",
UseProxy: confighelper.TriUnset,
generatedData: map[string]interface{}{
"Host": "123.45.67.8",
"Port": int64(1234),
},
expectedSetupAdapterCalled: true,
},
}
for _, tc := range tcs {
var p Provisioner
p.Prepare(testConfig(t))
p.config.UseProxy = tc.UseProxy
defer os.Remove(p.config.Command)
p.ansibleMajVersion = 1
var l provisionLogicTracker
l.setupAdapterCalled = false
p.setupAdapterFunc = l.setupAdapter
p.executeAnsibleFunc = l.executeAnsible
ctx := context.TODO()
comm := new(packer.MockCommunicator)
ui := &packer.BasicUi{
Reader: new(bytes.Buffer),
Writer: new(bytes.Buffer),
}
p.Provision(ctx, ui, comm, tc.generatedData)
if l.setupAdapterCalled != tc.expectedSetupAdapterCalled {
t.Fatalf("Should have called set up adapter: %s", tc.explanation)
}
os.Remove(p.config.Command)
}
}

View File

@ -183,6 +183,22 @@ Optional Parameters:
- `user` (string) - The `ansible_user` to use. Defaults to the user running
packer.
- `use_proxy` (boolean) - Whether or not to set up a localhost proxy adapter
so that Ansible has an IP address to connect to, even if your guest does not
have an IP address. For example, the adapter is necessary for Docker builds
to use the Ansible provisioner. Defaults to "true". If you set this option
to `false`, but Packer cannot find an IP address to connect Ansible to, it
will automatically set up the adapter anyway.
In order for Ansible to connect properly even when use_proxy is false, you
need to make sure that you are either providing a valid username and ssh key
to the ansible provisioner directly, or that the username and ssh key
being used by the ssh communicator will work for your needs. If you do not
provide a user to ansible, it will use the user associated with your
builder, not the user running Packer.
use_proxy=false is currently only supported for SSH, not winRM.
<%= partial "partials/provisioners/common-config" %>
## Default Extra Variables