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
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:
}
}
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)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With