github.com/dnephin/dobi@v0.15.0/tasks/image/build.go (about)

     1  package image
     2  
     3  import (
     4  	"io"
     5  	"io/ioutil"
     6  	"os"
     7  	"path/filepath"
     8  	"strings"
     9  
    10  	"github.com/dnephin/dobi/config"
    11  	"github.com/dnephin/dobi/tasks/context"
    12  	"github.com/dnephin/dobi/utils/fs"
    13  	"github.com/docker/cli/cli/command/image/build"
    14  	"github.com/docker/docker/pkg/archive"
    15  	docker "github.com/fsouza/go-dockerclient"
    16  	"github.com/pkg/errors"
    17  )
    18  
    19  // RunBuild builds an image if it is out of date
    20  func RunBuild(ctx *context.ExecuteContext, t *Task, hasModifiedDeps bool) (bool, error) {
    21  	if !hasModifiedDeps {
    22  		stale, err := buildIsStale(ctx, t)
    23  		switch {
    24  		case err != nil:
    25  			return false, err
    26  		case !stale:
    27  			t.logger().Info("is fresh")
    28  			return false, nil
    29  		}
    30  	}
    31  	t.logger().Debug("is stale")
    32  
    33  	if !t.config.IsBuildable() {
    34  		return false, errors.Errorf(
    35  			"%s is not buildable, missing required fields", t.name.Resource())
    36  	}
    37  
    38  	if err := buildImage(ctx, t); err != nil {
    39  		return false, err
    40  	}
    41  
    42  	image, err := GetImage(ctx, t.config)
    43  	if err != nil {
    44  		return false, err
    45  	}
    46  
    47  	record := imageModifiedRecord{ImageID: image.ID}
    48  	if err := updateImageRecord(recordPath(ctx, t.config), record); err != nil {
    49  		t.logger().Warnf("Failed to update image record: %s", err)
    50  	}
    51  	t.logger().Info("Created")
    52  	return true, nil
    53  }
    54  
    55  // TODO: this cyclo problem should be fixed
    56  // nolint: gocyclo
    57  func buildIsStale(ctx *context.ExecuteContext, t *Task) (bool, error) {
    58  	image, err := GetImage(ctx, t.config)
    59  	switch err {
    60  	case docker.ErrNoSuchImage:
    61  		t.logger().Debug("Image does not exist")
    62  		return true, nil
    63  	case nil:
    64  	default:
    65  		return true, err
    66  	}
    67  
    68  	paths := []string{t.config.Context}
    69  	// TODO: polymorphic config for different types of images
    70  	if t.config.Steps != "" && ctx.ConfigFile != "" {
    71  		paths = append(paths, ctx.ConfigFile)
    72  	}
    73  
    74  	excludes, err := build.ReadDockerignore(t.config.Context)
    75  	if err != nil {
    76  		t.logger().Warnf("Failed to read .dockerignore file.")
    77  	}
    78  	excludes = append(excludes, ".dobi")
    79  
    80  	mtime, err := fs.LastModified(&fs.LastModifiedSearch{
    81  		Root:     absPath(ctx.WorkingDir, t.config.Context),
    82  		Excludes: excludes,
    83  		Paths:    paths,
    84  	})
    85  	if err != nil {
    86  		t.logger().Warnf("Failed to get last modified time of context.")
    87  		return true, err
    88  	}
    89  
    90  	record, err := getImageRecord(recordPath(ctx, t.config))
    91  	if err != nil {
    92  		t.logger().Warnf("Failed to get image record: %s", err)
    93  		if image.Created.Before(mtime) {
    94  			t.logger().Debug("Image older than context")
    95  			return true, nil
    96  		}
    97  		return false, nil
    98  	}
    99  
   100  	if image.ID != record.ImageID || record.Info.ModTime().Before(mtime) {
   101  		t.logger().Debug("Image record older than context")
   102  		return true, nil
   103  	}
   104  	return false, nil
   105  }
   106  
   107  func absPath(path string, wd string) string {
   108  	if filepath.IsAbs(path) {
   109  		return filepath.Clean(path)
   110  	}
   111  	return filepath.Join(wd, path)
   112  }
   113  
   114  func buildImage(ctx *context.ExecuteContext, t *Task) error {
   115  	var err error
   116  	if t.config.Steps != "" {
   117  		err = t.buildImageFromSteps(ctx)
   118  	} else {
   119  		err = t.buildImageFromDockerfile(ctx)
   120  	}
   121  	if err != nil {
   122  		return err
   123  	}
   124  	image, err := GetImage(ctx, t.config)
   125  	if err != nil {
   126  		return err
   127  	}
   128  	record := imageModifiedRecord{ImageID: image.ID}
   129  	return updateImageRecord(recordPath(ctx, t.config), record)
   130  }
   131  
   132  func (t *Task) buildImageFromDockerfile(ctx *context.ExecuteContext) error {
   133  	return Stream(os.Stdout, func(out io.Writer) error {
   134  		opts := t.commonBuildImageOptions(ctx, out)
   135  		opts.Dockerfile = t.config.Dockerfile
   136  		opts.ContextDir = t.config.Context
   137  		return ctx.Client.BuildImage(opts)
   138  	})
   139  }
   140  
   141  func (t *Task) commonBuildImageOptions(
   142  	ctx *context.ExecuteContext,
   143  	out io.Writer,
   144  ) docker.BuildImageOptions {
   145  	return docker.BuildImageOptions{
   146  		Name:           GetImageName(ctx, t.config),
   147  		BuildArgs:      buildArgs(t.config.Args),
   148  		Target:         t.config.Target,
   149  		Pull:           t.config.PullBaseImageOnBuild,
   150  		NetworkMode:    t.config.NetworkMode,
   151  		CacheFrom:      t.config.CacheFrom,
   152  		RmTmpContainer: true,
   153  		OutputStream:   out,
   154  		RawJSONStream:  true,
   155  		SuppressOutput: ctx.Settings.Quiet,
   156  		AuthConfigs:    ctx.GetAuthConfigs(),
   157  	}
   158  }
   159  
   160  func buildArgs(args map[string]string) []docker.BuildArg {
   161  	out := []docker.BuildArg{}
   162  	for key, value := range args {
   163  		out = append(out, docker.BuildArg{Name: key, Value: value})
   164  	}
   165  	return out
   166  }
   167  
   168  func (t *Task) buildImageFromSteps(ctx *context.ExecuteContext) error {
   169  	buildContext, dockerfile, err := getBuildContext(t.config)
   170  	if err != nil {
   171  		return err
   172  	}
   173  	return Stream(os.Stdout, func(out io.Writer) error {
   174  		opts := t.commonBuildImageOptions(ctx, out)
   175  		opts.InputStream = buildContext
   176  		opts.Dockerfile = dockerfile
   177  		return ctx.Client.BuildImage(opts)
   178  	})
   179  }
   180  
   181  func getBuildContext(config *config.ImageConfig) (io.Reader, string, error) {
   182  	contextDir := config.Context
   183  	excludes, err := build.ReadDockerignore(contextDir)
   184  	if err != nil {
   185  		return nil, "", err
   186  	}
   187  	if err = build.ValidateContextDirectory(contextDir, excludes); err != nil {
   188  		return nil, "", err
   189  
   190  	}
   191  	buildCtx, err := archive.TarWithOptions(contextDir, &archive.TarOptions{
   192  		ExcludePatterns: excludes,
   193  	})
   194  	if err != nil {
   195  		return nil, "", err
   196  	}
   197  	dockerfileCtx := ioutil.NopCloser(strings.NewReader(config.Steps))
   198  	return build.AddDockerfileToBuildContext(dockerfileCtx, buildCtx)
   199  }