helpers/docker/machine_command.go (262 lines of code) (raw):
package docker
import (
"bufio"
"context"
"errors"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"sync"
"time"
"github.com/sirupsen/logrus"
)
const (
defaultDockerMachineExecutable = "docker-machine"
crashreportTokenOption = "--bugsnag-api-token"
crashreportToken = "no-report"
)
var dockerMachineExecutable = defaultDockerMachineExecutable
type logWriter struct {
log func(args ...interface{})
reader *bufio.Reader
}
func (l *logWriter) write(line string) {
line = strings.TrimRight(line, "\n")
if line == "" {
return
}
l.log(line)
}
func (l *logWriter) watch() {
var err error
for err != io.EOF {
var line string
line, err = l.reader.ReadString('\n')
if err != nil && err != io.EOF {
if !strings.Contains(err.Error(), "bad file descriptor") {
logrus.WithError(err).Warn("Problem while reading command output")
}
return
}
l.write(line)
}
}
func newLogWriter(logFunction func(args ...interface{}), reader io.Reader) {
writer := &logWriter{
log: logFunction,
reader: bufio.NewReader(reader),
}
go writer.watch()
}
func stdoutLogWriter(cmd *exec.Cmd, fields logrus.Fields) {
log := logrus.WithFields(fields)
reader, err := cmd.StdoutPipe()
if err == nil {
newLogWriter(log.Infoln, reader)
}
}
func stderrLogWriter(cmd *exec.Cmd, fields logrus.Fields) {
log := logrus.WithFields(fields)
reader, err := cmd.StderrPipe()
if err == nil {
newLogWriter(log.Errorln, reader)
}
}
type machineCommand struct {
cache map[string]machineInfo
cacheLock sync.RWMutex
}
type machineInfo struct {
expires time.Time
canConnect bool
}
func (m *machineCommand) Create(driver, name string, opts ...string) error {
args := []string{
"create",
"--driver", driver,
}
for _, opt := range opts {
args = append(args, "--"+opt)
}
args = append(args, name)
cmd := newDockerMachineCommand(args...)
fields := logrus.Fields{
"operation": "create",
"driver": driver,
"name": name,
}
stdoutLogWriter(cmd, fields)
stderrLogWriter(cmd, fields)
logrus.Debugln("Executing", cmd.Path, cmd.Args)
return cmd.Run()
}
func (m *machineCommand) Provision(name string) error {
cmd := newDockerMachineCommand("provision", name)
fields := logrus.Fields{
"operation": "provision",
"name": name,
}
stdoutLogWriter(cmd, fields)
stderrLogWriter(cmd, fields)
return cmd.Run()
}
func (m *machineCommand) Stop(name string, timeout time.Duration) error {
ctx, ctxCancelFn := context.WithTimeout(context.Background(), timeout)
defer ctxCancelFn()
cmd := newDockerMachineCommandCtx(ctx, "stop", name)
fields := logrus.Fields{
"operation": "stop",
"name": name,
}
stdoutLogWriter(cmd, fields)
stderrLogWriter(cmd, fields)
return cmd.Run()
}
func (m *machineCommand) Remove(name string) error {
cmd := newDockerMachineCommand("rm", "-y", name)
fields := logrus.Fields{
"operation": "remove",
"name": name,
}
stdoutLogWriter(cmd, fields)
stderrLogWriter(cmd, fields)
if err := cmd.Run(); err != nil {
return err
}
m.cacheLock.Lock()
delete(m.cache, name)
m.cacheLock.Unlock()
return nil
}
func (m *machineCommand) List() (hostNames []string, err error) {
dir, err := os.ReadDir(getMachineDir())
if err != nil {
errExist := err
// On Windows, ReadDir() on a regular file will satisfy ErrNotExist,
// due to this bug: https://github.com/golang/go/issues/46734
//
// For a workaround, we explicitly check whether the directory
// exists or not with a Stat call.
//nolint:goconst
if runtime.GOOS == "windows" {
_, errExist = os.Stat(getMachineDir())
}
if os.IsNotExist(errExist) {
return nil, nil
}
return nil, err
}
for _, file := range dir {
if file.IsDir() && !strings.HasPrefix(file.Name(), ".") {
hostNames = append(hostNames, file.Name())
}
}
return
}
func (m *machineCommand) get(args ...string) (out string, err error) {
// Execute docker-machine to fetch IP
cmd := newDockerMachineCommand(args...)
data, err := cmd.Output()
if err != nil {
return
}
// Save the IP
out = strings.TrimSpace(string(data))
if out == "" {
err = fmt.Errorf("failed to get %v", args)
}
return
}
func (m *machineCommand) IP(name string) (string, error) {
return m.get("ip", name)
}
func (m *machineCommand) URL(name string) (string, error) {
return m.get("url", name)
}
func (m *machineCommand) CertPath(name string) (string, error) {
return m.get("inspect", name, "-f", "{{.HostOptions.AuthOptions.StorePath}}")
}
func (m *machineCommand) Status(name string) (string, error) {
return m.get("status", name)
}
func (m *machineCommand) Exist(name string) bool {
configPath := filepath.Join(getMachineDir(), name, "config.json")
_, err := os.Stat(configPath)
if err != nil {
return false
}
cmd := newDockerMachineCommand("inspect", name)
fields := logrus.Fields{
"operation": "exists",
"name": name,
}
stderrLogWriter(cmd, fields)
return cmd.Run() == nil
}
func (m *machineCommand) CanConnect(name string, skipCache bool) bool {
m.cacheLock.RLock()
cachedInfo, ok := m.cache[name]
m.cacheLock.RUnlock()
if ok && !skipCache && time.Now().Before(cachedInfo.expires) {
return cachedInfo.canConnect
}
canConnect := m.canConnect(name)
if !canConnect {
return false // we only cache positive hits. Machines usually do not disconnect.
}
m.cacheLock.Lock()
m.cache[name] = machineInfo{
expires: time.Now().Add(5 * time.Minute),
canConnect: true,
}
m.cacheLock.Unlock()
return true
}
func (m *machineCommand) canConnect(name string) bool {
// Execute docker-machine config which actively ask the machine if it is up and online
cmd := newDockerMachineCommand("config", name)
err := cmd.Run()
return err == nil
}
func (m *machineCommand) Credentials(name string) (dc Credentials, err error) {
if !m.CanConnect(name, true) {
err = errors.New("can't connect")
return
}
dc.TLSVerify = true
dc.Host, err = m.URL(name)
if err == nil {
dc.CertPath, err = m.CertPath(name)
}
return
}
func newDockerMachineCommandCtx(ctx context.Context, args ...string) *exec.Cmd {
token := os.Getenv("MACHINE_BUGSNAG_API_TOKEN")
if token == "" {
token = crashreportToken
}
commandArgs := []string{
fmt.Sprintf("%s=%s", crashreportTokenOption, token),
}
commandArgs = append(commandArgs, args...)
cmd := exec.CommandContext(ctx, dockerMachineExecutable, commandArgs...)
cmd.Env = os.Environ()
return cmd
}
func getBaseDir() string {
homeDir := os.Getenv("HOME")
if runtime.GOOS == "windows" {
homeDir = os.Getenv("USERPROFILE")
}
baseDir := os.Getenv("MACHINE_STORAGE_PATH")
if baseDir == "" {
baseDir = filepath.Join(homeDir, ".docker", "machine")
}
return baseDir
}
func getMachineDir() string {
return filepath.Join(getBaseDir(), "machines")
}
func newDockerMachineCommand(args ...string) *exec.Cmd {
return newDockerMachineCommandCtx(context.Background(), args...)
}
func NewMachineCommand() Machine {
return &machineCommand{
cache: map[string]machineInfo{},
}
}