github.com/bdwilliams/libcompose@v0.3.1-0.20160826154243-d81a9bdacff0/docker/builder/builder.go (about)

     1  package builder
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"os"
     7  	"path"
     8  	"path/filepath"
     9  	"strings"
    10  
    11  	"golang.org/x/net/context"
    12  
    13  	"github.com/Sirupsen/logrus"
    14  	"github.com/docker/docker/builder"
    15  	"github.com/docker/docker/builder/dockerignore"
    16  	"github.com/docker/docker/pkg/archive"
    17  	"github.com/docker/docker/pkg/fileutils"
    18  	"github.com/docker/docker/pkg/jsonmessage"
    19  	"github.com/docker/docker/pkg/progress"
    20  	"github.com/docker/docker/pkg/streamformatter"
    21  	"github.com/docker/docker/pkg/term"
    22  	"github.com/docker/engine-api/client"
    23  	"github.com/docker/engine-api/types"
    24  	"github.com/docker/libcompose/logger"
    25  )
    26  
    27  // DefaultDockerfileName is the default name of a Dockerfile
    28  const DefaultDockerfileName = "Dockerfile"
    29  
    30  // Builder defines methods to provide a docker builder. This makes libcompose
    31  // not tied up to the docker daemon builder.
    32  type Builder interface {
    33  	Build(imageName string) error
    34  }
    35  
    36  // DaemonBuilder is the daemon "docker build" Builder implementation.
    37  type DaemonBuilder struct {
    38  	Client           client.ImageAPIClient
    39  	ContextDirectory string
    40  	Dockerfile       string
    41  	AuthConfigs      map[string]types.AuthConfig
    42  	NoCache          bool
    43  	ForceRemove      bool
    44  	Pull             bool
    45  	BuildArgs        map[string]string
    46  	LoggerFactory    logger.Factory
    47  }
    48  
    49  // Build implements Builder. It consumes the docker build API endpoint and sends
    50  // a tar of the specified service build context.
    51  func (d *DaemonBuilder) Build(ctx context.Context, imageName string) error {
    52  	buildCtx, err := CreateTar(d.ContextDirectory, d.Dockerfile)
    53  	if err != nil {
    54  		return err
    55  	}
    56  	defer buildCtx.Close()
    57  	if d.LoggerFactory == nil {
    58  		d.LoggerFactory = &logger.NullLogger{}
    59  	}
    60  
    61  	l := d.LoggerFactory.CreateBuildLogger(imageName)
    62  
    63  	progBuff := &logger.Wrapper{
    64  		Err:    false,
    65  		Logger: l,
    66  	}
    67  
    68  	buildBuff := &logger.Wrapper{
    69  		Err:    false,
    70  		Logger: l,
    71  	}
    72  
    73  	errBuff := &logger.Wrapper{
    74  		Err:    true,
    75  		Logger: l,
    76  	}
    77  
    78  	// Setup an upload progress bar
    79  	progressOutput := streamformatter.NewStreamFormatter().NewProgressOutput(progBuff, true)
    80  
    81  	var body io.Reader = progress.NewProgressReader(buildCtx, progressOutput, 0, "", "Sending build context to Docker daemon")
    82  
    83  	logrus.Infof("Building %s...", imageName)
    84  
    85  	outFd, isTerminalOut := term.GetFdInfo(os.Stdout)
    86  	w := l.OutWriter()
    87  	if w != nil {
    88  		outFd, isTerminalOut = term.GetFdInfo(w)
    89  	}
    90  
    91  	response, err := d.Client.ImageBuild(ctx, body, types.ImageBuildOptions{
    92  		Tags:        []string{imageName},
    93  		NoCache:     d.NoCache,
    94  		Remove:      true,
    95  		ForceRemove: d.ForceRemove,
    96  		PullParent:  d.Pull,
    97  		Dockerfile:  d.Dockerfile,
    98  		AuthConfigs: d.AuthConfigs,
    99  		BuildArgs:   d.BuildArgs,
   100  	})
   101  	if err != nil {
   102  		return err
   103  	}
   104  
   105  	err = jsonmessage.DisplayJSONMessagesStream(response.Body, buildBuff, outFd, isTerminalOut, nil)
   106  	if err != nil {
   107  		if jerr, ok := err.(*jsonmessage.JSONError); ok {
   108  			// If no error code is set, default to 1
   109  			if jerr.Code == 0 {
   110  				jerr.Code = 1
   111  			}
   112  			errBuff.Write([]byte(jerr.Error()))
   113  			return fmt.Errorf("Status: %s, Code: %d", jerr.Message, jerr.Code)
   114  		}
   115  	}
   116  	return err
   117  }
   118  
   119  // CreateTar create a build context tar for the specified project and service name.
   120  func CreateTar(contextDirectory, dockerfile string) (io.ReadCloser, error) {
   121  	// This code was ripped off from docker/api/client/build.go
   122  	dockerfileName := filepath.Join(contextDirectory, dockerfile)
   123  
   124  	absContextDirectory, err := filepath.Abs(contextDirectory)
   125  	if err != nil {
   126  		return nil, err
   127  	}
   128  
   129  	filename := dockerfileName
   130  
   131  	if dockerfile == "" {
   132  		// No -f/--file was specified so use the default
   133  		dockerfileName = DefaultDockerfileName
   134  		filename = filepath.Join(absContextDirectory, dockerfileName)
   135  
   136  		// Just to be nice ;-) look for 'dockerfile' too but only
   137  		// use it if we found it, otherwise ignore this check
   138  		if _, err = os.Lstat(filename); os.IsNotExist(err) {
   139  			tmpFN := path.Join(absContextDirectory, strings.ToLower(dockerfileName))
   140  			if _, err = os.Lstat(tmpFN); err == nil {
   141  				dockerfileName = strings.ToLower(dockerfileName)
   142  				filename = tmpFN
   143  			}
   144  		}
   145  	}
   146  
   147  	origDockerfile := dockerfileName // used for error msg
   148  	if filename, err = filepath.Abs(filename); err != nil {
   149  		return nil, err
   150  	}
   151  
   152  	// Now reset the dockerfileName to be relative to the build context
   153  	dockerfileName, err = filepath.Rel(absContextDirectory, filename)
   154  	if err != nil {
   155  		return nil, err
   156  	}
   157  
   158  	// And canonicalize dockerfile name to a platform-independent one
   159  	dockerfileName, err = archive.CanonicalTarNameForPath(dockerfileName)
   160  	if err != nil {
   161  		return nil, fmt.Errorf("Cannot canonicalize dockerfile path %s: %v", dockerfileName, err)
   162  	}
   163  
   164  	if _, err = os.Lstat(filename); os.IsNotExist(err) {
   165  		return nil, fmt.Errorf("Cannot locate Dockerfile: %s", origDockerfile)
   166  	}
   167  	var includes = []string{"."}
   168  	var excludes []string
   169  
   170  	dockerIgnorePath := path.Join(contextDirectory, ".dockerignore")
   171  	dockerIgnore, err := os.Open(dockerIgnorePath)
   172  	if err != nil {
   173  		if !os.IsNotExist(err) {
   174  			return nil, err
   175  		}
   176  		logrus.Warnf("Error while reading .dockerignore (%s) : %s", dockerIgnorePath, err.Error())
   177  		excludes = make([]string, 0)
   178  	} else {
   179  		excludes, err = dockerignore.ReadAll(dockerIgnore)
   180  		if err != nil {
   181  			return nil, err
   182  		}
   183  	}
   184  
   185  	// If .dockerignore mentions .dockerignore or the Dockerfile
   186  	// then make sure we send both files over to the daemon
   187  	// because Dockerfile is, obviously, needed no matter what, and
   188  	// .dockerignore is needed to know if either one needs to be
   189  	// removed.  The deamon will remove them for us, if needed, after it
   190  	// parses the Dockerfile.
   191  	keepThem1, _ := fileutils.Matches(".dockerignore", excludes)
   192  	keepThem2, _ := fileutils.Matches(dockerfileName, excludes)
   193  	if keepThem1 || keepThem2 {
   194  		includes = append(includes, ".dockerignore", dockerfileName)
   195  	}
   196  
   197  	if err := builder.ValidateContextDirectory(contextDirectory, excludes); err != nil {
   198  		return nil, fmt.Errorf("Error checking context is accessible: '%s'. Please check permissions and try again.", err)
   199  	}
   200  
   201  	options := &archive.TarOptions{
   202  		Compression:     archive.Uncompressed,
   203  		ExcludePatterns: excludes,
   204  		IncludeFiles:    includes,
   205  	}
   206  
   207  	return archive.TarWithOptions(contextDirectory, options)
   208  }