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 }