Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Accept user Input os.stdin to container using Golang docker SDK - Interactive Container

My last resort is asking here. I'm new to Golang and I've made simple programs.

I'm trying to do the following: Using golang: 1 - run a container 2 - accept input stdin to the container

The example I want to use is the hashicorp/terraform docker image, I want to do a simple terraform apply but I need to wait for user input

below is the code I have working so far...anyone trying the exact code below needs to update the AWS environment variables or change the terraform test file to another provider...or just use a different docker image ;-)

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)


}

My example terraform file 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"
  }
}

so if I build that go file and run something like

./build apply with the test.tf in the same directory

I get the below output:


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

What I've been trying to figure out is how to wait for user input.

The logs are printing after the container is run and exits I think.. so I believe I need to use a mixture of these:

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

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

I just dont know how to implement these and there are no examples.

Any ideas would be helpful. I dont want the full answer I just want the general direction on how to use the container/stream and/or Client.ContainerAttach to wait for user input

Thanks so much!

EDIT:

I've managed to get it working. Below is the working code

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


}
like image 664
David Walton Avatar asked Nov 06 '19 14:11

David Walton


1 Answers

The edit to your question was super helpful so I just wanted to update with what had worked for me.

I had problems using your solution for Stdin (pertaining to Scanner and waiter.Conn) and took nicerobot's advice to use io.Copy which is used in Docker's cli source code.

To get the terminal feel (removing the echo from stdin, autocomplete, etc), I referenced this thread: https://github.com/fsouza/go-dockerclient/issues/707 which basically said you have to treat stdin as a raw terminal (later realized this is also done in Docker's cli source code). It's also important to note that you have to restore the terminal state as soon as possible, otherwise your other print statements may be wonky.

In the end, my final code looks something like this...

// 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)
like image 55
Elizabeth Nguyen Avatar answered Nov 02 '22 19:11

Elizabeth Nguyen