diff --git a/CHANGELOG.md b/CHANGELOG.md index dcb835f15..935b002ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,25 @@ ## 0.5.2 (unreleased) +FEATURES: + +* **New post-processor:** `docker-import` - Import a Docker image + and give it a specific repository/tag. +* **New post-processor:** `docker-push` - Push an imported image to + a registry. + +IMPROVEMENTS: + +* core: Most downloads made by Packer now use a custom user agent. [GH-803] + BUG FIXES: +* core: Fix crash case if blank parameters are given to Packer. [GH-832] * builders/docker: user variables work properly. [GH-777] * builder/virtualbox,vmware: iso\_checksum is not required if the checksum type is "none" +* builder/virtualbox,vmware/qemu: Support for additional scancodes for + `boot_command` such as ``, ``, ``, etc. [GH-808] +* provisioners/ansible-local: Properly upload custom playbooks. [GH-829] ## 0.5.1 (01/02/2014) diff --git a/builder/docker/artifact_import.go b/builder/docker/artifact_import.go new file mode 100644 index 000000000..4c926eb53 --- /dev/null +++ b/builder/docker/artifact_import.go @@ -0,0 +1,33 @@ +package docker + +import ( + "fmt" +) + +// ImportArtifact is an Artifact implementation for when a container is +// exported from docker into a single flat file. +type ImportArtifact struct { + BuilderIdValue string + Driver Driver + IdValue string +} + +func (a *ImportArtifact) BuilderId() string { + return a.BuilderIdValue +} + +func (*ImportArtifact) Files() []string { + return nil +} + +func (a *ImportArtifact) Id() string { + return a.IdValue +} + +func (a *ImportArtifact) String() string { + return fmt.Sprintf("Imported Docker image: %s", a.Id()) +} + +func (a *ImportArtifact) Destroy() error { + return a.Driver.DeleteImage(a.Id()) +} diff --git a/builder/docker/artifact_import_test.go b/builder/docker/artifact_import_test.go new file mode 100644 index 000000000..971143953 --- /dev/null +++ b/builder/docker/artifact_import_test.go @@ -0,0 +1,57 @@ +package docker + +import ( + "errors" + "github.com/mitchellh/packer/packer" + "testing" +) + +func TestImportArtifact_impl(t *testing.T) { + var _ packer.Artifact = new(ImportArtifact) +} + +func TestImportArtifactBuilderId(t *testing.T) { + a := &ImportArtifact{BuilderIdValue: "foo"} + if a.BuilderId() != "foo" { + t.Fatalf("bad: %#v", a.BuilderId()) + } +} + +func TestImportArtifactFiles(t *testing.T) { + a := &ImportArtifact{} + if a.Files() != nil { + t.Fatalf("bad: %#v", a.Files()) + } +} + +func TestImportArtifactId(t *testing.T) { + a := &ImportArtifact{IdValue: "foo"} + if a.Id() != "foo" { + t.Fatalf("bad: %#v", a.Id()) + } +} + +func TestImportArtifactDestroy(t *testing.T) { + d := new(MockDriver) + a := &ImportArtifact{ + Driver: d, + IdValue: "foo", + } + + // No error + if err := a.Destroy(); err != nil { + t.Fatalf("err: %s", err) + } + if !d.DeleteImageCalled { + t.Fatal("delete image should be called") + } + if d.DeleteImageId != "foo" { + t.Fatalf("bad: %#v", d.DeleteImageId) + } + + // With an error + d.DeleteImageErr = errors.New("foo") + if err := a.Destroy(); err != d.DeleteImageErr { + t.Fatalf("err: %#v", err) + } +} diff --git a/builder/docker/driver.go b/builder/docker/driver.go index d182ba145..aaed2fc52 100644 --- a/builder/docker/driver.go +++ b/builder/docker/driver.go @@ -8,12 +8,21 @@ import ( // Docker. The Driver interface also allows the steps to be tested since // a mock driver can be shimmed in. type Driver interface { + // Delete an image that is imported into Docker + DeleteImage(id string) error + // Export exports the container with the given ID to the given writer. Export(id string, dst io.Writer) error + // Import imports a container from a tar file + Import(path, repo string) (string, error) + // Pull should pull down the given image. Pull(image string) error + // Push pushes an image to a Docker index/registry. + Push(name string) error + // StartContainer starts a container and returns the ID for that container, // along with a potential error. StartContainer(*ContainerConfig) (string, error) diff --git a/builder/docker/driver_docker.go b/builder/docker/driver_docker.go index 676045f1a..5c43e0078 100644 --- a/builder/docker/driver_docker.go +++ b/builder/docker/driver_docker.go @@ -6,6 +6,7 @@ import ( "github.com/mitchellh/packer/packer" "io" "log" + "os" "os/exec" "strings" ) @@ -15,6 +16,25 @@ type DockerDriver struct { Tpl *packer.ConfigTemplate } +func (d *DockerDriver) DeleteImage(id string) error { + var stderr bytes.Buffer + cmd := exec.Command("docker", "rmi", id) + cmd.Stderr = &stderr + + log.Printf("Deleting image: %s", id) + if err := cmd.Start(); err != nil { + return err + } + + if err := cmd.Wait(); err != nil { + err = fmt.Errorf("Error deleting image: %s\nStderr: %s", + err, stderr.String()) + return err + } + + return nil +} + func (d *DockerDriver) Export(id string, dst io.Writer) error { var stderr bytes.Buffer cmd := exec.Command("docker", "export", id) @@ -35,11 +55,49 @@ func (d *DockerDriver) Export(id string, dst io.Writer) error { return nil } +func (d *DockerDriver) Import(path string, repo string) (string, error) { + var stdout bytes.Buffer + cmd := exec.Command("docker", "import", "-", repo) + cmd.Stdout = &stdout + stdin, err := cmd.StdinPipe() + if err != nil { + return "", err + } + + // There should be only one artifact of the Docker builder + file, err := os.Open(path) + if err != nil { + return "", err + } + defer file.Close() + + if err := cmd.Start(); err != nil { + return "", err + } + + go func() { + defer stdin.Close() + io.Copy(stdin, file) + }() + + if err := cmd.Wait(); err != nil { + err = fmt.Errorf("Error importing container: %s", err) + return "", err + } + + return strings.TrimSpace(stdout.String()), nil +} + func (d *DockerDriver) Pull(image string) error { cmd := exec.Command("docker", "pull", image) return runAndStream(cmd, d.Ui) } +func (d *DockerDriver) Push(name string) error { + cmd := exec.Command("docker", "push", name) + return runAndStream(cmd, d.Ui) +} + func (d *DockerDriver) StartContainer(config *ContainerConfig) (string, error) { // Build up the template data var tplData startContainerTemplate diff --git a/builder/docker/driver_mock.go b/builder/docker/driver_mock.go index be28d680d..a48bb99f8 100644 --- a/builder/docker/driver_mock.go +++ b/builder/docker/driver_mock.go @@ -6,6 +6,20 @@ import ( // MockDriver is a driver implementation that can be used for tests. type MockDriver struct { + DeleteImageCalled bool + DeleteImageId string + DeleteImageErr error + + ImportCalled bool + ImportPath string + ImportRepo string + ImportId string + ImportErr error + + PushCalled bool + PushName string + PushErr error + ExportReader io.Reader ExportError error PullError error @@ -25,6 +39,12 @@ type MockDriver struct { VerifyCalled bool } +func (d *MockDriver) DeleteImage(id string) error { + d.DeleteImageCalled = true + d.DeleteImageId = id + return d.DeleteImageErr +} + func (d *MockDriver) Export(id string, dst io.Writer) error { d.ExportCalled = true d.ExportID = id @@ -39,12 +59,25 @@ func (d *MockDriver) Export(id string, dst io.Writer) error { return d.ExportError } +func (d *MockDriver) Import(path, repo string) (string, error) { + d.ImportCalled = true + d.ImportPath = path + d.ImportRepo = repo + return d.ImportId, d.ImportErr +} + func (d *MockDriver) Pull(image string) error { d.PullCalled = true d.PullImage = image return d.PullError } +func (d *MockDriver) Push(name string) error { + d.PushCalled = true + d.PushName = name + return d.PushErr +} + func (d *MockDriver) StartContainer(config *ContainerConfig) (string, error) { d.StartCalled = true d.StartConfig = config diff --git a/builder/qemu/step_type_boot_command.go b/builder/qemu/step_type_boot_command.go index 3b2dd62ad..6416e0ea1 100644 --- a/builder/qemu/step_type_boot_command.go +++ b/builder/qemu/step_type_boot_command.go @@ -92,6 +92,7 @@ func (s *stepTypeBootCommand) Run(state multistep.StateBag) multistep.StepAction func (*stepTypeBootCommand) Cleanup(multistep.StateBag) {} func vncSendString(c *vnc.ClientConn, original string) { + // Scancodes reference: https://github.com/qemu/qemu/blob/master/ui/vnc_keysym.h special := make(map[string]uint32) special[""] = 0xFF08 special[""] = 0xFFFF @@ -111,6 +112,16 @@ func vncSendString(c *vnc.ClientConn, original string) { special[""] = 0xFFC9 special[""] = 0xFF0D special[""] = 0xFF09 + special[""] = 0xFF52 + special[""] = 0xFF54 + special[""] = 0xFF51 + special[""] = 0xFF53 + special[""] = 0x020 + special[""] = 0xFF63 + special[""] = 0xFF50 + special[""] = 0xFF57 + special[""] = 0xFF55 + special[""] = 0xFF56 shiftedChars := "~!@#$%^&*()_+{}|:\"<>?" diff --git a/builder/virtualbox/iso/step_type_boot_command.go b/builder/virtualbox/iso/step_type_boot_command.go index d479b84e0..3e087f0df 100644 --- a/builder/virtualbox/iso/step_type_boot_command.go +++ b/builder/virtualbox/iso/step_type_boot_command.go @@ -118,6 +118,16 @@ func scancodes(message string) []string { special[""] = []string{"44", "c4"} special[""] = []string{"1c", "9c"} special[""] = []string{"0f", "8f"} + special[""] = []string{"48", "c8"} + special[""] = []string{"50", "d0"} + special[""] = []string{"4b", "cb"} + special[""] = []string{"4d", "cd"} + special[""] = []string{"39", "b9"} + special[""] = []string{"52", "d2"} + special[""] = []string{"47", "c7"} + special[""] = []string{"4f", "cf"} + special[""] = []string{"49", "c9"} + special[""] = []string{"51", "d1"} shiftedChars := "~!@#$%^&*()_+{}|:\"<>?" diff --git a/builder/vmware/iso/step_type_boot_command.go b/builder/vmware/iso/step_type_boot_command.go index 89e7ac432..4dbc7f82d 100644 --- a/builder/vmware/iso/step_type_boot_command.go +++ b/builder/vmware/iso/step_type_boot_command.go @@ -116,6 +116,7 @@ func (s *stepTypeBootCommand) Run(state multistep.StateBag) multistep.StepAction func (*stepTypeBootCommand) Cleanup(multistep.StateBag) {} func vncSendString(c *vnc.ClientConn, original string) { + // Scancodes reference: https://github.com/qemu/qemu/blob/master/ui/vnc_keysym.h special := make(map[string]uint32) special[""] = 0xFF08 special[""] = 0xFFFF @@ -135,6 +136,16 @@ func vncSendString(c *vnc.ClientConn, original string) { special[""] = 0xFFC9 special[""] = 0xFF0D special[""] = 0xFF09 + special[""] = 0xFF52 + special[""] = 0xFF54 + special[""] = 0xFF51 + special[""] = 0xFF53 + special[""] = 0x020 + special[""] = 0xFF63 + special[""] = 0xFF50 + special[""] = 0xFF57 + special[""] = 0xFF55 + special[""] = 0xFF56 shiftedChars := "~!@#$%^&*()_+{}|:\"<>?" diff --git a/common/download.go b/common/download.go index 2a0b37ee7..b5798b76c 100644 --- a/common/download.go +++ b/common/download.go @@ -43,6 +43,10 @@ type DownloadConfig struct { // for the downloader will be used to verify with this checksum after // it is downloaded. Checksum []byte + + // What to use for the user agent for HTTP requests. If set to "", use the + // default user agent provided by Go. + UserAgent string } // A DownloadClient helps download, verify checksums, etc. @@ -73,8 +77,8 @@ func HashForType(t string) hash.Hash { func NewDownloadClient(c *DownloadConfig) *DownloadClient { if c.DownloaderMap == nil { c.DownloaderMap = map[string]Downloader{ - "http": new(HTTPDownloader), - "https": new(HTTPDownloader), + "http": &HTTPDownloader{userAgent: c.UserAgent}, + "https": &HTTPDownloader{userAgent: c.UserAgent}, } } @@ -182,8 +186,9 @@ func (d *DownloadClient) VerifyChecksum(path string) (bool, error) { // HTTPDownloader is an implementation of Downloader that downloads // files over HTTP. type HTTPDownloader struct { - progress uint - total uint + progress uint + total uint + userAgent string } func (*HTTPDownloader) Cancel() { @@ -197,6 +202,10 @@ func (d *HTTPDownloader) Download(dst io.Writer, src *url.URL) error { return err } + if d.userAgent != "" { + req.Header.Set("User-Agent", d.userAgent) + } + httpClient := &http.Client{ Transport: &http.Transport{ Proxy: http.ProxyFromEnvironment, diff --git a/common/download_test.go b/common/download_test.go index f1fec941e..57b4ba7bc 100644 --- a/common/download_test.go +++ b/common/download_test.go @@ -4,6 +4,8 @@ import ( "crypto/md5" "encoding/hex" "io/ioutil" + "net/http" + "net/http/httptest" "os" "testing" ) @@ -41,6 +43,91 @@ func TestDownloadClient_VerifyChecksum(t *testing.T) { } } +func TestDownloadClientUsesDefaultUserAgent(t *testing.T) { + tf, err := ioutil.TempFile("", "packer") + if err != nil { + t.Fatalf("tempfile error: %s", err) + } + defer os.Remove(tf.Name()) + + defaultUserAgent := "" + asserted := false + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if defaultUserAgent == "" { + defaultUserAgent = r.UserAgent() + } else { + incomingUserAgent := r.UserAgent() + if incomingUserAgent != defaultUserAgent { + t.Fatalf("Expected user agent %s, got: %s", defaultUserAgent, incomingUserAgent) + } + + asserted = true + } + })) + + req, err := http.NewRequest("GET", server.URL, nil) + if err != nil { + t.Fatal(err) + } + + httpClient := &http.Client{ + Transport: &http.Transport{ + Proxy: http.ProxyFromEnvironment, + }, + } + + _, err = httpClient.Do(req) + if err != nil { + t.Fatal(err) + } + + config := &DownloadConfig{ + Url: server.URL, + TargetPath: tf.Name(), + } + + client := NewDownloadClient(config) + _, err = client.Get() + if err != nil { + t.Fatal(err) + } + + if !asserted { + t.Fatal("User-Agent never observed") + } +} + +func TestDownloadClientSetsUserAgent(t *testing.T) { + tf, err := ioutil.TempFile("", "packer") + if err != nil { + t.Fatalf("tempfile error: %s", err) + } + defer os.Remove(tf.Name()) + + asserted := false + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + asserted = true + if r.UserAgent() != "fancy user agent" { + t.Fatalf("Expected useragent fancy user agent, got: %s", r.UserAgent()) + } + })) + config := &DownloadConfig{ + Url: server.URL, + TargetPath: tf.Name(), + UserAgent: "fancy user agent", + } + + client := NewDownloadClient(config) + _, err = client.Get() + if err != nil { + t.Fatal(err) + } + + if !asserted { + t.Fatal("HTTP request never made") + } +} + func TestHashForType(t *testing.T) { if h := HashForType("md5"); h == nil { t.Fatalf("md5 hash is nil") diff --git a/common/step_download.go b/common/step_download.go index 4bf44eb5a..34156d52d 100644 --- a/common/step_download.go +++ b/common/step_download.go @@ -70,6 +70,7 @@ func (s *StepDownload) Run(state multistep.StateBag) multistep.StepAction { CopyFile: false, Hash: HashForType(s.ChecksumType), Checksum: checksum, + UserAgent: packer.VersionString(), } path, err, retry := s.download(config, state) diff --git a/config.go b/config.go index 483bfe090..14301a321 100644 --- a/config.go +++ b/config.go @@ -42,7 +42,9 @@ const defaultConfig = ` "post-processors": { "vagrant": "packer-post-processor-vagrant", - "vsphere": "packer-post-processor-vsphere" + "vsphere": "packer-post-processor-vsphere", + "docker-push": "packer-post-processor-docker-push", + "docker-import": "packer-post-processor-docker-import" }, "provisioners": { diff --git a/packer/environment.go b/packer/environment.go index 34dd8ba7d..a7067a682 100644 --- a/packer/environment.go +++ b/packer/environment.go @@ -221,7 +221,7 @@ func (e *coreEnvironment) Cli(args []string) (result int, err error) { // Trim up to the command name for i, v := range args { - if v[0] != '-' { + if len(v) > 0 && v[0] != '-' { args = args[i:] break } diff --git a/packer/environment_test.go b/packer/environment_test.go index b5ac7b9c8..65bc83057 100644 --- a/packer/environment_test.go +++ b/packer/environment_test.go @@ -198,10 +198,17 @@ func TestEnvironment_Cli_CallsRun(t *testing.T) { func TestEnvironment_DefaultCli_Empty(t *testing.T) { defaultEnv := testEnvironment() + // Test with no args exitCode, _ := defaultEnv.Cli([]string{}) if exitCode != 1 { t.Fatalf("bad: %d", exitCode) } + + // Test with only blank args + exitCode, _ = defaultEnv.Cli([]string{""}) + if exitCode != 1 { + t.Fatalf("bad: %d", exitCode) + } } func TestEnvironment_DefaultCli_Help(t *testing.T) { diff --git a/packer/version.go b/packer/version.go index 73c9f3018..31821a807 100644 --- a/packer/version.go +++ b/packer/version.go @@ -31,6 +31,18 @@ func (versionCommand) Run(env Environment, args []string) int { env.Ui().Machine("version-prelease", VersionPrerelease) env.Ui().Machine("version-commit", GitCommit) + env.Ui().Say(VersionString()) + return 0 +} + +func (versionCommand) Synopsis() string { + return "print Packer version" +} + +// VersionString returns the Packer version in human-readable +// form complete with pre-release and git commit info if it is +// available. +func VersionString() string { var versionString bytes.Buffer fmt.Fprintf(&versionString, "Packer v%s", Version) if VersionPrerelease != "" { @@ -41,10 +53,5 @@ func (versionCommand) Run(env Environment, args []string) int { } } - env.Ui().Say(versionString.String()) - return 0 -} - -func (versionCommand) Synopsis() string { - return "print Packer version" + return versionString.String() } diff --git a/plugin/post-processor-docker-import/main.go b/plugin/post-processor-docker-import/main.go new file mode 100644 index 000000000..e9446b113 --- /dev/null +++ b/plugin/post-processor-docker-import/main.go @@ -0,0 +1,15 @@ +package main + +import ( + "github.com/mitchellh/packer/packer/plugin" + "github.com/mitchellh/packer/post-processor/docker-import" +) + +func main() { + server, err := plugin.Server() + if err != nil { + panic(err) + } + server.RegisterPostProcessor(new(dockerimport.PostProcessor)) + server.Serve() +} diff --git a/plugin/post-processor-docker-import/main_test.go b/plugin/post-processor-docker-import/main_test.go new file mode 100644 index 000000000..06ab7d0f9 --- /dev/null +++ b/plugin/post-processor-docker-import/main_test.go @@ -0,0 +1 @@ +package main diff --git a/plugin/post-processor-docker-push/main.go b/plugin/post-processor-docker-push/main.go new file mode 100644 index 000000000..eb45b13bd --- /dev/null +++ b/plugin/post-processor-docker-push/main.go @@ -0,0 +1,15 @@ +package main + +import ( + "github.com/mitchellh/packer/packer/plugin" + "github.com/mitchellh/packer/post-processor/docker-push" +) + +func main() { + server, err := plugin.Server() + if err != nil { + panic(err) + } + server.RegisterPostProcessor(new(dockerpush.PostProcessor)) + server.Serve() +} diff --git a/plugin/post-processor-docker-push/main_test.go b/plugin/post-processor-docker-push/main_test.go new file mode 100644 index 000000000..06ab7d0f9 --- /dev/null +++ b/plugin/post-processor-docker-push/main_test.go @@ -0,0 +1 @@ +package main diff --git a/post-processor/docker-import/post-processor.go b/post-processor/docker-import/post-processor.go new file mode 100644 index 000000000..78543a754 --- /dev/null +++ b/post-processor/docker-import/post-processor.go @@ -0,0 +1,98 @@ +package dockerimport + +import ( + "fmt" + "github.com/mitchellh/packer/builder/docker" + "github.com/mitchellh/packer/common" + "github.com/mitchellh/packer/packer" +) + +const BuilderId = "packer.post-processor.docker-import" + +type Config struct { + common.PackerConfig `mapstructure:",squash"` + + Repository string `mapstructure:"repository"` + Tag string `mapstructure:"tag"` + + tpl *packer.ConfigTemplate +} + +type PostProcessor struct { + config Config +} + +func (p *PostProcessor) Configure(raws ...interface{}) error { + _, err := common.DecodeConfig(&p.config, raws...) + if err != nil { + return err + } + + p.config.tpl, err = packer.NewConfigTemplate() + if err != nil { + return err + } + p.config.tpl.UserVars = p.config.PackerUserVars + + // Accumulate any errors + errs := new(packer.MultiError) + + templates := map[string]*string{ + "repository": &p.config.Repository, + "tag": &p.config.Tag, + } + + for key, ptr := range templates { + if *ptr == "" { + errs = packer.MultiErrorAppend( + errs, fmt.Errorf("%s must be set", key)) + } + + *ptr, err = p.config.tpl.Process(*ptr, nil) + if err != nil { + errs = packer.MultiErrorAppend( + errs, fmt.Errorf("Error processing %s: %s", key, err)) + } + } + + if len(errs.Errors) > 0 { + return errs + } + + return nil + +} + +func (p *PostProcessor) PostProcess(ui packer.Ui, artifact packer.Artifact) (packer.Artifact, bool, error) { + if artifact.BuilderId() != docker.BuilderId { + err := fmt.Errorf( + "Unknown artifact type: %s\nCan only import from Docker builder artifacts.", + artifact.BuilderId()) + return nil, false, err + } + + importRepo := p.config.Repository + if p.config.Tag != "" { + importRepo += ":" + p.config.Tag + } + + driver := &docker.DockerDriver{Tpl: p.config.tpl, Ui: ui} + + ui.Message("Importing image: " + artifact.Id()) + ui.Message("Repository: " + importRepo) + id, err := driver.Import(artifact.Files()[0], importRepo) + if err != nil { + return nil, false, err + } + + ui.Message("Imported ID: " + id) + + // Build the artifact + artifact = &docker.ImportArtifact{ + BuilderIdValue: BuilderId, + Driver: driver, + IdValue: importRepo, + } + + return artifact, false, nil +} diff --git a/post-processor/docker-import/post-processor_test.go b/post-processor/docker-import/post-processor_test.go new file mode 100644 index 000000000..43ac0b4ef --- /dev/null +++ b/post-processor/docker-import/post-processor_test.go @@ -0,0 +1,31 @@ +package dockerimport + +import ( + "bytes" + "github.com/mitchellh/packer/packer" + "testing" +) + +func testConfig() map[string]interface{} { + return map[string]interface{}{} +} + +func testPP(t *testing.T) *PostProcessor { + var p PostProcessor + if err := p.Configure(testConfig()); err != nil { + t.Fatalf("err: %s", err) + } + + return &p +} + +func testUi() *packer.BasicUi { + return &packer.BasicUi{ + Reader: new(bytes.Buffer), + Writer: new(bytes.Buffer), + } +} + +func TestPostProcessor_ImplementsPostProcessor(t *testing.T) { + var _ packer.PostProcessor = new(PostProcessor) +} diff --git a/post-processor/docker-push/post-processor.go b/post-processor/docker-push/post-processor.go new file mode 100644 index 000000000..fa3b40f56 --- /dev/null +++ b/post-processor/docker-push/post-processor.go @@ -0,0 +1,67 @@ +package dockerpush + +import ( + "fmt" + "github.com/mitchellh/packer/builder/docker" + "github.com/mitchellh/packer/common" + "github.com/mitchellh/packer/packer" + "github.com/mitchellh/packer/post-processor/docker-import" + "strings" +) + +type Config struct { + common.PackerConfig `mapstructure:",squash"` + + tpl *packer.ConfigTemplate +} + +type PostProcessor struct { + config Config +} + +func (p *PostProcessor) Configure(raws ...interface{}) error { + _, err := common.DecodeConfig(&p.config, raws...) + if err != nil { + return err + } + + p.config.tpl, err = packer.NewConfigTemplate() + if err != nil { + return err + } + p.config.tpl.UserVars = p.config.PackerUserVars + + // Accumulate any errors + errs := new(packer.MultiError) + if len(errs.Errors) > 0 { + return errs + } + + return nil + +} + +func (p *PostProcessor) PostProcess(ui packer.Ui, artifact packer.Artifact) (packer.Artifact, bool, error) { + if artifact.BuilderId() != dockerimport.BuilderId { + err := fmt.Errorf( + "Unknown artifact type: %s\nCan only import from docker-import artifacts.", + artifact.BuilderId()) + return nil, false, err + } + + driver := &docker.DockerDriver{Tpl: p.config.tpl, Ui: ui} + + // Get the name. We strip off any tags from the name because the + // push doesn't use those. + name := artifact.Id() + if i := strings.Index(name, ":"); i >= 0 { + name = name[:i] + } + + ui.Message("Pushing: " + name) + if err := driver.Push(name); err != nil { + return nil, false, err + } + + return nil, false, nil +} diff --git a/post-processor/docker-push/post-processor_test.go b/post-processor/docker-push/post-processor_test.go new file mode 100644 index 000000000..7631da79d --- /dev/null +++ b/post-processor/docker-push/post-processor_test.go @@ -0,0 +1,31 @@ +package dockerpush + +import ( + "bytes" + "github.com/mitchellh/packer/packer" + "testing" +) + +func testConfig() map[string]interface{} { + return map[string]interface{}{} +} + +func testPP(t *testing.T) *PostProcessor { + var p PostProcessor + if err := p.Configure(testConfig()); err != nil { + t.Fatalf("err: %s", err) + } + + return &p +} + +func testUi() *packer.BasicUi { + return &packer.BasicUi{ + Reader: new(bytes.Buffer), + Writer: new(bytes.Buffer), + } +} + +func TestPostProcessor_ImplementsPostProcessor(t *testing.T) { + var _ packer.PostProcessor = new(PostProcessor) +} diff --git a/provisioner/ansible-local/provisioner.go b/provisioner/ansible-local/provisioner.go index 1cbbe2626..1347376f0 100644 --- a/provisioner/ansible-local/provisioner.go +++ b/provisioner/ansible-local/provisioner.go @@ -137,7 +137,7 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { } for _, src := range p.config.PlaybookPaths { dst := filepath.Join(p.config.StagingDir, "playbooks", filepath.Base(src)) - if err := p.uploadFile(ui, comm, dst, src); err != nil { + if err := p.uploadDir(ui, comm, dst, src); err != nil { return fmt.Errorf("Error uploading playbooks: %s", err) } } diff --git a/scripts/devcompile.sh b/scripts/devcompile.sh index 51f1b18f0..4a4274c5b 100755 --- a/scripts/devcompile.sh +++ b/scripts/devcompile.sh @@ -15,7 +15,7 @@ verify_go () { return 0 fi local IFS=. - local i ver1=($1) ver2=($2) + local i ver1="$1" ver2="$2" for ((i=${#ver1[@]}; i<${#ver2[@]}; i++)) do @@ -56,4 +56,10 @@ export XC_OS=$(go env GOOS) ./scripts/compile.sh # Move all the compiled things to the PATH -cp pkg/${XC_OS}_${XC_ARCH}/* ${GOPATH}/bin +case $(uname) in + CYGWIN*) + GOPATH="$(cygpath $GOPATH)" + ;; +esac +IFS=: MAIN_GOPATH=( $GOPATH ) +cp pkg/${XC_OS}_${XC_ARCH}/* ${MAIN_GOPATH}/bin diff --git a/website/source/docs/builders/digitalocean.html.markdown b/website/source/docs/builders/digitalocean.html.markdown index a2525a840..cf637daff 100644 --- a/website/source/docs/builders/digitalocean.html.markdown +++ b/website/source/docs/builders/digitalocean.html.markdown @@ -36,14 +36,14 @@ Required: Optional: * `image_id` (int) - The ID of the base image to use. This is the image that - will be used to launch a new droplet and provision it. Defaults to "284203", - which happens to be "Ubuntu 12.04 x64 Server." + will be used to launch a new droplet and provision it. Defaults to "1505447", + which happens to be "Ubuntu 12.04.3 x64 Server." * `region_id` (int) - The ID of the region to launch the droplet in. Consequently, this is the region where the snapshot will be available. This defaults to - "1", which is "New York." + "1", which is "New York 1." -* `size_id` (int) - The ID of the droplet size to use. This defaults to "66," +* `size_id` (int) - The ID of the droplet size to use. This defaults to "66", which is the 512MB droplet. * `private_networking` (bool) - Set to `true` to enable private networking diff --git a/website/source/docs/builders/docker.html.markdown b/website/source/docs/builders/docker.html.markdown index fbced5009..315d5ff63 100644 --- a/website/source/docs/builders/docker.html.markdown +++ b/website/source/docs/builders/docker.html.markdown @@ -64,15 +64,35 @@ Optional: `["run", "-d", "-i", "-t", "-v", "{{.Volumes}}", "{{.Image}}", "/bin/bash"]`. As you can see, you have a couple template variables to customize, as well. -## Using the generated artifact +## Using the Artifact Once the tar artifact has been generated, you will likely want to import, tag, -and push it to a container repository. Until packer supports management of the -docker image metadata, this process is manual. For example, the following will -import `mycontainer-123456789.tar` to the repository -`registry.mydomain.com/mycontainer`, tagged with `latest`: +and push it to a container repository. Packer can do this for you automatically +with the [docker-import](/docs/post-processors/docker-import.html) and +[docker-push](/docs/post-processors/docker-push.html) post-processors. - sudo docker import - registry.mydomain.com/mycontainer:latest < mycontainer-123456789.tar +The example below shows a full configuration that would import and push +the created image: + +
+{
+    "post-processors": [
+		[
+			{
+				"type": "docker-import",
+				"repository": "mitchellh/packer",
+				"tag": "0.7"
+			},
+			"docker-push"
+		]
+	]
+}
+
+ +If you want to do this manually, however, perhaps from a script, you can +import the image using the process below: + + docker import - registry.mydomain.com/mycontainer:latest < artifact.tar You can then add additional tags and push the image as usual with `docker tag` and `docker push`, respectively. @@ -103,8 +123,3 @@ by Packer in the future: volumes, and other metadata. Packer builds a raw Docker container image that has none of this metadata. You can pass in much of this metadata at runtime with `docker run`. - -* Images made without dockerfiles are missing critical metadata that - make them easily pushable to the Docker registry. You can work around - this by using a metadata-only Dockerfile with the exported image and - building that. A future Packer version will automatically do this for you. diff --git a/website/source/docs/builders/googlecompute.markdown b/website/source/docs/builders/googlecompute.markdown index d68105177..6443a530a 100644 --- a/website/source/docs/builders/googlecompute.markdown +++ b/website/source/docs/builders/googlecompute.markdown @@ -28,16 +28,10 @@ Follow the steps below: 2. Click on the project you want to use Packer with (or create one if you don't have one yet). 3. Click "APIs & auth" in the left sidebar -4. Click "Registered apps" in the left sidebar -5. Click "Register App" and register a "Web Application". Choose any - name you'd like. -7. After creating the app, click "Certificate" (below the OAuth 2.0 Client - ID section), and click "Download JSON". This is your _client secrets JSON_ - file. Make sure you didn't download the JSON from the "OAuth 2.0" section! - This is a common mistake and will cause the builder to not work. -8. Next, click "Generate Certificate". You should be prompted to download - a private key. Note the password for the private key! This private key - is your _client private key_. +4. Click "Credentials" in the left sidebar +5. Click "Create New Client ID" and choose "Service Account" +6. A private key will be downloaded for you. Note the password for the private key! This private key is your _client private key_. +7. After creating the account, click "Download JSON". This is your _client secrets JSON_ file. Make sure you didn't download the JSON from the "OAuth 2.0" section! This is a common mistake and will cause the builder to not work. Finally, one last step, you'll have to convert the `p12` file you got from Google into the PEM format. You can do this with OpenSSL, which diff --git a/website/source/docs/builders/qemu.html.markdown b/website/source/docs/builders/qemu.html.markdown index e432e6796..e6c7f30f0 100644 --- a/website/source/docs/builders/qemu.html.markdown +++ b/website/source/docs/builders/qemu.html.markdown @@ -8,7 +8,7 @@ Type: `qemu` The Qemu builder is able to create [KVM](http://www.linux-kvm.org) and [Xen](http://www.xenproject.org) virtual machine images. Support -for Xen is experimanetal at this time. +for Xen is experimental at this time. The builder builds a virtual machine by creating a new virtual machine from scratch, booting it, installing an OS, rebooting the machine with the diff --git a/website/source/docs/builders/virtualbox-iso.html.markdown b/website/source/docs/builders/virtualbox-iso.html.markdown index 801b7b5a6..490fbeb65 100644 --- a/website/source/docs/builders/virtualbox-iso.html.markdown +++ b/website/source/docs/builders/virtualbox-iso.html.markdown @@ -5,7 +5,7 @@ page_title: "VirtualBox Builder (from an ISO)" # VirtualBox Builder (from an ISO) -Type: `virtualbox` +Type: `virtualbox-iso` The VirtualBox builder is able to create [VirtualBox](https://www.virtualbox.org/) virtual machines and export them in the OVF format, starting from an diff --git a/website/source/docs/extend/post-processor.html.markdown b/website/source/docs/extend/post-processor.html.markdown index bec75b06b..b453dda29 100644 --- a/website/source/docs/extend/post-processor.html.markdown +++ b/website/source/docs/extend/post-processor.html.markdown @@ -46,11 +46,11 @@ type PostProcessor interface { The `Configure` method for each post-processor is called early in the build process to configure the post-processor. The configuration is passed -in as a raw `interface{]`. The configure method is responsible for translating +in as a raw `interface{}`. The configure method is responsible for translating this configuration into an internal structure, validating it, and returning any errors. -For decoding the `interface{]` into a meaningful structure, the +For decoding the `interface{}` into a meaningful structure, the [mapstructure](https://github.com/mitchellh/mapstructure) library is recommended. Mapstructure will take an `interface{}` and decode it into an arbitrarily complex struct. If there are any errors, it generates very diff --git a/website/source/docs/post-processors/docker-import.html.markdown b/website/source/docs/post-processors/docker-import.html.markdown new file mode 100644 index 000000000..e2ca3c93b --- /dev/null +++ b/website/source/docs/post-processors/docker-import.html.markdown @@ -0,0 +1,44 @@ +--- +layout: "docs" +page_title: "docker-import Post-Processor" +--- + +# Docker Import Post-Processor + +Type: `docker-import` + +The Docker import post-processor takes an artifact from the +[docker builder](/docs/builders/docker.html) and imports it with Docker +locally. This allows you to apply a repository and tag to the image +and lets you use the other Docker post-processors such as +[docker-push](/docs/post-processors/docker-push.html) to push the image +to a registry. + +## Configuration + +The configuration for this post-processor is extremely simple. At least +a repository is required. The tag is optional. + +* `repository` (string) - The repository of the imported image. + +* `tag` (string) - The tag for the imported image. By default this is not + set. + +## Example + +An example is shown below, showing only the post-processor configuration: + +
+{
+  "type": "docker-import",
+  "repository": "mitchellh/packer",
+  "tag": "0.7"
+}
+
+ +This example would take the image created by the Docker builder +and import it into the local Docker process with a name of `mitchellh/packer:0.7`. + +Following this, you can use the +[docker-push](/docs/post-processors/docker-push.html) +post-processor to push it to a registry, if you want. diff --git a/website/source/docs/post-processors/docker-push.html.markdown b/website/source/docs/post-processors/docker-push.html.markdown new file mode 100644 index 000000000..602e126e0 --- /dev/null +++ b/website/source/docs/post-processors/docker-push.html.markdown @@ -0,0 +1,28 @@ +--- +layout: "docs" +page_title: "Docker Push Post-Processor" +--- + +# Docker Push Post-Processor + +Type: `docker-push` + +The Docker push post-processor takes an artifact from the +[docker-import](/docs/post-processors/docker-import.html) post-processor +and pushes it to a Docker registry. + +
+Before you use this, you must manually docker login +to the proper repository. A future version of Packer will automate this +for you, but for now you must manually do this. +
+ +## Configuration + +This post-processor has no configuration! Simply add it to your chain +of post-processors and the image will be uploaded. + +## Example + +For an example of using docker-push, see the section on using +generated artifacts from the [docker builder](/docs/builders/docker.html). diff --git a/website/source/intro/getting-started/build-image.html.markdown b/website/source/intro/getting-started/build-image.html.markdown index e37f56664..158733bb9 100644 --- a/website/source/intro/getting-started/build-image.html.markdown +++ b/website/source/intro/getting-started/build-image.html.markdown @@ -63,7 +63,7 @@ briefly. Create a file `example.json` and fill it with the following contents: } -When building, you'll pass in the `aws_access_key` and `aws_access_key` as +When building, you'll pass in the `aws_access_key` and `aws_secret_key` as a [user variable](/docs/templates/user-variables.html), keeping your secret keys out of the template. You can create security credentials on [this page](https://console.aws.amazon.com/iam/home?#security_credential). diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index ed9a45b58..134a2e658 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -55,6 +55,8 @@