Skip to content
This repository was archived by the owner on Jul 18, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions cli/command/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
},
},
}
}
Expand Down Expand Up @@ -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.",
Expand Down Expand Up @@ -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" {
Expand Down
21 changes: 7 additions & 14 deletions docker/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand Down
69 changes: 59 additions & 10 deletions docker/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -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
}
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions integration/assets/simple-build/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
one:
build: one
2 changes: 2 additions & 0 deletions integration/assets/simple-build/one/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
FROM busybox
CMD ["echo", "one"]
7 changes: 0 additions & 7 deletions integration/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package integration

import (
"fmt"
"os"
"os/exec"
"strings"

Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
22 changes: 22 additions & 0 deletions integration/up_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package integration

import (
"fmt"
"os/exec"
"path/filepath"
"strings"

Expand Down Expand Up @@ -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)
}
1 change: 1 addition & 0 deletions project/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type Context struct {
ForceRecreate bool
NoRecreate bool
NoCache bool
NoBuild bool
Signal int
ComposeFiles []string
ComposeBytes [][]byte
Expand Down