github.com/DaoCloud/dao@v0.0.0-20161212064103-c3dbfd13ee36/api/client/image/build.go (about) 1 package image 2 3 import ( 4 "archive/tar" 5 "bufio" 6 "bytes" 7 "fmt" 8 "io" 9 "os" 10 "path/filepath" 11 "regexp" 12 "runtime" 13 14 "golang.org/x/net/context" 15 16 "github.com/docker/docker/api" 17 "github.com/docker/docker/api/client" 18 "github.com/docker/docker/builder" 19 "github.com/docker/docker/builder/dockerignore" 20 "github.com/docker/docker/cli" 21 "github.com/docker/docker/opts" 22 "github.com/docker/docker/pkg/archive" 23 "github.com/docker/docker/pkg/fileutils" 24 "github.com/docker/docker/pkg/jsonmessage" 25 "github.com/docker/docker/pkg/progress" 26 "github.com/docker/docker/pkg/streamformatter" 27 "github.com/docker/docker/pkg/urlutil" 28 "github.com/docker/docker/reference" 29 runconfigopts "github.com/docker/docker/runconfig/opts" 30 "github.com/docker/engine-api/types" 31 "github.com/docker/engine-api/types/container" 32 "github.com/docker/go-units" 33 "github.com/spf13/cobra" 34 ) 35 36 type buildOptions struct { 37 context string 38 dockerfileName string 39 tags opts.ListOpts 40 labels opts.ListOpts 41 buildArgs opts.ListOpts 42 ulimits *runconfigopts.UlimitOpt 43 memory string 44 memorySwap string 45 shmSize string 46 cpuShares int64 47 cpuPeriod int64 48 cpuQuota int64 49 cpuSetCpus string 50 cpuSetMems string 51 cgroupParent string 52 isolation string 53 quiet bool 54 noCache bool 55 rm bool 56 forceRm bool 57 pull bool 58 } 59 60 // NewBuildCommand creates a new `docker build` command 61 func NewBuildCommand(dockerCli *client.DockerCli) *cobra.Command { 62 ulimits := make(map[string]*units.Ulimit) 63 options := buildOptions{ 64 tags: opts.NewListOpts(validateTag), 65 buildArgs: opts.NewListOpts(runconfigopts.ValidateEnv), 66 ulimits: runconfigopts.NewUlimitOpt(&ulimits), 67 labels: opts.NewListOpts(runconfigopts.ValidateEnv), 68 } 69 70 cmd := &cobra.Command{ 71 Use: "build [OPTIONS] PATH | URL | -", 72 Short: "从一个Dockerfile构建新的镜像", 73 Args: cli.ExactArgs(1), 74 RunE: func(cmd *cobra.Command, args []string) error { 75 options.context = args[0] 76 return runBuild(dockerCli, options) 77 }, 78 } 79 80 flags := cmd.Flags() 81 82 flags.VarP(&options.tags, "tag", "t", "镜像名称以及可选的标签,若指定标签,格式为:'名称:标签' ") 83 flags.Var(&options.buildArgs, "build-arg", "设置构建时的环境变量") 84 flags.Var(options.ulimits, "ulimit", "设置Ulimit参数") 85 flags.StringVarP(&options.dockerfileName, "file", "f", "", "Dockerfile的名称(默认为当前目录下的Dockerfile文件路径)") 86 flags.StringVarP(&options.memory, "memory", "m", "", "内存限制") 87 flags.StringVar(&options.memorySwap, "memory-swap", "", "交换内存限制 等于 实际内存 + 交换区内存: '-1' 代表启用不受限的交换区内存") 88 flags.StringVar(&options.shmSize, "shm-size", "", "共享内存/dev/shm 的大小, 默认值是64MB") 89 flags.Int64VarP(&options.cpuShares, "cpu-shares", "c", 0, "CPU计算资源的值(相对值)") 90 flags.Int64Var(&options.cpuPeriod, "cpu-period", 0, "限制CPU绝对公平调度算法(CFS)的时间周期") 91 flags.Int64Var(&options.cpuQuota, "cpu-quota", 0, "限制CPU绝对公平调度算法(CFS)的时间限额") 92 flags.StringVar(&options.cpuSetCpus, "cpuset-cpus", "", "允许容器执行的CPU核指定(0-3,0,1): 0-3代表运行运行在0,1,2,3这4个核上") 93 flags.StringVar(&options.cpuSetMems, "cpuset-mems", "", "允许容器执行的CPU内存所在核指定(0-3,0,1): 0-3代表运行运行在0,1,2,3这4个核上") 94 flags.StringVar(&options.cgroupParent, "cgroup-parent", "", "容器的可选cgroup父路径") 95 flags.StringVar(&options.isolation, "isolation", "", "设置容器的隔离策略") 96 flags.Var(&options.labels, "label", "为一个镜像设置元数据") 97 flags.BoolVar(&options.noCache, "no-cache", false, "构建镜像时不使用镜像缓存") 98 flags.BoolVar(&options.rm, "rm", true, "成狗构建后删除中间容器") 99 flags.BoolVar(&options.forceRm, "force-rm", false, "总是产出中间结果的容器") 100 flags.BoolVarP(&options.quiet, "quiet", "q", false, "压缩构建输出,并在构建成功时打印镜像ID") 101 flags.BoolVar(&options.pull, "pull", false, "总是尝试下拉最新版本的镜像") 102 103 client.AddTrustedFlags(flags, true) 104 105 return cmd 106 } 107 108 func runBuild(dockerCli *client.DockerCli, options buildOptions) error { 109 110 var ( 111 buildCtx io.ReadCloser 112 err error 113 ) 114 115 specifiedContext := options.context 116 117 var ( 118 contextDir string 119 tempDir string 120 relDockerfile string 121 progBuff io.Writer 122 buildBuff io.Writer 123 ) 124 125 progBuff = dockerCli.Out() 126 buildBuff = dockerCli.Out() 127 if options.quiet { 128 progBuff = bytes.NewBuffer(nil) 129 buildBuff = bytes.NewBuffer(nil) 130 } 131 132 switch { 133 case specifiedContext == "-": 134 buildCtx, relDockerfile, err = builder.GetContextFromReader(dockerCli.In(), options.dockerfileName) 135 case urlutil.IsGitURL(specifiedContext): 136 tempDir, relDockerfile, err = builder.GetContextFromGitURL(specifiedContext, options.dockerfileName) 137 case urlutil.IsURL(specifiedContext): 138 buildCtx, relDockerfile, err = builder.GetContextFromURL(progBuff, specifiedContext, options.dockerfileName) 139 default: 140 contextDir, relDockerfile, err = builder.GetContextFromLocalDir(specifiedContext, options.dockerfileName) 141 } 142 143 if err != nil { 144 if options.quiet && urlutil.IsURL(specifiedContext) { 145 fmt.Fprintln(dockerCli.Err(), progBuff) 146 } 147 return fmt.Errorf("准备构建上下文失败: %s", err) 148 } 149 150 if tempDir != "" { 151 defer os.RemoveAll(tempDir) 152 contextDir = tempDir 153 } 154 155 if buildCtx == nil { 156 // And canonicalize dockerfile name to a platform-independent one 157 relDockerfile, err = archive.CanonicalTarNameForPath(relDockerfile) 158 if err != nil { 159 return fmt.Errorf("不能规范Dockerfile路径 %s: %v", relDockerfile, err) 160 } 161 162 f, err := os.Open(filepath.Join(contextDir, ".dockerignore")) 163 if err != nil && !os.IsNotExist(err) { 164 return err 165 } 166 167 var excludes []string 168 if err == nil { 169 excludes, err = dockerignore.ReadAll(f) 170 if err != nil { 171 return err 172 } 173 } 174 175 if err := builder.ValidateContextDirectory(contextDir, excludes); err != nil { 176 return fmt.Errorf("检验构建上下文出错: '%s'.", err) 177 } 178 179 // If .dockerignore mentions .dockerignore or the Dockerfile 180 // then make sure we send both files over to the daemon 181 // because Dockerfile is, obviously, needed no matter what, and 182 // .dockerignore is needed to know if either one needs to be 183 // removed. The daemon will remove them for us, if needed, after it 184 // parses the Dockerfile. Ignore errors here, as they will have been 185 // caught by validateContextDirectory above. 186 var includes = []string{"."} 187 keepThem1, _ := fileutils.Matches(".dockerignore", excludes) 188 keepThem2, _ := fileutils.Matches(relDockerfile, excludes) 189 if keepThem1 || keepThem2 { 190 includes = append(includes, ".dockerignore", relDockerfile) 191 } 192 193 buildCtx, err = archive.TarWithOptions(contextDir, &archive.TarOptions{ 194 Compression: archive.Uncompressed, 195 ExcludePatterns: excludes, 196 IncludeFiles: includes, 197 }) 198 if err != nil { 199 return err 200 } 201 } 202 203 ctx := context.Background() 204 205 var resolvedTags []*resolvedTag 206 if client.IsTrusted() { 207 // Wrap the tar archive to replace the Dockerfile entry with the rewritten 208 // Dockerfile which uses trusted pulls. 209 buildCtx = replaceDockerfileTarWrapper(ctx, buildCtx, relDockerfile, dockerCli.TrustedReference, &resolvedTags) 210 } 211 212 // Setup an upload progress bar 213 progressOutput := streamformatter.NewStreamFormatter().NewProgressOutput(progBuff, true) 214 215 var body io.Reader = progress.NewProgressReader(buildCtx, progressOutput, 0, "", "Sending build context to Docker daemon") 216 217 var memory int64 218 if options.memory != "" { 219 parsedMemory, err := units.RAMInBytes(options.memory) 220 if err != nil { 221 return err 222 } 223 memory = parsedMemory 224 } 225 226 var memorySwap int64 227 if options.memorySwap != "" { 228 if options.memorySwap == "-1" { 229 memorySwap = -1 230 } else { 231 parsedMemorySwap, err := units.RAMInBytes(options.memorySwap) 232 if err != nil { 233 return err 234 } 235 memorySwap = parsedMemorySwap 236 } 237 } 238 239 var shmSize int64 240 if options.shmSize != "" { 241 shmSize, err = units.RAMInBytes(options.shmSize) 242 if err != nil { 243 return err 244 } 245 } 246 247 buildOptions := types.ImageBuildOptions{ 248 Memory: memory, 249 MemorySwap: memorySwap, 250 Tags: options.tags.GetAll(), 251 SuppressOutput: options.quiet, 252 NoCache: options.noCache, 253 Remove: options.rm, 254 ForceRemove: options.forceRm, 255 PullParent: options.pull, 256 Isolation: container.Isolation(options.isolation), 257 CPUSetCPUs: options.cpuSetCpus, 258 CPUSetMems: options.cpuSetMems, 259 CPUShares: options.cpuShares, 260 CPUQuota: options.cpuQuota, 261 CPUPeriod: options.cpuPeriod, 262 CgroupParent: options.cgroupParent, 263 Dockerfile: relDockerfile, 264 ShmSize: shmSize, 265 Ulimits: options.ulimits.GetList(), 266 BuildArgs: runconfigopts.ConvertKVStringsToMap(options.buildArgs.GetAll()), 267 AuthConfigs: dockerCli.RetrieveAuthConfigs(), 268 Labels: runconfigopts.ConvertKVStringsToMap(options.labels.GetAll()), 269 } 270 271 response, err := dockerCli.Client().ImageBuild(ctx, body, buildOptions) 272 if err != nil { 273 return err 274 } 275 defer response.Body.Close() 276 277 err = jsonmessage.DisplayJSONMessagesStream(response.Body, buildBuff, dockerCli.OutFd(), dockerCli.IsTerminalOut(), nil) 278 if err != nil { 279 if jerr, ok := err.(*jsonmessage.JSONError); ok { 280 // If no error code is set, default to 1 281 if jerr.Code == 0 { 282 jerr.Code = 1 283 } 284 if options.quiet { 285 fmt.Fprintf(dockerCli.Err(), "%s%s", progBuff, buildBuff) 286 } 287 return cli.StatusError{Status: jerr.Message, StatusCode: jerr.Code} 288 } 289 } 290 291 // Windows: show error message about modified file permissions if the 292 // daemon isn't running Windows. 293 if response.OSType != "windows" && runtime.GOOS == "windows" { 294 fmt.Fprintln(dockerCli.Err(), `安全警告: 您在一个非Windows的Docker引擎上构建Windows机器的Docker的镜像。所有添加到构建上下文的文件和目录将添加'-rwxr-xr-x'权限。推荐您为敏感的文件和目录进行权限的重复查验。`) 295 } 296 297 // Everything worked so if -q was provided the output from the daemon 298 // should be just the image ID and we'll print that to stdout. 299 if options.quiet { 300 fmt.Fprintf(dockerCli.Out(), "%s", buildBuff) 301 } 302 303 if client.IsTrusted() { 304 // Since the build was successful, now we must tag any of the resolved 305 // images from the above Dockerfile rewrite. 306 for _, resolved := range resolvedTags { 307 if err := dockerCli.TagTrusted(ctx, resolved.digestRef, resolved.tagRef); err != nil { 308 return err 309 } 310 } 311 } 312 313 return nil 314 } 315 316 type translatorFunc func(context.Context, reference.NamedTagged) (reference.Canonical, error) 317 318 // validateTag checks if the given image name can be resolved. 319 func validateTag(rawRepo string) (string, error) { 320 _, err := reference.ParseNamed(rawRepo) 321 if err != nil { 322 return "", err 323 } 324 325 return rawRepo, nil 326 } 327 328 var dockerfileFromLinePattern = regexp.MustCompile(`(?i)^[\s]*FROM[ \f\r\t\v]+(?P<image>[^ \f\r\t\v\n#]+)`) 329 330 // resolvedTag records the repository, tag, and resolved digest reference 331 // from a Dockerfile rewrite. 332 type resolvedTag struct { 333 digestRef reference.Canonical 334 tagRef reference.NamedTagged 335 } 336 337 // rewriteDockerfileFrom rewrites the given Dockerfile by resolving images in 338 // "FROM <image>" instructions to a digest reference. `translator` is a 339 // function that takes a repository name and tag reference and returns a 340 // trusted digest reference. 341 func rewriteDockerfileFrom(ctx context.Context, dockerfile io.Reader, translator translatorFunc) (newDockerfile []byte, resolvedTags []*resolvedTag, err error) { 342 scanner := bufio.NewScanner(dockerfile) 343 buf := bytes.NewBuffer(nil) 344 345 // Scan the lines of the Dockerfile, looking for a "FROM" line. 346 for scanner.Scan() { 347 line := scanner.Text() 348 349 matches := dockerfileFromLinePattern.FindStringSubmatch(line) 350 if matches != nil && matches[1] != api.NoBaseImageSpecifier { 351 // Replace the line with a resolved "FROM repo@digest" 352 ref, err := reference.ParseNamed(matches[1]) 353 if err != nil { 354 return nil, nil, err 355 } 356 ref = reference.WithDefaultTag(ref) 357 if ref, ok := ref.(reference.NamedTagged); ok && client.IsTrusted() { 358 trustedRef, err := translator(ctx, ref) 359 if err != nil { 360 return nil, nil, err 361 } 362 363 line = dockerfileFromLinePattern.ReplaceAllLiteralString(line, fmt.Sprintf("FROM %s", trustedRef.String())) 364 resolvedTags = append(resolvedTags, &resolvedTag{ 365 digestRef: trustedRef, 366 tagRef: ref, 367 }) 368 } 369 } 370 371 _, err := fmt.Fprintln(buf, line) 372 if err != nil { 373 return nil, nil, err 374 } 375 } 376 377 return buf.Bytes(), resolvedTags, scanner.Err() 378 } 379 380 // replaceDockerfileTarWrapper wraps the given input tar archive stream and 381 // replaces the entry with the given Dockerfile name with the contents of the 382 // new Dockerfile. Returns a new tar archive stream with the replaced 383 // Dockerfile. 384 func replaceDockerfileTarWrapper(ctx context.Context, inputTarStream io.ReadCloser, dockerfileName string, translator translatorFunc, resolvedTags *[]*resolvedTag) io.ReadCloser { 385 pipeReader, pipeWriter := io.Pipe() 386 go func() { 387 tarReader := tar.NewReader(inputTarStream) 388 tarWriter := tar.NewWriter(pipeWriter) 389 390 defer inputTarStream.Close() 391 392 for { 393 hdr, err := tarReader.Next() 394 if err == io.EOF { 395 // Signals end of archive. 396 tarWriter.Close() 397 pipeWriter.Close() 398 return 399 } 400 if err != nil { 401 pipeWriter.CloseWithError(err) 402 return 403 } 404 405 var content io.Reader = tarReader 406 if hdr.Name == dockerfileName { 407 // This entry is the Dockerfile. Since the tar archive was 408 // generated from a directory on the local filesystem, the 409 // Dockerfile will only appear once in the archive. 410 var newDockerfile []byte 411 newDockerfile, *resolvedTags, err = rewriteDockerfileFrom(ctx, content, translator) 412 if err != nil { 413 pipeWriter.CloseWithError(err) 414 return 415 } 416 hdr.Size = int64(len(newDockerfile)) 417 content = bytes.NewBuffer(newDockerfile) 418 } 419 420 if err := tarWriter.WriteHeader(hdr); err != nil { 421 pipeWriter.CloseWithError(err) 422 return 423 } 424 425 if _, err := io.Copy(tarWriter, content); err != nil { 426 pipeWriter.CloseWithError(err) 427 return 428 } 429 } 430 }() 431 432 return pipeReader 433 }