Merge branch 'andytson-feature/docker-image'

This commit is contained in:
Mitchell Hashimoto 2014-09-04 17:56:52 -07:00
commit 5f126dc154
18 changed files with 636 additions and 8 deletions

View File

@ -35,6 +35,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
&StepPull{}, &StepPull{},
&StepRun{}, &StepRun{},
&StepProvision{}, &StepProvision{},
&StepCommit{},
&StepExport{}, &StepExport{},
} }
@ -64,8 +65,17 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
return nil, rawErr.(error) return nil, rawErr.(error)
} }
var artifact packer.Artifact
// No errors, must've worked // No errors, must've worked
artifact := &ExportArtifact{path: b.config.ExportPath} if b.config.Export {
artifact = &ExportArtifact{path: b.config.ExportPath}
} else {
artifact = &ImportArtifact{
IdValue: state.Get("image_id").(string),
BuilderIdValue: "packer.post-processor.docker-import",
Driver: driver,
}
}
return artifact, nil return artifact, nil
} }

View File

@ -10,6 +10,7 @@ type Config struct {
common.PackerConfig `mapstructure:",squash"` common.PackerConfig `mapstructure:",squash"`
ExportPath string `mapstructure:"export_path"` ExportPath string `mapstructure:"export_path"`
Export bool
Image string Image string
Pull bool Pull bool
RunCommand []string `mapstructure:"run_command"` RunCommand []string `mapstructure:"run_command"`
@ -71,10 +72,7 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
} }
} }
if c.ExportPath == "" { c.Export = c.ExportPath != ""
errs = packer.MultiErrorAppend(errs,
fmt.Errorf("export_path must be specified"))
}
if c.Image == "" { if c.Image == "" {
errs = packer.MultiErrorAppend(errs, errs = packer.MultiErrorAppend(errs,

View File

@ -46,13 +46,19 @@ func TestConfigPrepare_exportPath(t *testing.T) {
// No export path // No export path
delete(raw, "export_path") delete(raw, "export_path")
_, warns, errs := NewConfig(raw) c, warns, errs := NewConfig(raw)
testConfigErr(t, warns, errs) testConfigOk(t, warns, errs)
if c.Export {
t.Fatal("should not export")
}
// Good export path // Good export path
raw["export_path"] = "good" raw["export_path"] = "good"
_, warns, errs = NewConfig(raw) c, warns, errs = NewConfig(raw)
testConfigOk(t, warns, errs) testConfigOk(t, warns, errs)
if !c.Export {
t.Fatal("should export")
}
} }
func TestConfigPrepare_image(t *testing.T) { func TestConfigPrepare_image(t *testing.T) {

View File

@ -8,6 +8,9 @@ import (
// Docker. The Driver interface also allows the steps to be tested since // Docker. The Driver interface also allows the steps to be tested since
// a mock driver can be shimmed in. // a mock driver can be shimmed in.
type Driver interface { type Driver interface {
// Commit the container to a tag
Commit(id string) (string, error)
// Delete an image that is imported into Docker // Delete an image that is imported into Docker
DeleteImage(id string) error DeleteImage(id string) error
@ -23,6 +26,9 @@ type Driver interface {
// Push pushes an image to a Docker index/registry. // Push pushes an image to a Docker index/registry.
Push(name string) error Push(name string) error
// Save an image with the given ID to the given writer.
SaveImage(id string, dst io.Writer) error
// StartContainer starts a container and returns the ID for that container, // StartContainer starts a container and returns the ID for that container,
// along with a potential error. // along with a potential error.
StartContainer(*ContainerConfig) (string, error) StartContainer(*ContainerConfig) (string, error)
@ -30,6 +36,9 @@ type Driver interface {
// StopContainer forcibly stops a container. // StopContainer forcibly stops a container.
StopContainer(id string) error StopContainer(id string) error
// TagImage tags the image with the given ID
TagImage(id string, repo string) error
// Verify verifies that the driver can run // Verify verifies that the driver can run
Verify() error Verify() error
} }

View File

@ -35,6 +35,27 @@ func (d *DockerDriver) DeleteImage(id string) error {
return nil return nil
} }
func (d *DockerDriver) Commit(id string) (string, error) {
var stdout bytes.Buffer
var stderr bytes.Buffer
cmd := exec.Command("docker", "commit", id)
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Start(); err != nil {
return "", err
}
if err := cmd.Wait(); err != nil {
err = fmt.Errorf("Error committing container: %s\nStderr: %s",
err, stderr.String())
return "", err
}
return strings.TrimSpace(stdout.String()), nil
}
func (d *DockerDriver) Export(id string, dst io.Writer) error { func (d *DockerDriver) Export(id string, dst io.Writer) error {
var stderr bytes.Buffer var stderr bytes.Buffer
cmd := exec.Command("docker", "export", id) cmd := exec.Command("docker", "export", id)
@ -98,6 +119,26 @@ func (d *DockerDriver) Push(name string) error {
return runAndStream(cmd, d.Ui) return runAndStream(cmd, d.Ui)
} }
func (d *DockerDriver) SaveImage(id string, dst io.Writer) error {
var stderr bytes.Buffer
cmd := exec.Command("docker", "save", id)
cmd.Stdout = dst
cmd.Stderr = &stderr
log.Printf("Exporting image: %s", id)
if err := cmd.Start(); err != nil {
return err
}
if err := cmd.Wait(); err != nil {
err = fmt.Errorf("Error exporting: %s\nStderr: %s",
err, stderr.String())
return err
}
return nil
}
func (d *DockerDriver) StartContainer(config *ContainerConfig) (string, error) { func (d *DockerDriver) StartContainer(config *ContainerConfig) (string, error) {
// Build up the template data // Build up the template data
var tplData startContainerTemplate var tplData startContainerTemplate
@ -156,6 +197,24 @@ func (d *DockerDriver) StopContainer(id string) error {
return exec.Command("docker", "rm", id).Run() return exec.Command("docker", "rm", id).Run()
} }
func (d *DockerDriver) TagImage(id string, repo string) error {
var stderr bytes.Buffer
cmd := exec.Command("docker", "tag", id, repo)
cmd.Stderr = &stderr
if err := cmd.Start(); err != nil {
return err
}
if err := cmd.Wait(); err != nil {
err = fmt.Errorf("Error tagging image: %s\nStderr: %s",
err, stderr.String())
return err
}
return nil
}
func (d *DockerDriver) Verify() error { func (d *DockerDriver) Verify() error {
if _, err := exec.LookPath("docker"); err != nil { if _, err := exec.LookPath("docker"); err != nil {
return err return err

View File

@ -6,6 +6,11 @@ import (
// MockDriver is a driver implementation that can be used for tests. // MockDriver is a driver implementation that can be used for tests.
type MockDriver struct { type MockDriver struct {
CommitCalled bool
CommitContainerId string
CommitImageId string
CommitErr error
DeleteImageCalled bool DeleteImageCalled bool
DeleteImageId string DeleteImageId string
DeleteImageErr error DeleteImageErr error
@ -20,6 +25,16 @@ type MockDriver struct {
PushName string PushName string
PushErr error PushErr error
SaveImageCalled bool
SaveImageId string
SaveImageReader io.Reader
SaveImageError error
TagImageCalled bool
TagImageImageId string
TagImageRepo string
TagImageErr error
ExportReader io.Reader ExportReader io.Reader
ExportError error ExportError error
PullError error PullError error
@ -39,6 +54,12 @@ type MockDriver struct {
VerifyCalled bool VerifyCalled bool
} }
func (d *MockDriver) Commit(id string) (string, error) {
d.CommitCalled = true
d.CommitContainerId = id
return d.CommitImageId, d.CommitErr
}
func (d *MockDriver) DeleteImage(id string) error { func (d *MockDriver) DeleteImage(id string) error {
d.DeleteImageCalled = true d.DeleteImageCalled = true
d.DeleteImageId = id d.DeleteImageId = id
@ -78,6 +99,20 @@ func (d *MockDriver) Push(name string) error {
return d.PushErr return d.PushErr
} }
func (d *MockDriver) SaveImage(id string, dst io.Writer) error {
d.SaveImageCalled = true
d.SaveImageId = id
if d.SaveImageReader != nil {
_, err := io.Copy(dst, d.SaveImageReader)
if err != nil {
return err
}
}
return d.SaveImageError
}
func (d *MockDriver) StartContainer(config *ContainerConfig) (string, error) { func (d *MockDriver) StartContainer(config *ContainerConfig) (string, error) {
d.StartCalled = true d.StartCalled = true
d.StartConfig = config d.StartConfig = config
@ -90,6 +125,13 @@ func (d *MockDriver) StopContainer(id string) error {
return d.StopError return d.StopError
} }
func (d *MockDriver) TagImage(id string, repo string) error {
d.TagImageCalled = true
d.TagImageImageId = id
d.TagImageRepo = repo
return d.TagImageErr
}
func (d *MockDriver) Verify() error { func (d *MockDriver) Verify() error {
d.VerifyCalled = true d.VerifyCalled = true
return d.VerifyError return d.VerifyError

View File

@ -0,0 +1,40 @@
package docker
import (
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
)
// StepCommit commits the container to a image.
type StepCommit struct {
imageId string
}
func (s *StepCommit) Run(state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(*Config)
driver := state.Get("driver").(Driver)
containerId := state.Get("container_id").(string)
ui := state.Get("ui").(packer.Ui)
if config.Export {
return multistep.ActionContinue
}
ui.Say("Committing the container")
imageId, err := driver.Commit(containerId)
if err != nil {
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
// Save the container ID
s.imageId = imageId
state.Put("image_id", s.imageId)
ui.Message(fmt.Sprintf("Image ID: %s", s.imageId))
return multistep.ActionContinue
}
func (s *StepCommit) Cleanup(state multistep.StateBag) {}

View File

@ -0,0 +1,95 @@
package docker
import (
"errors"
"github.com/mitchellh/multistep"
"testing"
)
func testStepCommitState(t *testing.T) multistep.StateBag {
state := testState(t)
state.Put("container_id", "foo")
return state
}
func TestStepCommit_impl(t *testing.T) {
var _ multistep.Step = new(StepCommit)
}
func TestStepCommit(t *testing.T) {
state := testStepCommitState(t)
step := new(StepCommit)
defer step.Cleanup(state)
config := state.Get("config").(*Config)
config.Export = false
driver := state.Get("driver").(*MockDriver)
driver.CommitImageId = "bar"
// run the step
if action := step.Run(state); action != multistep.ActionContinue {
t.Fatalf("bad action: %#v", action)
}
// verify we did the right thing
if !driver.CommitCalled {
t.Fatal("should've called")
}
// verify the ID is saved
idRaw, ok := state.GetOk("image_id")
if !ok {
t.Fatal("should've saved ID")
}
id := idRaw.(string)
if id != driver.CommitImageId {
t.Fatalf("bad: %#v", id)
}
}
func TestStepCommit_skip(t *testing.T) {
state := testStepCommitState(t)
step := new(StepCommit)
defer step.Cleanup(state)
config := state.Get("config").(*Config)
config.Export = true
driver := state.Get("driver").(*MockDriver)
// run the step
if action := step.Run(state); action != multistep.ActionContinue {
t.Fatalf("bad action: %#v", action)
}
// verify we did the right thing
if driver.CommitCalled {
t.Fatal("shouldn't have called")
}
// verify the ID is not saved
if _, ok := state.GetOk("image_id"); ok {
t.Fatal("shouldn't save image ID")
}
}
func TestStepCommit_error(t *testing.T) {
state := testStepCommitState(t)
step := new(StepCommit)
defer step.Cleanup(state)
config := state.Get("config").(*Config)
config.Export = false
driver := state.Get("driver").(*MockDriver)
driver.CommitErr = errors.New("foo")
// run the step
if action := step.Run(state); action != multistep.ActionHalt {
t.Fatalf("bad action: %#v", action)
}
// verify the ID is not saved
if _, ok := state.GetOk("image_id"); ok {
t.Fatal("shouldn't save image ID")
}
}

View File

@ -12,6 +12,11 @@ type StepExport struct{}
func (s *StepExport) Run(state multistep.StateBag) multistep.StepAction { func (s *StepExport) Run(state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(*Config) config := state.Get("config").(*Config)
if !config.Export {
return multistep.ActionContinue
}
driver := state.Get("driver").(Driver) driver := state.Get("driver").(Driver)
containerId := state.Get("container_id").(string) containerId := state.Get("container_id").(string)
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)

View File

@ -34,6 +34,7 @@ func TestStepExport(t *testing.T) {
config := state.Get("config").(*Config) config := state.Get("config").(*Config)
config.ExportPath = tf.Name() config.ExportPath = tf.Name()
config.Export = true
driver := state.Get("driver").(*MockDriver) driver := state.Get("driver").(*MockDriver)
driver.ExportReader = bytes.NewReader([]byte("data!")) driver.ExportReader = bytes.NewReader([]byte("data!"))
@ -61,6 +62,26 @@ func TestStepExport(t *testing.T) {
} }
} }
func TestStepExport_skip(t *testing.T) {
state := testStepExportState(t)
step := new(StepExport)
defer step.Cleanup(state)
config := state.Get("config").(*Config)
config.Export = false
driver := state.Get("driver").(*MockDriver)
// run the step
if action := step.Run(state); action != multistep.ActionContinue {
t.Fatalf("bad action: %#v", action)
}
// verify we did the right thing
if driver.ExportCalled {
t.Fatal("shouldn't have exported")
}
}
func TestStepExport_error(t *testing.T) { func TestStepExport_error(t *testing.T) {
state := testStepExportState(t) state := testStepExportState(t)
step := new(StepExport) step := new(StepExport)
@ -79,6 +100,7 @@ func TestStepExport_error(t *testing.T) {
config := state.Get("config").(*Config) config := state.Get("config").(*Config)
config.ExportPath = tf.Name() config.ExportPath = tf.Name()
config.Export = true
driver := state.Get("driver").(*MockDriver) driver := state.Get("driver").(*MockDriver)
driver.ExportError = errors.New("foo") driver.ExportError = errors.New("foo")

View File

@ -0,0 +1,15 @@
package main
import (
"github.com/mitchellh/packer/packer/plugin"
"github.com/mitchellh/packer/post-processor/docker-save"
)
func main() {
server, err := plugin.Server()
if err != nil {
panic(err)
}
server.RegisterPostProcessor(new(dockersave.PostProcessor))
server.Serve()
}

View File

@ -0,0 +1 @@
package main

View File

@ -0,0 +1,15 @@
package main
import (
"github.com/mitchellh/packer/packer/plugin"
"github.com/mitchellh/packer/post-processor/docker-tag"
)
func main() {
server, err := plugin.Server()
if err != nil {
panic(err)
}
server.RegisterPostProcessor(new(dockertag.PostProcessor))
server.Serve()
}

View File

@ -0,0 +1 @@
package main

View File

@ -0,0 +1,104 @@
package dockersave
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"
"os"
)
const BuilderId = "packer.post-processor.docker-save"
type Config struct {
common.PackerConfig `mapstructure:",squash"`
Path string `mapstructure:"path"`
tpl *packer.ConfigTemplate
}
type PostProcessor struct {
Driver docker.Driver
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{
"path": &p.config.Path,
}
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() != dockerimport.BuilderId {
err := fmt.Errorf(
"Unknown artifact type: %s\nCan only save Docker builder artifacts.",
artifact.BuilderId())
return nil, false, err
}
path := p.config.Path
// Open the file that we're going to write to
f, err := os.Create(path)
if err != nil {
err := fmt.Errorf("Error creating output file: %s", err)
return nil, false, err
}
driver := p.Driver
if driver == nil {
// If no driver is set, then we use the real driver
driver = &docker.DockerDriver{Tpl: p.config.tpl, Ui: ui}
}
ui.Message("Saving image: " + artifact.Id())
if err := driver.SaveImage(artifact.Id(), f); err != nil {
f.Close()
os.Remove(f.Name())
return nil, false, err
}
f.Close()
ui.Message("Saved to: " + path)
return artifact, true, nil
}

View File

@ -0,0 +1,31 @@
package dockersave
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)
}

View File

@ -0,0 +1,103 @@
package dockertag
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"
)
const BuilderId = "packer.post-processor.docker-tag"
type Config struct {
common.PackerConfig `mapstructure:",squash"`
Repository string `mapstructure:"repository"`
Tag string `mapstructure:"tag"`
tpl *packer.ConfigTemplate
}
type PostProcessor struct {
Driver docker.Driver
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() != dockerimport.BuilderId {
err := fmt.Errorf(
"Unknown artifact type: %s\nCan only tag from Docker builder artifacts.",
artifact.BuilderId())
return nil, false, err
}
driver := p.Driver
if driver == nil {
// If no driver is set, then we use the real driver
driver = &docker.DockerDriver{Tpl: p.config.tpl, Ui: ui}
}
importRepo := p.config.Repository
if p.config.Tag != "" {
importRepo += ":" + p.config.Tag
}
ui.Message("Tagging image: " + artifact.Id())
ui.Message("Repository: " + importRepo)
err := driver.TagImage(artifact.Id(), importRepo)
if err != nil {
return nil, false, err
}
// Build the artifact
artifact = &docker.ImportArtifact{
BuilderIdValue: BuilderId,
Driver: driver,
IdValue: importRepo,
}
return artifact, true, nil
}

View File

@ -0,0 +1,72 @@
package dockertag
import (
"bytes"
"github.com/mitchellh/packer/builder/docker"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/packer"
"github.com/mitchellh/packer/post-processor/docker-import"
"testing"
)
func testConfig() map[string]interface{} {
return map[string]interface{}{
"repository": "foo",
"tag": "bar",
}
}
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)
}
func TestPostProcessor_PostProcess(t *testing.T) {
driver := &docker.MockDriver{}
p := &PostProcessor{Driver: driver}
_, err := common.DecodeConfig(&p.config, testConfig())
if err != nil {
t.Fatalf("err %s", err)
}
artifact := &packer.MockArtifact{
BuilderIdValue: dockerimport.BuilderId,
IdValue: "1234567890abcdef",
}
result, keep, err := p.PostProcess(testUi(), artifact)
if _, ok := result.(packer.Artifact); !ok {
t.Fatal("should be instance of Artifact")
}
if !keep {
t.Fatal("should keep")
}
if err != nil {
t.Fatalf("err: %s", err)
}
if !driver.TagImageCalled {
t.Fatal("should call TagImage")
}
if driver.TagImageImageId != "1234567890abcdef" {
t.Fatal("bad image id")
}
if driver.TagImageRepo != "foo:bar" {
t.Fatal("bad repo")
}
}