dockergoioreaderdocker-cli

How to use io reader client


I would like to copy a zipped file from host machine to a container using go code running inside a container. The setup has go code running in a container with docker.sock mounted. The idea is to copy zip file from host machine to the container that runs go code. The path parameter is on the host machine. On host machine command line looks like this

docker cp hostFile.zip myContainer:/tmp/

The documentation for docker-client CopyToContainer looks

func (cli *Client) CopyToContainer(ctx context.Context, containerID, dstPath string, content io.Reader, options types.CopyToContainerOptions) error

How to create content io.Reader argument ?

cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
    panic(err)
}

// TODO
// reader := io.Reader()
// reader := file.NewReader()
// tar.NewReader()

cli.CopyToContainer(context.Background(), containerID, dst, reader, types.CopyToContainerOptions{
    AllowOverwriteDirWithFile: true,
    CopyUIDGID:                true,
})

Solution

  • There are a huge variety of things that implement io.Reader. In this case the normal way would be to open a file with os.Open, and then the resulting *os.File pointer is an io.Reader.

    As you note in comments, though, this only helps you read and write things from your "local" filesystem. Having access to the host's Docker socket is super powerful but it doesn't directly give you read and write access to the host filesystem. (As @mkopriva suggests in a comment, launching your container with a docker run -v /host/path:/container/path bind mount is much simpler and avoids the massive security problem I'm about to discuss.)

    What you need to do instead is launch a second container that bind-mounts the content you need, and read the file out of the container. It sounds like you're trying to write it into the local filesystem, which simplifies things. From a docker exec shell prompt inside the container you might do something like

    docker run --rm -v /:/host busybox cat /host/some/path/hostFile.zip \
      > /tmp/hostFile.zip 
    

    In Go it's more involved but still very doable (untested, imports omitted)

    ctx := context.Background()
    cid, err := client.ContainerCreate(
      ctx,
      &container.Config{
        Image: "docker.io/library/busybox:latest",
        Cmd: strslice.StrSlice{"cat", "/host/etc/shadow"},
      },
      &container.HostConfig{
        Mounts: []mount.Mount{
          {
            Type: mount.TypeBind,
            Source: "/",
            Target: "/host",
          },
        },
      },
      nil,
      nil,
      ""
    )
    if err != nil {
      return err
    }
    
    defer client.ContainerRemove(ctx, cid.ID, &types.ContainerRemoveOptions{})
    
    rawLogs, err := client.ContainerLogs(
      ctx,
      cid.ID, 
      types.ContainerLogsOptions{ShowStdout: true},
    )
    if err != nil {
      return err
    }
    defer rawLogs.close()
    
    go func() {
      of, err := os.Create("/tmp/host-shadow")
      if err != nil {
        panic(err)
      }
      defer of.Close()
    
      _ = stdcopy.StdCopy(of, io.Discard, rawLogs)
    }()
    
    done, cerr := client.ContainerWait(ctx, cid.ID, container. WaitConditionNotRunning)
    for {
      select {
        case err := <-cerr:
          return err
        case waited := <-done:
          if waited.Error != nil {
            return errors.New(waited.Error.Message)
          } else if waited.StatusCode != 0 {
            return fmt.Errorf("cat container exited with status code %v", waited.StatusCode)
          } else {
            return nil
          }
      }
    }
    

    As I hinted earlier and showed in the code, this approach bypasses all controls on the host; I've decided to read back the host's /etc/shadow encrypted password file because I can, and nothing would stop me from writing it back with my choice of root password using basically the same approach. Owners, permissions, and anything else don't matter: the Docker daemon runs as root and most containers run as root by default (and you can explicitly request it if not).