mirror of
https://github.com/moby/moby.git
synced 2022-11-09 12:21:53 -05:00
Merge pull request #32406 from ijc25/docker-build-iidfile
Add `docker build --iidfile=FILE`
This commit is contained in:
commit
d624f9a7b0
11 changed files with 155 additions and 6 deletions
|
@ -132,7 +132,10 @@ func newImageBuildOptions(ctx context.Context, r *http.Request) (*types.ImageBui
|
||||||
}
|
}
|
||||||
|
|
||||||
func (br *buildRouter) postBuild(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
func (br *buildRouter) postBuild(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
||||||
var notVerboseBuffer = bytes.NewBuffer(nil)
|
var (
|
||||||
|
notVerboseBuffer = bytes.NewBuffer(nil)
|
||||||
|
version = httputils.VersionFromContext(ctx)
|
||||||
|
)
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
@ -177,10 +180,12 @@ func (br *buildRouter) postBuild(ctx context.Context, w http.ResponseWriter, r *
|
||||||
return progress.NewProgressReader(in, progressOutput, r.ContentLength, "Downloading context", buildOptions.RemoteContext)
|
return progress.NewProgressReader(in, progressOutput, r.ContentLength, "Downloading context", buildOptions.RemoteContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
wantAux := versions.GreaterThanOrEqualTo(version, "1.30")
|
||||||
|
|
||||||
imgID, err := br.backend.Build(ctx, backend.BuildConfig{
|
imgID, err := br.backend.Build(ctx, backend.BuildConfig{
|
||||||
Source: r.Body,
|
Source: r.Body,
|
||||||
Options: buildOptions,
|
Options: buildOptions,
|
||||||
ProgressWriter: buildProgressWriter(out, createProgressReader),
|
ProgressWriter: buildProgressWriter(out, wantAux, createProgressReader),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errf(err)
|
return errf(err)
|
||||||
|
@ -221,13 +226,19 @@ func (s *syncWriter) Write(b []byte) (count int, err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildProgressWriter(out io.Writer, createProgressReader func(io.ReadCloser) io.ReadCloser) backend.ProgressWriter {
|
func buildProgressWriter(out io.Writer, wantAux bool, createProgressReader func(io.ReadCloser) io.ReadCloser) backend.ProgressWriter {
|
||||||
out = &syncWriter{w: out}
|
out = &syncWriter{w: out}
|
||||||
|
|
||||||
|
var aux *streamformatter.AuxFormatter
|
||||||
|
if wantAux {
|
||||||
|
aux = &streamformatter.AuxFormatter{Writer: out}
|
||||||
|
}
|
||||||
|
|
||||||
return backend.ProgressWriter{
|
return backend.ProgressWriter{
|
||||||
Output: out,
|
Output: out,
|
||||||
StdoutFormatter: streamformatter.NewStdoutWriter(out),
|
StdoutFormatter: streamformatter.NewStdoutWriter(out),
|
||||||
StderrFormatter: streamformatter.NewStderrWriter(out),
|
StderrFormatter: streamformatter.NewStderrWriter(out),
|
||||||
|
AuxFormatter: aux,
|
||||||
ProgressReaderFunc: createProgressReader,
|
ProgressReaderFunc: createProgressReader,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/pkg/streamformatter"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ProgressWriter is a data object to transport progress streams to the client
|
// ProgressWriter is a data object to transport progress streams to the client
|
||||||
|
@ -11,6 +12,7 @@ type ProgressWriter struct {
|
||||||
Output io.Writer
|
Output io.Writer
|
||||||
StdoutFormatter io.Writer
|
StdoutFormatter io.Writer
|
||||||
StderrFormatter io.Writer
|
StderrFormatter io.Writer
|
||||||
|
AuxFormatter *streamformatter.AuxFormatter
|
||||||
ProgressReaderFunc func(io.ReadCloser) io.ReadCloser
|
ProgressReaderFunc func(io.ReadCloser) io.ReadCloser
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -530,3 +530,8 @@ type PushResult struct {
|
||||||
Digest string
|
Digest string
|
||||||
Size int
|
Size int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BuildResult contains the image id of a successful build
|
||||||
|
type BuildResult struct {
|
||||||
|
ID string
|
||||||
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ import (
|
||||||
"github.com/docker/docker/builder/dockerfile/command"
|
"github.com/docker/docker/builder/dockerfile/command"
|
||||||
"github.com/docker/docker/builder/dockerfile/parser"
|
"github.com/docker/docker/builder/dockerfile/parser"
|
||||||
"github.com/docker/docker/builder/remotecontext"
|
"github.com/docker/docker/builder/remotecontext"
|
||||||
|
"github.com/docker/docker/pkg/streamformatter"
|
||||||
"github.com/docker/docker/pkg/stringid"
|
"github.com/docker/docker/pkg/stringid"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"golang.org/x/net/context"
|
"golang.org/x/net/context"
|
||||||
|
@ -89,6 +90,7 @@ type Builder struct {
|
||||||
|
|
||||||
Stdout io.Writer
|
Stdout io.Writer
|
||||||
Stderr io.Writer
|
Stderr io.Writer
|
||||||
|
Aux *streamformatter.AuxFormatter
|
||||||
Output io.Writer
|
Output io.Writer
|
||||||
|
|
||||||
docker builder.Backend
|
docker builder.Backend
|
||||||
|
@ -114,6 +116,7 @@ func newBuilder(clientCtx context.Context, options builderOptions) *Builder {
|
||||||
options: config,
|
options: config,
|
||||||
Stdout: options.ProgressWriter.StdoutFormatter,
|
Stdout: options.ProgressWriter.StdoutFormatter,
|
||||||
Stderr: options.ProgressWriter.StderrFormatter,
|
Stderr: options.ProgressWriter.StderrFormatter,
|
||||||
|
Aux: options.ProgressWriter.AuxFormatter,
|
||||||
Output: options.ProgressWriter.Output,
|
Output: options.ProgressWriter.Output,
|
||||||
docker: options.Backend,
|
docker: options.Backend,
|
||||||
tmpContainers: map[string]struct{}{},
|
tmpContainers: map[string]struct{}{},
|
||||||
|
@ -161,6 +164,13 @@ func (b *Builder) build(source builder.Source, dockerfile *parser.Result) (*buil
|
||||||
return &builder.Result{ImageID: dispatchState.imageID, FromImage: dispatchState.baseImage}, nil
|
return &builder.Result{ImageID: dispatchState.imageID, FromImage: dispatchState.baseImage}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func emitImageID(aux *streamformatter.AuxFormatter, state *dispatchState) error {
|
||||||
|
if aux == nil || state.imageID == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return aux.Emit(types.BuildResult{ID: state.imageID})
|
||||||
|
}
|
||||||
|
|
||||||
func (b *Builder) dispatchDockerfileWithCancellation(dockerfile *parser.Result) (*dispatchState, error) {
|
func (b *Builder) dispatchDockerfileWithCancellation(dockerfile *parser.Result) (*dispatchState, error) {
|
||||||
shlex := NewShellLex(dockerfile.EscapeToken)
|
shlex := NewShellLex(dockerfile.EscapeToken)
|
||||||
state := newDispatchState()
|
state := newDispatchState()
|
||||||
|
@ -176,6 +186,15 @@ func (b *Builder) dispatchDockerfileWithCancellation(dockerfile *parser.Result)
|
||||||
// Not cancelled yet, keep going...
|
// Not cancelled yet, keep going...
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If this is a FROM and we have a previous image then
|
||||||
|
// emit an aux message for that image since it is the
|
||||||
|
// end of the previous stage
|
||||||
|
if n.Value == command.From {
|
||||||
|
if err := emitImageID(b.Aux, state); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if n.Value == command.From && state.isCurrentStage(b.options.Target) {
|
if n.Value == command.From && state.isCurrentStage(b.options.Target) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -198,6 +217,12 @@ func (b *Builder) dispatchDockerfileWithCancellation(dockerfile *parser.Result)
|
||||||
b.clearTmp()
|
b.clearTmp()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Emit a final aux message for the final image
|
||||||
|
if err := emitImageID(b.Aux, state); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
return state, nil
|
return state, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"archive/tar"
|
"archive/tar"
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
@ -65,6 +66,7 @@ type buildOptions struct {
|
||||||
networkMode string
|
networkMode string
|
||||||
squash bool
|
squash bool
|
||||||
target string
|
target string
|
||||||
|
imageIDFile string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewBuildCommand creates a new `docker build` command
|
// NewBuildCommand creates a new `docker build` command
|
||||||
|
@ -117,6 +119,7 @@ func NewBuildCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||||
flags.SetAnnotation("network", "version", []string{"1.25"})
|
flags.SetAnnotation("network", "version", []string{"1.25"})
|
||||||
flags.Var(&options.extraHosts, "add-host", "Add a custom host-to-IP mapping (host:ip)")
|
flags.Var(&options.extraHosts, "add-host", "Add a custom host-to-IP mapping (host:ip)")
|
||||||
flags.StringVar(&options.target, "target", "", "Set the target build stage to build.")
|
flags.StringVar(&options.target, "target", "", "Set the target build stage to build.")
|
||||||
|
flags.StringVar(&options.imageIDFile, "iidfile", "", "Write the image ID to the file")
|
||||||
|
|
||||||
command.AddTrustVerificationFlags(flags)
|
command.AddTrustVerificationFlags(flags)
|
||||||
|
|
||||||
|
@ -162,6 +165,12 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error {
|
||||||
progBuff = bytes.NewBuffer(nil)
|
progBuff = bytes.NewBuffer(nil)
|
||||||
buildBuff = bytes.NewBuffer(nil)
|
buildBuff = bytes.NewBuffer(nil)
|
||||||
}
|
}
|
||||||
|
if options.imageIDFile != "" {
|
||||||
|
// Avoid leaving a stale file if we eventually fail
|
||||||
|
if err := os.Remove(options.imageIDFile); err != nil && !os.IsNotExist(err) {
|
||||||
|
return errors.Wrap(err, "Removing image ID file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if options.dockerfileName == "-" {
|
if options.dockerfileName == "-" {
|
||||||
if specifiedContext == "-" {
|
if specifiedContext == "-" {
|
||||||
|
@ -316,7 +325,17 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error {
|
||||||
}
|
}
|
||||||
defer response.Body.Close()
|
defer response.Body.Close()
|
||||||
|
|
||||||
err = jsonmessage.DisplayJSONMessagesStream(response.Body, buildBuff, dockerCli.Out().FD(), dockerCli.Out().IsTerminal(), nil)
|
imageID := ""
|
||||||
|
aux := func(auxJSON *json.RawMessage) {
|
||||||
|
var result types.BuildResult
|
||||||
|
if err := json.Unmarshal(*auxJSON, &result); err != nil {
|
||||||
|
fmt.Fprintf(dockerCli.Err(), "Failed to parse aux message: %s", err)
|
||||||
|
} else {
|
||||||
|
imageID = result.ID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = jsonmessage.DisplayJSONMessagesStream(response.Body, buildBuff, dockerCli.Out().FD(), dockerCli.Out().IsTerminal(), aux)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if jerr, ok := err.(*jsonmessage.JSONError); ok {
|
if jerr, ok := err.(*jsonmessage.JSONError); ok {
|
||||||
// If no error code is set, default to 1
|
// If no error code is set, default to 1
|
||||||
|
@ -344,9 +363,18 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error {
|
||||||
// Everything worked so if -q was provided the output from the daemon
|
// Everything worked so if -q was provided the output from the daemon
|
||||||
// should be just the image ID and we'll print that to stdout.
|
// should be just the image ID and we'll print that to stdout.
|
||||||
if options.quiet {
|
if options.quiet {
|
||||||
fmt.Fprintf(dockerCli.Out(), "%s", buildBuff)
|
imageID = fmt.Sprintf("%s", buildBuff)
|
||||||
|
fmt.Fprintf(dockerCli.Out(), imageID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if options.imageIDFile != "" {
|
||||||
|
if imageID == "" {
|
||||||
|
return errors.Errorf("Server did not provide an image ID. Cannot write %s", options.imageIDFile)
|
||||||
|
}
|
||||||
|
if err := ioutil.WriteFile(options.imageIDFile, []byte(imageID), 0666); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
if command.IsTrusted() {
|
if command.IsTrusted() {
|
||||||
// Since the build was successful, now we must tag any of the resolved
|
// Since the build was successful, now we must tag any of the resolved
|
||||||
// images from the above Dockerfile rewrite.
|
// images from the above Dockerfile rewrite.
|
||||||
|
|
|
@ -18,6 +18,7 @@ keywords: "API, Docker, rcli, REST, documentation"
|
||||||
[Docker Engine API v1.30](https://docs.docker.com/engine/api/v1.30/) documentation
|
[Docker Engine API v1.30](https://docs.docker.com/engine/api/v1.30/) documentation
|
||||||
|
|
||||||
* `GET /info` now returns the list of supported logging drivers, including plugins.
|
* `GET /info` now returns the list of supported logging drivers, including plugins.
|
||||||
|
* `POST /build/` now (when not silent) produces an `Aux` message in the JSON output stream with payload `types.BuildResult` for each image produced. The final such message will reference the image resulting from the build.
|
||||||
|
|
||||||
## v1.29 API changes
|
## v1.29 API changes
|
||||||
|
|
||||||
|
|
|
@ -35,6 +35,7 @@ Options:
|
||||||
-f, --file string Name of the Dockerfile (Default is 'PATH/Dockerfile')
|
-f, --file string Name of the Dockerfile (Default is 'PATH/Dockerfile')
|
||||||
--force-rm Always remove intermediate containers
|
--force-rm Always remove intermediate containers
|
||||||
--help Print usage
|
--help Print usage
|
||||||
|
--iidfile string Write the image ID to the file
|
||||||
--isolation string Container isolation technology
|
--isolation string Container isolation technology
|
||||||
--label value Set metadata for an image (default [])
|
--label value Set metadata for an image (default [])
|
||||||
-m, --memory string Memory limit
|
-m, --memory string Memory limit
|
||||||
|
|
|
@ -28,6 +28,7 @@ import (
|
||||||
"github.com/docker/docker/pkg/testutil"
|
"github.com/docker/docker/pkg/testutil"
|
||||||
icmd "github.com/docker/docker/pkg/testutil/cmd"
|
icmd "github.com/docker/docker/pkg/testutil/cmd"
|
||||||
"github.com/go-check/check"
|
"github.com/go-check/check"
|
||||||
|
"github.com/opencontainers/go-digest"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *DockerSuite) TestBuildJSONEmptyRun(c *check.C) {
|
func (s *DockerSuite) TestBuildJSONEmptyRun(c *check.C) {
|
||||||
|
@ -6398,3 +6399,49 @@ CMD echo foo
|
||||||
out, _ := dockerCmd(c, "inspect", "--format", "{{ json .Config.Cmd }}", "build2")
|
out, _ := dockerCmd(c, "inspect", "--format", "{{ json .Config.Cmd }}", "build2")
|
||||||
c.Assert(strings.TrimSpace(out), checker.Equals, `["/bin/sh","-c","echo foo"]`)
|
c.Assert(strings.TrimSpace(out), checker.Equals, `["/bin/sh","-c","echo foo"]`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *DockerSuite) TestBuildIidFile(c *check.C) {
|
||||||
|
tmpDir, err := ioutil.TempDir("", "TestBuildIidFile")
|
||||||
|
if err != nil {
|
||||||
|
c.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
tmpIidFile := filepath.Join(tmpDir, "iid")
|
||||||
|
|
||||||
|
name := "testbuildiidfile"
|
||||||
|
// Use a Dockerfile with multiple stages to ensure we get the last one
|
||||||
|
cli.BuildCmd(c, name,
|
||||||
|
build.WithDockerfile(`FROM `+minimalBaseImage()+` AS stage1
|
||||||
|
ENV FOO FOO
|
||||||
|
FROM `+minimalBaseImage()+`
|
||||||
|
ENV BAR BAZ`),
|
||||||
|
cli.WithFlags("--iidfile", tmpIidFile))
|
||||||
|
|
||||||
|
id, err := ioutil.ReadFile(tmpIidFile)
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
d, err := digest.Parse(string(id))
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
c.Assert(d.String(), checker.Equals, getIDByName(c, name))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DockerSuite) TestBuildIidFileCleanupOnFail(c *check.C) {
|
||||||
|
tmpDir, err := ioutil.TempDir("", "TestBuildIidFileCleanupOnFail")
|
||||||
|
if err != nil {
|
||||||
|
c.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
tmpIidFile := filepath.Join(tmpDir, "iid")
|
||||||
|
|
||||||
|
err = ioutil.WriteFile(tmpIidFile, []byte("Dummy"), 0666)
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
|
cli.Docker(cli.Build("testbuildiidfilecleanuponfail"),
|
||||||
|
build.WithDockerfile(`FROM `+minimalBaseImage()+`
|
||||||
|
RUN /non/existing/command`),
|
||||||
|
cli.WithFlags("--iidfile", tmpIidFile)).Assert(c, icmd.Expected{
|
||||||
|
ExitCode: 1,
|
||||||
|
})
|
||||||
|
_, err = os.Stat(tmpIidFile)
|
||||||
|
c.Assert(err, check.NotNil)
|
||||||
|
c.Assert(os.IsNotExist(err), check.Equals, true)
|
||||||
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ docker-build - Build an image from a Dockerfile
|
||||||
[**--cpu-shares**[=*0*]]
|
[**--cpu-shares**[=*0*]]
|
||||||
[**--cgroup-parent**[=*CGROUP-PARENT*]]
|
[**--cgroup-parent**[=*CGROUP-PARENT*]]
|
||||||
[**--help**]
|
[**--help**]
|
||||||
|
[**--iidfile**[=*CIDFILE*]]
|
||||||
[**-f**|**--file**[=*PATH/Dockerfile*]]
|
[**-f**|**--file**[=*PATH/Dockerfile*]]
|
||||||
[**-squash**] *Experimental*
|
[**-squash**] *Experimental*
|
||||||
[**--force-rm**]
|
[**--force-rm**]
|
||||||
|
@ -104,6 +105,9 @@ option can be set multiple times.
|
||||||
**--no-cache**=*true*|*false*
|
**--no-cache**=*true*|*false*
|
||||||
Do not use cache when building the image. The default is *false*.
|
Do not use cache when building the image. The default is *false*.
|
||||||
|
|
||||||
|
**--iidfile**=""
|
||||||
|
Write the image ID to the file
|
||||||
|
|
||||||
**--help**
|
**--help**
|
||||||
Print usage statement
|
Print usage statement
|
||||||
|
|
||||||
|
|
|
@ -109,7 +109,7 @@ type JSONMessage struct {
|
||||||
TimeNano int64 `json:"timeNano,omitempty"`
|
TimeNano int64 `json:"timeNano,omitempty"`
|
||||||
Error *JSONError `json:"errorDetail,omitempty"`
|
Error *JSONError `json:"errorDetail,omitempty"`
|
||||||
ErrorMessage string `json:"error,omitempty"` //deprecated
|
ErrorMessage string `json:"error,omitempty"` //deprecated
|
||||||
// Aux contains out-of-band data, such as digests for push signing.
|
// Aux contains out-of-band data, such as digests for push signing and image id after building.
|
||||||
Aux *json.RawMessage `json:"aux,omitempty"`
|
Aux *json.RawMessage `json:"aux,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -132,3 +132,28 @@ func (out *progressOutput) WriteProgress(prog progress.Progress) error {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AuxFormatter is a streamFormatter that writes aux progress messages
|
||||||
|
type AuxFormatter struct {
|
||||||
|
io.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emit emits the given interface as an aux progress message
|
||||||
|
func (sf *AuxFormatter) Emit(aux interface{}) error {
|
||||||
|
auxJSONBytes, err := json.Marshal(aux)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
auxJSON := new(json.RawMessage)
|
||||||
|
*auxJSON = auxJSONBytes
|
||||||
|
msgJSON, err := json.Marshal(&jsonmessage.JSONMessage{Aux: auxJSON})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
msgJSON = appendNewline(msgJSON)
|
||||||
|
n, err := sf.Writer.Write(msgJSON)
|
||||||
|
if n != len(msgJSON) {
|
||||||
|
return io.ErrShortWrite
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue