diff --git a/cli/command/command.go b/cli/command/command.go index fc11aa1d9..4f92a5bbd 100644 --- a/cli/command/command.go +++ b/cli/command/command.go @@ -23,6 +23,10 @@ func CreateCommand(factory app.ProjectFactory) cli.Command { Name: "force-recreate", Usage: "Recreate containers even if their configuration and image haven't changed. Incompatible with --no-recreate.", }, + cli.BoolFlag{ + Name: "no-build", + Usage: "Don't build an image, even if it's missing.", + }, }, } } @@ -89,6 +93,10 @@ func UpCommand(factory app.ProjectFactory) cli.Command { Name: "d", Usage: "Do not block and log", }, + cli.BoolFlag{ + Name: "no-build", + Usage: "Don't build an image, even if it's missing.", + }, cli.BoolFlag{ Name: "no-recreate", Usage: "If containers already exist, don't recreate them. Incompatible with --force-recreate.", @@ -283,6 +291,7 @@ func Populate(context *project.Context, c *cli.Context) { context.Log = !c.Bool("d") context.NoRecreate = c.Bool("no-recreate") context.ForceRecreate = c.Bool("force-recreate") + context.NoBuild = c.Bool("no-build") } else if c.Command.Name == "stop" || c.Command.Name == "restart" || c.Command.Name == "scale" { context.Timeout = uint(c.Int("timeout")) } else if c.Command.Name == "kill" { diff --git a/docker/builder.go b/docker/builder.go index 8313cffae..70893469f 100644 --- a/docker/builder.go +++ b/docker/builder.go @@ -22,7 +22,7 @@ const DefaultDockerfileName = "Dockerfile" // Builder defines methods to provide a docker builder. This makes libcompose // not tied up to the docker daemon builder. type Builder interface { - Build(p *project.Project, service project.Service) (string, error) + Build(imageName string, p *project.Project, service project.Service) error } // DaemonBuilder is the daemon "docker build" Builder implementation. @@ -39,38 +39,31 @@ func NewDaemonBuilder(context *Context) *DaemonBuilder { // Build implements Builder. It consumes the docker build API endpoint and sends // a tar of the specified service build context. -func (d *DaemonBuilder) Build(p *project.Project, service project.Service) (string, error) { +func (d *DaemonBuilder) Build(imageName string, p *project.Project, service project.Service) error { if service.Config().Build == "" { - return service.Config().Image, nil + return fmt.Errorf("Specified service does not have a build section") } - tag := fmt.Sprintf("%s_%s", p.Name, service.Name()) context, err := CreateTar(p, service.Name()) if err != nil { - return "", err + return err } defer context.Close() client := d.context.ClientFactory.Create(service) - logrus.Infof("Building %s...", tag) + logrus.Infof("Building %s...", imageName) - err = client.BuildImage(dockerclient.BuildImageOptions{ + return client.BuildImage(dockerclient.BuildImageOptions{ InputStream: context, OutputStream: os.Stdout, RawJSONStream: false, - Name: tag, + Name: imageName, RmTmpContainer: true, Dockerfile: service.Config().Dockerfile, NoCache: d.context.NoCache, }) - - if err != nil { - return "", err - } - - return tag, nil } // CreateTar create a build context tar for the specified project and service name. diff --git a/docker/service.go b/docker/service.go index e8989bf38..cc573403b 100644 --- a/docker/service.go +++ b/docker/service.go @@ -2,10 +2,12 @@ package docker import ( "fmt" + "github.com/Sirupsen/logrus" "github.com/docker/docker/pkg/nat" "github.com/docker/libcompose/project" "github.com/docker/libcompose/utils" + "github.com/fsouza/go-dockerclient" ) // Service is a project.Service implementations. @@ -39,14 +41,15 @@ func (s *Service) DependentServices() []project.ServiceRelationship { return project.DefaultDependentServices(s.context.Project, s) } -// Create implements Service.Create. +// Create implements Service.Create. It ensures the image exists or build it +// if it can and then create a container. func (s *Service) Create() error { containers, err := s.collectContainers() if err != nil { return err } - imageName, err := s.build() + imageName, err := s.ensureImageExists() if err != nil { return err } @@ -87,20 +90,57 @@ func (s *Service) createOne(imageName string) (*Container, error) { return containers[0], err } +func (s *Service) ensureImageExists() (string, error) { + err := s.imageExists() + + if err == nil { + return s.imageName(), nil + } + + if err != nil && err != docker.ErrNoSuchImage { + return "", err + } + + if s.Config().Build != "" { + if s.context.NoBuild { + return "", fmt.Errorf("Service %q needs to be built, but no-build was specified", s.name) + } + return s.imageName(), s.build() + } + + return s.imageName(), s.Pull() +} + +func (s *Service) imageExists() error { + client := s.context.ClientFactory.Create(s) + + _, err := client.InspectImage(s.imageName()) + return err +} + +func (s *Service) imageName() string { + if s.Config().Image != "" { + return s.Config().Image + } + return fmt.Sprintf("%s_%s", s.context.ProjectName, s.Name()) +} + // Build implements Service.Build. If an imageName is specified or if the context has // no build to work with it will do nothing. Otherwise it will try to build // the image and returns an error if any. func (s *Service) Build() error { - _, err := s.build() - return err + if s.Config().Image != "" { + return nil + } + return s.build() } -func (s *Service) build() (string, error) { +func (s *Service) build() error { if s.context.Builder == nil { - return s.Config().Image, nil + return fmt.Errorf("Cannot build an image without a builder configured") } - return s.context.Builder.Build(s.context.Project, s) + return s.context.Builder.Build(s.imageName(), s.context.Project, s) } func (s *Service) constructContainers(imageName string, count int) ([]*Container, error) { @@ -145,11 +185,19 @@ func (s *Service) constructContainers(imageName string, count int) ([]*Container // Up implements Service.Up. It builds the image if needed, creates a container // and start it. func (s *Service) Up() error { - imageName, err := s.build() + containers, err := s.collectContainers() if err != nil { return err } + var imageName = s.imageName() + if len(containers) == 0 || !s.context.NoRecreate { + imageName, err = s.ensureImageExists() + if err != nil { + return err + } + } + return s.up(imageName, true) } @@ -310,7 +358,7 @@ func (s *Service) Scale(scale int) error { } if foundCount != scale { - imageName, err := s.build() + imageName, err := s.ensureImageExists() if err != nil { return err } @@ -323,7 +371,8 @@ func (s *Service) Scale(scale int) error { return s.up("", false) } -// Pull implements Service.Pull. It pulls or build the image of the service. +// Pull implements Service.Pull. It pulls the image of the service and skip the service that +// would need to be built. func (s *Service) Pull() error { if s.Config().Image == "" { return nil diff --git a/integration/assets/simple-build/docker-compose.yml b/integration/assets/simple-build/docker-compose.yml new file mode 100644 index 000000000..fdac1e131 --- /dev/null +++ b/integration/assets/simple-build/docker-compose.yml @@ -0,0 +1,2 @@ +one: + build: one diff --git a/integration/assets/simple-build/one/Dockerfile b/integration/assets/simple-build/one/Dockerfile new file mode 100644 index 000000000..59e931397 --- /dev/null +++ b/integration/assets/simple-build/one/Dockerfile @@ -0,0 +1,2 @@ +FROM busybox +CMD ["echo", "one"] diff --git a/integration/build_test.go b/integration/build_test.go index ef9f2f525..28ffb52ae 100644 --- a/integration/build_test.go +++ b/integration/build_test.go @@ -2,7 +2,6 @@ package integration import ( "fmt" - "os" "os/exec" "strings" @@ -12,9 +11,6 @@ import ( func (s *RunSuite) TestBuild(c *C) { p := s.RandomProject() cmd := exec.Command(s.command, "-f", "./assets/build/docker-compose.yml", "-p", p, "build") - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - cmd.Stdin = os.Stdin err := cmd.Run() oneImageName := fmt.Sprintf("%s_one", p) @@ -67,9 +63,6 @@ func (s *RunSuite) TestBuildWithNoCache2(c *C) { func (s *RunSuite) TestBuildWithNoCache3(c *C) { p := s.RandomProject() cmd := exec.Command(s.command, "-f", "./assets/build/docker-compose.yml", "-p", p, "build", "--no-cache") - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - cmd.Stdin = os.Stdin err := cmd.Run() oneImageName := fmt.Sprintf("%s_one", p) diff --git a/integration/up_test.go b/integration/up_test.go index 7fe79cdf5..de211f0b8 100644 --- a/integration/up_test.go +++ b/integration/up_test.go @@ -2,6 +2,7 @@ package integration import ( "fmt" + "os/exec" "path/filepath" "strings" @@ -297,3 +298,24 @@ func (s *RunSuite) TestRelativeVolume(c *C) { c.Assert(len(cn.Mounts), DeepEquals, 1) c.Assert(cn.Mounts[0].Source, DeepEquals, absPath) } + +func (s *RunSuite) TestUpNoBuildFailIfImageNotPresent(c *C) { + p := s.RandomProject() + cmd := exec.Command(s.command, "-f", "./assets/build/docker-compose.yml", "-p", p, "up", "--no-build") + err := cmd.Run() + + c.Assert(err, NotNil) +} + +func (s *RunSuite) TestUpNoBuildShouldWorkIfImageIsPresent(c *C) { + p := s.RandomProject() + cmd := exec.Command(s.command, "-f", "./assets/simple-build/docker-compose.yml", "-p", p, "build") + err := cmd.Run() + + c.Assert(err, IsNil) + + cmd = exec.Command(s.command, "-f", "./assets/simple-build/docker-compose.yml", "-p", p, "up", "-d", "--no-build") + err = cmd.Run() + + c.Assert(err, IsNil) +} diff --git a/project/context.go b/project/context.go index ea0d11a85..6c5304b7f 100644 --- a/project/context.go +++ b/project/context.go @@ -24,6 +24,7 @@ type Context struct { ForceRecreate bool NoRecreate bool NoCache bool + NoBuild bool Signal int ComposeFiles []string ComposeBytes [][]byte