diff --git a/config.go b/config.go index f2ed48956..7bfaabc76 100644 --- a/config.go +++ b/config.go @@ -35,6 +35,7 @@ const defaultConfig = ` }, "provisioners": { + "file": "packer-provisioner-file", "shell": "packer-provisioner-shell" } } diff --git a/plugin/provisioner-file/main.go b/plugin/provisioner-file/main.go new file mode 100644 index 000000000..1f5a63413 --- /dev/null +++ b/plugin/provisioner-file/main.go @@ -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)) +} diff --git a/provisioner/file/provisioner.go b/provisioner/file/provisioner.go new file mode 100644 index 000000000..711beffa6 --- /dev/null +++ b/provisioner/file/provisioner.go @@ -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) +} diff --git a/provisioner/file/provisioner_test.go b/provisioner/file/provisioner_test.go new file mode 100644 index 000000000..017bd3365 --- /dev/null +++ b/provisioner/file/provisioner_test.go @@ -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") + } +} diff --git a/website/source/docs/provisioners/file.html.markdown b/website/source/docs/provisioners/file.html.markdown new file mode 100644 index 000000000..30d082f0c --- /dev/null +++ b/website/source/docs/provisioners/file.html.markdown @@ -0,0 +1,31 @@ +--- +layout: "docs" +--- + +# File Provisioner + +Type: `file` + +The file provisioner uploads files to machines build by Packer. + +## Basic Example + +
+{ + "type": "file", + "source": "app.tar.gz", + "destination": "/tmp/app.tar.gz" +} ++ +## 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. diff --git a/website/source/docs/provisioners/shell.html.markdown b/website/source/docs/provisioners/shell.html.markdown index 9dc0d64c0..51ddce502 100644 --- a/website/source/docs/provisioners/shell.html.markdown +++ b/website/source/docs/provisioners/shell.html.markdown @@ -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: diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index 2582862e9..a6ed7f2cd 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -36,6 +36,7 @@