如何使用io reader客户端

huangapple go评论79阅读模式
英文:

How to use io reader client

问题

我想使用运行在容器内的Go代码将一个压缩文件从主机复制到容器中。设置中有一个运行在容器中的Go代码,并挂载了docker.sock。想法是将主机上的zip文件复制到运行Go代码的容器中。路径参数在主机上。在主机上的命令行如下所示:

docker cp hostFile.zip myContainer:/tmp/

docker-client CopyToContainer的文档如下:

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

如何创建content io.Reader参数?

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,
})
英文:

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,
})

答案1

得分: 2

有很多东西实现了io.Reader接口。在这种情况下,通常的方法是使用os.Open打开一个文件,然后得到的*os.File指针就是一个io.Reader

正如你在评论中提到的,这只能帮助你读取和写入你的“本地”文件系统中的内容。虽然拥有对主机的Docker套接字的访问权限非常强大,但它并不能直接给你对主机文件系统的读写权限。(正如@mkopriva在评论中建议的那样,使用docker run -v /host/path:/container/path绑定挂载方式更简单,也避免了我即将讨论的严重安全问题。)

相反,你需要启动一个绑定挂载所需内容的第二个容器,并从容器中读取文件。听起来你想将它写入本地文件系统,这样会简化事情。在容器内的docker exec shell提示符下,你可以这样做:

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

在Go中,这样做会更复杂,但仍然很可行(未经测试,省略了导入部分):

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
      }
  }
}

正如我之前暗示并在代码中展示的,这种方法绕过了主机上的所有控制;我决定读取主机的/etc/shadow加密密码文件,因为我可以这样做,没有任何东西能阻止我使用基本相同的方法将其写回,并选择自己的root密码。所有者、权限和其他任何东西都无关紧要:Docker守护程序以root身份运行,大多数容器默认以root身份运行(如果不是的话,你也可以显式请求)。

英文:

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 \
  &gt; /tmp/hostFile.zip 

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

ctx := context.Background()
cid, err := client.ContainerCreate(
  ctx,
  &amp;container.Config{
    Image: &quot;docker.io/library/busybox:latest&quot;,
    Cmd: strslice.StrSlice{&quot;cat&quot;, &quot;/host/etc/shadow&quot;},
  },
  &amp;container.HostConfig{
    Mounts: []mount.Mount{
      {
        Type: mount.TypeBind,
        Source: &quot;/&quot;,
        Target: &quot;/host&quot;,
      },
    },
  },
  nil,
  nil,
  &quot;&quot;
)
if err != nil {
  return err
}

defer client.ContainerRemove(ctx, cid.ID, &amp;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(&quot;/tmp/host-shadow&quot;)
  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 := &lt;-cerr:
      return err
    case waited := &lt;-done:
      if waited.Error != nil {
        return errors.New(waited.Error.Message)
      } else if waited.StatusCode != 0 {
        return fmt.Errorf(&quot;cat container exited with status code %v&quot;, 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).

huangapple
  • 本文由 发表于 2022年4月5日 14:09:50
  • 转载请务必保留本文链接:https://go.coder-hub.com/71746994.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定