【问题标题】:Accept user Input os.stdin to container using Golang docker SDK - Interactive Container使用 Golang docker SDK 接受用户输入 os.stdin 到容器 - 交互式容器
【发布时间】:2020-03-03 01:56:47
【问题描述】:

我最后的手段是在这里问。我是 Golang 的新手,我制作了简单的程序。

我正在尝试执行以下操作: 使用 golang: 1 - 运行一个容器 2 - 接受容器的输入标准输入

我要使用的例子是hashicorp/terraform docker镜像,我想做一个简单的terraform apply但是我需要等待用户输入

以下是我迄今为止工作的代码...任何尝试以下确切代码的人都需要更新 AWS 环境变量或将 terraform 测试文件更改为另一个提供商...或者只使用不同的 docker 映像;-)

package main

import (
    "fmt"
    "github.com/docker/docker/api/types"
    "github.com/docker/docker/api/types/container"
    "github.com/docker/docker/api/types/mount"
    "github.com/docker/docker/client"
    "github.com/docker/docker/pkg/stdcopy"
    "golang.org/x/net/context"
    "io"
    "os"
)

const workingDir = "/home"


func main() {
    ctx := context.Background()
    cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
    if err != nil {
        panic(err)
    }

    reader, err := cli.ImagePull(ctx, "hashicorp/terraform", types.ImagePullOptions{})
    if err != nil {
        panic(err)
    }
    io.Copy(os.Stdout, reader)

    fmt.Println(os.Args)
    cwd, _ := os.Getwd()

    resp, err := cli.ContainerCreate(ctx, &container.Config{
        AttachStdin: true,
        Tty:         false,
        StdinOnce:   true,
        AttachStdout:true,
        Cmd:         os.Args[1:],
        Image:       "hashicorp/terraform",
        WorkingDir:   workingDir,
        Env:         []string{"AWS_ACCESS_KEY_ID=XXX", "AWS_SECRET_ACCESS_KEY=XXX", "AWS_SESSION_TOKEN=XXX"},
        },
    &container.HostConfig{

            Mounts: []mount.Mount{
                mount.Mount{
                    Type: mount.TypeBind,
                    Source: cwd,
                    Target: workingDir,
                },
            },
        },nil, "")
    if err != nil {
        panic(err)
    }

    if err := cli.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{}); err != nil {
        panic(err)
    }

    statusCh, errCh := cli.ContainerWait(ctx, resp.ID, container.WaitConditionNotRunning)
    select {
    case err := <-errCh:
        if err != nil {
            panic(err)
        }
    case <-statusCh:
    }




    out, err := cli.ContainerLogs(ctx, resp.ID, types.ContainerLogsOptions{ShowStdout: true})
    if err != nil {
        panic(err)
    }

    stdcopy.StdCopy(os.Stdout, os.Stderr, out)


}

我的示例 terraform 文件 test.tf

provider "aws" {
  region                  = "eu-west-1"
}


resource "aws_vpc" "main" {
  cidr_block       = "10.0.0.0/16"
  instance_tenancy = "dedicated"

  tags = {
    Name = "test-main-vpc"
  }
}

所以如果我构建那个 go 文件并运行类似的东西

./build apply与test.tf在同一目录

我得到以下输出:


An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # aws_vpc.main will be created
  + resource "aws_vpc" "main" {
      + arn                              = (known after apply)
      + assign_generated_ipv6_cidr_block = false
      + cidr_block                       = "10.0.0.0/16"
      + default_network_acl_id           = (known after apply)
      + default_route_table_id           = (known after apply)
      + default_security_group_id        = (known after apply)
      + dhcp_options_id                  = (known after apply)
      + enable_classiclink               = (known after apply)
      + enable_classiclink_dns_support   = (known after apply)
      + enable_dns_hostnames             = (known after apply)
      + enable_dns_support               = true
      + id                               = (known after apply)
      + instance_tenancy                 = "dedicated"
      + ipv6_association_id              = (known after apply)
      + ipv6_cidr_block                  = (known after apply)
      + main_route_table_id              = (known after apply)
      + owner_id                         = (known after apply)
      + tags                             = {
          + "Name" = "test-main-vpc"
        }
    }

Plan: 1 to add, 0 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: 
Apply cancelled.

Process finished with exit code 0

我一直想弄清楚的是如何等待用户输入。

我认为在容器运行并退出后日志正在打印..所以我相信我需要混合使用这些:

https://godoc.org/github.com/docker/docker/container/stream

https://godoc.org/github.com/docker/docker/client#Client.ContainerAttach

我只是不知道如何实现这些并且没有示例。

任何想法都会有所帮助。我不想要完整的答案我只想要关于如何使用容器/流和/或 Client.ContainerAttach 来等待用户输入的大致方向

非常感谢!

编辑:

我已经设法让它工作了。下面是工作代码

package main

import (
    "bufio"
    "fmt"
    "github.com/docker/docker/api/types"
    "github.com/docker/docker/api/types/container"
    "github.com/docker/docker/api/types/mount"
    "github.com/docker/docker/client"
    "golang.org/x/net/context"
    "io"
    "os"
)

const workingDir = "/home"
var inout chan []byte

func main() {
    inout = make(chan []byte)

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

    reader, err := cli.ImagePull(ctx, "hashicorp/terraform", types.ImagePullOptions{})
    if err != nil {
        panic(err)
    }
    go io.Copy(os.Stdout, reader)


    //fmt.Println(os.Args)
    cwd, _ := os.Getwd()

    resp, err := cli.ContainerCreate(ctx, &container.Config{
        AttachStderr:true,
        AttachStdin: true,
        Tty:         true,
        AttachStdout:true,
        OpenStdin:   true,
        Cmd:         os.Args[1:],
        Image:       "hashicorp/terraform",
        WorkingDir:   workingDir,
        Env:         []string{"AWS_ACCESS_KEY_ID=",
            "AWS_SECRET_ACCESS_KEY=",
            "AWS_SESSION_TOKEN=",

        },
    },
        &container.HostConfig{

            Mounts: []mount.Mount{
                mount.Mount{
                    Type: mount.TypeBind,
                    Source: cwd,
                    Target: workingDir,
                },
            },
        },nil, "")
    if err != nil {
        panic(err)
    }


    if err := cli.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{}); err != nil {
        panic(err)
    }



    waiter, err := cli.ContainerAttach(ctx, resp.ID, types.ContainerAttachOptions{
        Stderr:       true,
        Stdout:       true,
        Stdin:        true,
        Stream:       true,
    })

    go  io.Copy(os.Stdout, waiter.Reader)
    go  io.Copy(os.Stderr, waiter.Reader)

    if err != nil {
        panic(err)
    }

    go func() {
        scanner := bufio.NewScanner(os.Stdin)
        for scanner.Scan() {
            inout <- []byte(scanner.Text())
        }
    }()

    // Write to docker container
    go func(w io.WriteCloser) {
        for {
            data, ok := <-inout
            //log.Println("Received to send to docker", string(data))
            if !ok {
                fmt.Println("!ok")
                w.Close()
                return
            }

            w.Write(append(data, '\n'))
        }
    }(waiter.Conn)


    statusCh, errCh := cli.ContainerWait(ctx, resp.ID, container.WaitConditionNotRunning)
    select {
    case err := <-errCh:
        if err != nil {
            panic(err)
        }
    case <-statusCh:
    }


}

【问题讨论】:

  • 对于 Terraform 等高度交互的程序,在 Docker 中运行它们并从其他程序中驱动它们都会增加显着的复杂性。您还将无法访问仅存在于主机上的 AWS 凭证等内容。由于 Terraform 是作为单个二进制文件分发的,因此安装它并不复杂;我会直接运行terraform apply 而不会尝试将其包装在任何东西中。
  • 我使用 docker 运行良好,但我正在做的是在 golang 中运行 bash/shell 命令。这很好用,我用 aws mfa 很好地工作。我只是想用编程方式替换 docker shell 命令来运行交互式 docker 映像
  • 而不是那些 Scannerwaiter.Conn goroutines,为什么不只是:go io.Copy(waiter.Conn, os.Stdin)
  • 除了复制标准输入和标准输出之外,StdinOnce: true 设置允许我在交互模式下保持正在运行的容器。但是,如果您有执行会话,则不需要这样做。

标签: docker go sdk terraform stdin


【解决方案1】:

对您问题的编辑非常有帮助,所以我只想更新对我有用的内容。

我在使用您的 Stdin 解决方案(与 Scanner 和 waiter.Conn 相关)时遇到问题,并接受了 nicerobot 的建议来使用 Docker 的 cli 源代码中使用的 io.Copy。

为了获得终端的感觉(从标准输入、自动完成等中删除回显),我引用了这个线程:https://github.com/fsouza/go-dockerclient/issues/707 基本上说你必须将标准输入视为原始终端(后来意识到这也在 Docker 的 cli 中完成源代码)。还需要注意的是,您必须尽快恢复终端状态,否则您的其他打印语句可能会出现问题。

最后,我的最终代码看起来像这样......

// ContainerCreate
    
// ContainerAttach

go io.Copy(os.Stdout, waiter.Reader)
go io.Copy(os.Stderr, waiter.Reader)
go io.Copy(waiter.Conn, os.Stdin)

// ContainerStart

// import("golang.org/x/net/context")
fd := int(os.Stdin.Fd())
var oldState *terminal.State
if terminal.IsTerminal(fd) {
  oldState, err = terminal.MakeRaw(fd)
  if err != nil {
    // print error
  }
  defer terminal.Restore(fd, oldState)
}

// ContainerWait for exit

terminal.Restore(fd, oldState)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-06-06
    • 2020-05-05
    • 1970-01-01
    • 2021-09-04
    • 1970-01-01
    相关资源
    最近更新 更多