func (d *dockerExecutor) initWorkspaceVolume() {
	if !d.wsFromDockerVolume {
		util.LogInfo("Workspace volume will be mounted from host machine")
		return
	}
	name := "agent-" + d.agentId
	ok, v := d.getVolume(name)
	if ok {
		d.wsVolume = *v
		util.LogInfo("Agent volume '%s' existed", name)
		return
	}
	body := volume.VolumesCreateBody{Name: name}
	created, err := d.cli.VolumeCreate(d.context, body)
	util.PanicIfErr(err)
	d.wsVolume = created
	util.LogInfo("Workspace volume '%s' created", name)
	return
}
agent volume that bind to /ws inside docker
package executor
import (
	"context"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"github.com/docker/docker/api/types"
	"github.com/docker/docker/api/types/container"
	"github.com/docker/docker/api/types/filters"
	"github.com/docker/docker/api/types/mount"
	"github.com/docker/docker/api/types/volume"
	volumetypes "github.com/docker/docker/api/types/volume"
	"github.com/docker/docker/client"
	"github/flowci/flow-agent-x/domain"
	"github/flowci/flow-agent-x/util"
	"io/ioutil"
	"os"
	"strings"
	"time"
)
const (
	dockerWorkspace       = "/ws"
	dockerPluginDir       = dockerWorkspace + "/.plugins"
	dockerBin             = "/ws/bin"
	dockerEnvFile         = "/tmp/.env"
	dockerPullRetry       = 3
	dockerSock            = "/var/run/docker.sock"
	dockerNetwork         = "flow-ci-agent-default"
	dockerNetworkDriver   = "bridge"
	dockerVarDockerHost   = "DOCKER_HOST"
	dockerDefaultExitCode = -1
	dockerShellPidPath = "/tmp/.shell.pid"
	writeShellPid      = "echo $$ > /tmp/.shell.pid\n"
	writeTtyPid        = "echo $$ > /tmp/.tty.pid\n"
	killShell = "kill -9 $(cat /tmp/.shell.pid)"
	killTty   = "kill -9 $(cat /tmp/.tty.pid)"
)
var (
	placeHolder void
)
type (
	void struct{}
	dockerExecutor struct {
		BaseExecutor
		wsFromDockerVolume bool
		wsVolume           types.Volume
		cli                *client.Client
		configs            []*domain.DockerConfig
		ttyExecId          string
		envFile            string
	}
)
func (d *dockerExecutor) runtime() *domain.DockerConfig {
	if len(d.configs) > 0 {
		return d.configs[0]
	}
	return nil
}
func (d *dockerExecutor) Init() (out error) {
	defer util.RecoverPanic(func(e error) {
		out = e
	})
	d.os = util.OSLinux  
	d.result.StartAt = time.Now()
	d.cli, out = client.NewEnvClient()
	util.PanicIfErr(out)
	util.LogInfo("Docker client version: %s", d.cli.ClientVersion())
	d.initVolumeData()
	d.initNetwork()
	d.initWorkspaceVolume()
	d.initConfig()
	return nil
}
func (d *dockerExecutor) Start() (out error) {
	for i := d.inCmd.Retry; i >= 0; i-- {
		out = d.doStart()
		r := d.result
		if r.Status == domain.CmdStatusException || out != nil {
			if i > 0 {
				d.writeSingleLog(">>>>>>> retry >>>>>>>")
			}
			continue
		}
		break
	}
	return
}
func (d *dockerExecutor) doStart() (out error) {
	defer util.RecoverPanic(func(e error) {
		out = d.handleErrors(e)
	})
	defer d.cleanupContainer()
	d.stdOutWg.Add(1)
	d.pullImage()
	d.startContainer()
	d.copyPlugins()
	d.copyCache()
	eid := d.runShell()
	util.LogDebug("Exec %s is running", eid)
	exitCode := d.waitForExit(d.context, eid, func(pid int) {
		d.toStartStatus(pid)
	})
	d.exportEnv()
	if d.IsInteracting() {
		util.LogDebug("Tty is running, wait..")
		<-d.ttyCtx.Done()
	}
	d.writeCache()
	if d.result.IsFinishStatus() {
		return nil
	}
	d.toFinishStatus(exitCode)
	return
}
func (d *dockerExecutor) StartTty(ttyId string, onStarted func(ttyId string)) (out error) {
	defer util.RecoverPanic(func(e error) {
		out = e
		d.ttyExecId = ""
		d.ttyId = ""
	})
	if d.IsInteracting() {
		panic(fmt.Errorf("interaction is ongoning"))
	}
	runtime := d.runtime()
	if runtime.ContainerID == "" {
		panic(fmt.Errorf("container not started"))
	}
	config := types.ExecConfig{
		Tty:          true,
		AttachStdin:  true,
		AttachStderr: true,
		AttachStdout: true,
		Cmd:          runtime.Config.Entrypoint,
	}
	exec, err := d.cli.ContainerExecCreate(d.context, runtime.ContainerID, config)
	util.PanicIfErr(err)
	attach, err := d.cli.ContainerExecAttach(d.context, exec.ID, config)
	util.PanicIfErr(err)
	d.ttyExecId = exec.ID
	d.ttyId = ttyId
	d.ttyCtx, d.ttyCancel = context.WithCancel(d.context)
	defer func() {
		d.ttyCancel()
		d.ttyCtx = nil
		d.ttyCancel = nil
	}()
	onStarted(ttyId)
	_, _ = attach.Conn.Write([]byte(writeTtyPid))
	go d.writeTtyIn(attach.Conn)
	go d.writeTtyOut(attach.Reader)
	d.waitForExit(d.context, exec.ID, nil)
	return
}
func (d *dockerExecutor) StopTty() {
	_, err := d.runSingleScript(killTty)
	util.LogIfError(err)
}
func (d *dockerExecutor) initVolumeData() {
	runCmd := func(v *domain.DockerVolume) {
		c, err := d.cli.ContainerCreate(d.context,
			&container.Config{
				Image: v.Image,
				Cmd:   []string{v.InitScriptInImage()},
			},
			&container.HostConfig{
				Mounts: []mount.Mount{
					{
						Type:   mount.TypeVolume,
						Source: v.Name,
						Target: v.DefaultTargetInImage(),
					},
				},
			},
			nil,
			fmt.Sprintf("%s-init", v.Name),
		)
		util.PanicIfErr(err)
		defer func() {
			_ = d.cli.ContainerRemove(d.context, c.ID, types.ContainerRemoveOptions{})
		}()
		err = d.cli.ContainerStart(d.context, c.ID, types.ContainerStartOptions{})
		util.PanicIfErr(err)
		_, _ = d.cli.ContainerWait(d.context, c.ID)
	}
	for _, v := range d.volumes {
		if !v.HasImage() {
			continue
		}
		args := filters.NewArgs()
		args.Add("name", v.Name)
		r, err := d.cli.VolumeList(d.context, args)
		util.PanicIfErr(err)
		if len(r.Volumes) > 0 {
			util.LogDebug("volume %s existed", v.Name)
			continue
		}
		err = d.pullImageWithName(v.Image, nil)
		util.PanicIfErr(err)
		_, err = d.cli.VolumeCreate(d.context, volumetypes.VolumesCreateBody{
			Name: v.Name,
		})
		util.PanicIfErr(err)
		runCmd(v)
	}
}
func (d *dockerExecutor) initNetwork() {
	args := filters.NewArgs()
	args.Add("name", dockerNetwork)
	list, err := d.cli.NetworkList(d.context, types.NetworkListOptions{
		Filters: args,
	})
	util.PanicIfErr(err)
	if len(list) == 0 {
		network, err := d.cli.NetworkCreate(d.context, dockerNetwork, types.NetworkCreate{
			CheckDuplicate: true,
			Driver:         dockerNetworkDriver,
		})
		util.PanicIfErr(err)
		util.LogInfo("network %s=%s has been created", dockerNetwork, network.ID)
	}
}
func (d *dockerExecutor) initConfig() {
	d.configs = make([]*domain.DockerConfig, len(d.inCmd.Dockers))
	var runtimeOption *domain.DockerOption
	for i, item := range d.inCmd.Dockers {
		item.SetDefaultNetwork(dockerNetwork)
		if item.IsRuntime {
			runtimeOption = item
			continue
		}
		d.configs[i] = item.ToConfig()
	}
	if runtimeOption == nil {
		if len(d.inCmd.Dockers) > 1 {
			panic(fmt.Errorf("no runtime docker option available"))
		}
		runtimeOption = d.inCmd.Dockers[0]
	}
	d.jobDir = dockerWorkspace + "/" + util.ParseString(d.inCmd.FlowId)
	d.vars[domain.VarAgentWorkspace] = dockerWorkspace
	d.vars[domain.VarAgentJobDir] = d.jobDir
	d.vars[domain.VarAgentPluginDir] = dockerPluginDir
	d.vars[domain.VarAgentDockerNetwork] = dockerNetwork
	ws := d.workspace
	if d.wsFromDockerVolume {
		ws = d.wsVolume.Name
	}
	binds := []string{ws + ":" + dockerWorkspace}
	for _, v := range d.volumes {
		ok, _ := d.getVolume(v.Name)
		if !ok {
			util.LogWarn("Volume %s not found", v.Name)
			continue
		}
		binds = append(binds, v.ToBindStr())
	}
	if util.IsFileExists(dockerSock) || util.IsWindows() {
		binds = append(binds, fmt.Sprintf("%s:%s", dockerSock, dockerSock))
	}
	if d.isK8sEnabled() {
		agentIpKey := fmt.Sprintf(domain.VarAgentIpPattern, "en0")
		d.vars[agentIpKey] = d.k8sConfig.PodIp
		d.vars[dockerVarDockerHost] = fmt.Sprintf("tcp://%s:2375", d.k8sConfig.PodIp)
	}
	d.vars.Resolve()
	config := runtimeOption.ToRuntimeConfig(domain.ConnectVars(d.vars, d.secretVars), d.jobDir, binds)
	if !config.HasEntrypoint() {
		config.Config.Entrypoint = []string{linuxBash}
	}
	d.configs[0] = config
}