2

I am using the Go Docker Client to attempt to build an image from a Dockerfile whose contents are defined in code.

According to the Docker Daemon API Documentation

The input stream must be a tar archive...

...The archive must include a build instructions file, typically called Dockerfile at the archive’s root.

So I want to create the build context in code, write it to a tar file, then send that to the Docker Daemon to be built. To do this I can use the ImageBuild function and pass in the tar file (build context) as an io.ReadCloser. As long as my Dockerfile is at the root of that compressed archive, it should find it and build it.

However, I get the common error:

Error response from daemon: Cannot locate specified Dockerfile: Dockerfile

Which obviously means that it can't find the Dockerfile at the root of the archive. I am unsure why. I believe the way I am doing this adds a Dockerfile to the root of the tar archive. The daemon should see this. What am I misunderstanding here?

code snippet to reproduce

    var buf bytes.Buffer
    tarWriter := tar.NewWriter(&buf)

    contents := "FROM alpine\nCMD [\"echo\", \"this is from the archive\"]"

    if err := tarWriter.WriteHeader(&tar.Header{
        Name:     "Dockerfile",
        Mode:     777,
        Size:     int64(len(contents)),
        Typeflag: tar.TypeReg,
    }); err != nil {
        panic(err)
    }

    if _, err := tarWriter.Write([]byte(contents)); err != nil {
        panic(err)
    }

    if err := tarWriter.Close(); err != nil {
        panic(err)
    }

    reader := tar.NewReader(&buf)
    c, err := client.NewEnvClient()
    if err != nil {
        panic(err)
    }

    _, err = c.ImageBuild(context.Background(), reader, types.ImageBuildOptions{
        Context:    reader,
        Dockerfile: "Dockerfile",
    })
    if err != nil {
        panic(err)
    }

go.mod file

module docker-tar

go 1.12

require (
    github.com/docker/distribution v2.7.1+incompatible // indirect
    github.com/docker/docker v1.13.1
    github.com/docker/go-connections v0.4.0 // indirect
    github.com/docker/go-units v0.4.0 // indirect
    github.com/opencontainers/go-digest v1.0.0-rc1 // indirect
    github.com/pkg/errors v0.8.1 // indirect
    golang.org/x/net v0.0.0-20191112182307-2180aed22343 // indirect
)
Community
  • 1
  • 1
northsideknight
  • 1,519
  • 1
  • 15
  • 24

1 Answers1

3
  1. Add a zero in front of 777 for octal numeral system: 0777, 0o777, or 0O777

  2. Use reader := bytes.NewReader(buf.Bytes()) not tar.NewReader(&buf)

  3. Use client.WithAPIVersionNegotiation() for newer versions too.

  4. Try this working version:

package main

import (
    "archive/tar"
    "bytes"
    "context"
    "fmt"

    "github.com/docker/docker/api/types"
    "github.com/docker/docker/client"
)

func main() {
    var buf bytes.Buffer
    tarWriter := tar.NewWriter(&buf)
    contents := `FROM alpine:3.10.3
CMD ["echo", "this is from the archive"]
`
    header := &tar.Header{
        Name:     "Dockerfile",
        Mode:     0o777,
        Size:     int64(len(contents)),
        Typeflag: tar.TypeReg,
    }
    err := tarWriter.WriteHeader(header)
    if err != nil {
        panic(err)
    }
    _, err = tarWriter.Write([]byte(contents))
    if err != nil {
        panic(err)
    }
    err = tarWriter.Close()
    if err != nil {
        panic(err)
    }

    c, err := client.NewClientWithOpts(client.WithAPIVersionNegotiation())
    if err != nil {
        panic(err)
    }
    fmt.Println(c.ClientVersion())

    reader := bytes.NewReader(buf.Bytes()) // tar.NewReader(&buf)
    ctx := context.Background()
    buildOptions := types.ImageBuildOptions{
        Context:    reader,
        Dockerfile: "Dockerfile",
        Tags:       []string{"alpine-echo:1.2.4"},
    }
    _, err = c.ImageBuild(ctx, reader, buildOptions)
    if err != nil {
        panic(err)
    }
}


Output for docker image ls after go run .:

REPOSITORY                       TAG                 IMAGE ID            CREATED             SIZE
alpine-echo                      1.2.4               d81774f32812        26 seconds ago      5.55MB
alpine                           3.10.3              b168ac0e770e        4 days ago          5.55MB

Output for docker run alpine-echo:1.2.4:

this is from the archive

Note: You may need to edit FROM alpine:3.10.3 for your specific version.

wasmup
  • 14,541
  • 6
  • 42
  • 58
  • I don't understand how `0777` is allowed for octal instead the more explicit `0o777`. It's misleading and it's really not clear to me what it adds as value . – Clément Nov 14 '19 at 05:29
  • See: ` mode = os.ModeDir | 0777` line 317 file `go1.13.4.src/src/archive/zip/struct.go` and 129 search results for `0777` – wasmup Nov 14 '19 at 05:38
  • I'm not arguing at all about whether or nor it is allowed, I'm more interested about why it is. If anyone else cares, this has been discussed a lot apparently: [Proposal: Go 2 Number Literal Changes](https://go.googlesource.com/proposal/+/master/design/19308-number-literals.md), [#151](https://github.com/golang/go/issues/12711) and [#12711](https://github.com/golang/go/issues/151). – Clément Nov 14 '19 at 07:16
  • [Integer literals](https://golang.org/ref/spec#Integer_literals): An integer literal is a sequence of digits representing an integer constant. An optional prefix sets a non-decimal base: 0b or 0B for binary, **0, 0o, or 0O for octal**, and 0x or 0X for hexadecimal. A single 0 is considered a decimal zero. In hexadecimal literals, letters a through f and A through F represent values 10 through 15. – wasmup Nov 14 '19 at 09:44