Merge pull request #136 from ericlathrop/file-provisioner

File upload provisioner [GH-118]
This commit is contained in:
Mitchell Hashimoto 2013-07-04 12:42:19 -07:00
commit df977c4c1d
7 changed files with 225 additions and 1 deletions

View File

@ -35,6 +35,7 @@ const defaultConfig = `
},
"provisioners": {
"file": "packer-provisioner-file",
"shell": "packer-provisioner-shell"
}
}

View File

@ -0,0 +1,10 @@
package main
import (
"github.com/mitchellh/packer/packer/plugin"
"github.com/mitchellh/packer/provisioner/file"
)
func main() {
plugin.ServeProvisioner(new(file.Provisioner))
}

View File

@ -0,0 +1,53 @@
package file
import (
"errors"
"fmt"
"github.com/mitchellh/mapstructure"
"github.com/mitchellh/packer/packer"
"os"
)
type config struct {
// The local path of the file to upload.
Source string
// The remote path where the local file will be uploaded to.
Destination string
}
type Provisioner struct {
config config
}
func (p *Provisioner) Prepare(raws ...interface{}) error {
for _, raw := range raws {
if err := mapstructure.Decode(raw, &p.config); err != nil {
return err
}
}
errs := []error{}
if _, err := os.Stat(p.config.Source); err != nil {
errs = append(errs, fmt.Errorf("Bad source file '%s': %s", p.config.Source, err))
}
if len(p.config.Destination) == 0 {
errs = append(errs, errors.New("Destination must be specified."))
}
if len(errs) > 0 {
return &packer.MultiError{errs}
}
return nil
}
func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
ui.Say(fmt.Sprintf("Uploading %s => %s", p.config.Source, p.config.Destination))
f, err := os.Open(p.config.Source)
if err != nil {
return err
}
return comm.Upload(p.config.Destination, f)
}

View File

@ -0,0 +1,128 @@
package file
import (
"github.com/mitchellh/packer/packer"
"io"
"io/ioutil"
"os"
"strings"
"testing"
)
func TestProvisioner_Impl(t *testing.T) {
var raw interface{}
raw = &Provisioner{}
if _, ok := raw.(packer.Provisioner); !ok {
t.Fatalf("must be a provisioner")
}
}
func TestProvisionerPrepare_InvalidSource(t *testing.T) {
var p Provisioner
config := map[string]interface{}{"source": "/this/should/not/exist", "destination": "something"}
err := p.Prepare(config)
if err == nil {
t.Fatalf("should require existing file")
}
}
func TestProvisionerPrepare_ValidSource(t *testing.T) {
var p Provisioner
tf, err := ioutil.TempFile("", "packer")
if err != nil {
t.Fatalf("error tempfile: %s", err)
}
defer os.Remove(tf.Name())
config := map[string]interface{}{"source": tf.Name(), "destination": "something"}
err = p.Prepare(config)
if err != nil {
t.Fatalf("should allow valid file: %s", err)
}
}
func TestProvisionerPrepare_EmptyDestination(t *testing.T) {
var p Provisioner
config := map[string]interface{}{"source": "/this/exists"}
err := p.Prepare(config)
if err == nil {
t.Fatalf("should require destination path")
}
}
type stubUploadCommunicator struct {
dest string
data io.Reader
}
func (suc *stubUploadCommunicator) Download(src string, data io.Writer) error {
return nil
}
func (suc *stubUploadCommunicator) Upload(dest string, data io.Reader) error {
suc.dest = dest
suc.data = data
return nil
}
func (suc *stubUploadCommunicator) Start(cmd *packer.RemoteCmd) error {
return nil
}
type stubUi struct {
sayMessages string
}
func (su *stubUi) Ask(string) (string, error) {
return "", nil
}
func (su *stubUi) Error(string) {
}
func (su *stubUi) Message(string) {
}
func (su *stubUi) Say(msg string) {
su.sayMessages += msg
}
func TestProvisionerProvision_SendsFile(t *testing.T) {
var p Provisioner
tf, err := ioutil.TempFile("", "packer")
if err != nil {
t.Fatalf("error tempfile: %s", err)
}
defer os.Remove(tf.Name())
if _, err = tf.Write([]byte("hello")); err != nil {
t.Fatalf("error writing tempfile: %s", err)
}
config := map[string]interface{}{"source": tf.Name(), "destination": "something"}
p.Prepare(config)
ui := &stubUi{}
comm := &stubUploadCommunicator{}
err = p.Provision(ui, comm)
if err != nil {
t.Fatalf("should successfully provision: %s", err)
}
if !strings.Contains(ui.sayMessages, tf.Name()) {
t.Fatalf("should print source filename")
}
if !strings.Contains(ui.sayMessages, "something") {
t.Fatalf("should print destination filename")
}
if comm.dest != "something" {
t.Fatalf("should upload to configured destination")
}
read, err := ioutil.ReadAll(comm.data)
if err != nil || string(read) != "hello" {
t.Fatalf("should upload with source file's data")
}
}

View File

@ -0,0 +1,31 @@
---
layout: "docs"
---
# File Provisioner
Type: `file`
The file provisioner uploads files to machines build by Packer.
## Basic Example
<pre class="prettyprint">
{
"type": "file",
"source": "app.tar.gz",
"destination": "/tmp/app.tar.gz"
}
</pre>
## Configuration Reference
The available configuration options are listed below. All elements are required.
* `source` (string) - The path to a local file to upload to the machine. The
path can be absolute or relative. If it is relative, it is relative to the
working directory when ?Packer is executed.
* `destination` (string) - The path where the file will be uploaded to in the
machine. This value must be a writable location and any parent directories
must already exist.

View File

@ -23,7 +23,7 @@ The example below is fully functional.
## Configuration Reference
The reference of available configuratin options is listed below. The only
The reference of available configuration options is listed below. The only
required element is either "inline" or "script". Every other option is optional.
Exactly _one_ of the following is required:

View File

@ -36,6 +36,7 @@
<ul>
<li><h4>Provisioners</h4></li>
<li><a href="/docs/provisioners/shell.html">Shell Scripts</a></li>
<li><a href="/docs/provisioners/file.html">File Uploads</a></li>
<li><a href="/docs/provisioners/custom.html">Custom</a></li>
</ul>