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