github.com/bigcommerce/nomad@v0.9.3-bc/drivers/docker/utils.go (about)

     1  package docker
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"fmt"
     7  	"os"
     8  	"os/exec"
     9  	"path/filepath"
    10  	"strings"
    11  
    12  	"github.com/docker/cli/cli/config/configfile"
    13  	"github.com/docker/distribution/reference"
    14  	"github.com/docker/docker/registry"
    15  	"github.com/docker/docker/volume/mounts"
    16  	docker "github.com/fsouza/go-dockerclient"
    17  )
    18  
    19  func parseDockerImage(image string) (repo, tag string) {
    20  	repo, tag = docker.ParseRepositoryTag(image)
    21  	if tag != "" {
    22  		return repo, tag
    23  	}
    24  	if i := strings.IndexRune(image, '@'); i > -1 { // Has digest (@sha256:...)
    25  		// when pulling images with a digest, the repository contains the sha hash, and the tag is empty
    26  		// see: https://github.com/fsouza/go-dockerclient/blob/master/image_test.go#L471
    27  		repo = image
    28  	} else {
    29  		tag = "latest"
    30  	}
    31  	return repo, tag
    32  }
    33  
    34  func dockerImageRef(repo string, tag string) string {
    35  	if tag == "" {
    36  		return repo
    37  	}
    38  	return fmt.Sprintf("%s:%s", repo, tag)
    39  }
    40  
    41  // loadDockerConfig loads the docker config at the specified path, returning an
    42  // error if it couldn't be read.
    43  func loadDockerConfig(file string) (*configfile.ConfigFile, error) {
    44  	f, err := os.Open(file)
    45  	if err != nil {
    46  		return nil, fmt.Errorf("Failed to open auth config file: %v, error: %v", file, err)
    47  	}
    48  	defer f.Close()
    49  
    50  	cfile := new(configfile.ConfigFile)
    51  	if err = cfile.LoadFromReader(f); err != nil {
    52  		return nil, fmt.Errorf("Failed to parse auth config file: %v", err)
    53  	}
    54  	return cfile, nil
    55  }
    56  
    57  // parseRepositoryInfo takes a repo and returns the Docker RepositoryInfo. This
    58  // is useful for interacting with a Docker config object.
    59  func parseRepositoryInfo(repo string) (*registry.RepositoryInfo, error) {
    60  	name, err := reference.ParseNormalizedNamed(repo)
    61  	if err != nil {
    62  		return nil, fmt.Errorf("Failed to parse named repo %q: %v", repo, err)
    63  	}
    64  
    65  	repoInfo, err := registry.ParseRepositoryInfo(name)
    66  	if err != nil {
    67  		return nil, fmt.Errorf("Failed to parse repository: %v", err)
    68  	}
    69  
    70  	return repoInfo, nil
    71  }
    72  
    73  // firstValidAuth tries a list of auth backends, returning first error or AuthConfiguration
    74  func firstValidAuth(repo string, backends []authBackend) (*docker.AuthConfiguration, error) {
    75  	for _, backend := range backends {
    76  		auth, err := backend(repo)
    77  		if auth != nil || err != nil {
    78  			return auth, err
    79  		}
    80  	}
    81  	return nil, nil
    82  }
    83  
    84  // authFromTaskConfig generates an authBackend for any auth given in the task-configuration
    85  func authFromTaskConfig(driverConfig *TaskConfig) authBackend {
    86  	return func(string) (*docker.AuthConfiguration, error) {
    87  		// If all auth fields are empty, return
    88  		if len(driverConfig.Auth.Username) == 0 && len(driverConfig.Auth.Password) == 0 && len(driverConfig.Auth.Email) == 0 && len(driverConfig.Auth.ServerAddr) == 0 {
    89  			return nil, nil
    90  		}
    91  		return &docker.AuthConfiguration{
    92  			Username:      driverConfig.Auth.Username,
    93  			Password:      driverConfig.Auth.Password,
    94  			Email:         driverConfig.Auth.Email,
    95  			ServerAddress: driverConfig.Auth.ServerAddr,
    96  		}, nil
    97  	}
    98  }
    99  
   100  // authFromDockerConfig generate an authBackend for a dockercfg-compatible file.
   101  // The authBacken can either be from explicit auth definitions or via credential
   102  // helpers
   103  func authFromDockerConfig(file string) authBackend {
   104  	return func(repo string) (*docker.AuthConfiguration, error) {
   105  		if file == "" {
   106  			return nil, nil
   107  		}
   108  		repoInfo, err := parseRepositoryInfo(repo)
   109  		if err != nil {
   110  			return nil, err
   111  		}
   112  
   113  		cfile, err := loadDockerConfig(file)
   114  		if err != nil {
   115  			return nil, err
   116  		}
   117  
   118  		return firstValidAuth(repo, []authBackend{
   119  			func(string) (*docker.AuthConfiguration, error) {
   120  				dockerAuthConfig := registry.ResolveAuthConfig(cfile.AuthConfigs, repoInfo.Index)
   121  				auth := &docker.AuthConfiguration{
   122  					Username:      dockerAuthConfig.Username,
   123  					Password:      dockerAuthConfig.Password,
   124  					Email:         dockerAuthConfig.Email,
   125  					ServerAddress: dockerAuthConfig.ServerAddress,
   126  				}
   127  				if authIsEmpty(auth) {
   128  					return nil, nil
   129  				}
   130  				return auth, nil
   131  			},
   132  			authFromHelper(cfile.CredentialHelpers[registry.GetAuthConfigKey(repoInfo.Index)]),
   133  			authFromHelper(cfile.CredentialsStore),
   134  		})
   135  	}
   136  }
   137  
   138  // authFromHelper generates an authBackend for a docker-credentials-helper;
   139  // A script taking the requested domain on input, outputting JSON with
   140  // "Username" and "Secret"
   141  func authFromHelper(helperName string) authBackend {
   142  	return func(repo string) (*docker.AuthConfiguration, error) {
   143  		if helperName == "" {
   144  			return nil, nil
   145  		}
   146  		helper := dockerAuthHelperPrefix + helperName
   147  		cmd := exec.Command(helper, "get")
   148  
   149  		repoInfo, err := parseRepositoryInfo(repo)
   150  		if err != nil {
   151  			return nil, err
   152  		}
   153  
   154  		// Ensure that the HTTPs prefix exists
   155  		repoAddr := fmt.Sprintf("https://%s", repoInfo.Index.Name)
   156  
   157  		cmd.Stdin = strings.NewReader(repoAddr)
   158  		output, err := cmd.Output()
   159  		if err != nil {
   160  			switch err.(type) {
   161  			default:
   162  				return nil, err
   163  			case *exec.ExitError:
   164  				return nil, fmt.Errorf("%s with input %q failed with stderr: %s", helper, repo, output)
   165  			}
   166  		}
   167  
   168  		var response map[string]string
   169  		if err := json.Unmarshal(output, &response); err != nil {
   170  			return nil, err
   171  		}
   172  
   173  		auth := &docker.AuthConfiguration{
   174  			Username: response["Username"],
   175  			Password: response["Secret"],
   176  		}
   177  
   178  		if authIsEmpty(auth) {
   179  			return nil, nil
   180  		}
   181  		return auth, nil
   182  	}
   183  }
   184  
   185  // authIsEmpty returns if auth is nil or an empty structure
   186  func authIsEmpty(auth *docker.AuthConfiguration) bool {
   187  	if auth == nil {
   188  		return false
   189  	}
   190  	return auth.Username == "" &&
   191  		auth.Password == "" &&
   192  		auth.Email == "" &&
   193  		auth.ServerAddress == ""
   194  }
   195  
   196  func validateCgroupPermission(s string) bool {
   197  	for _, c := range s {
   198  		switch c {
   199  		case 'r', 'w', 'm':
   200  		default:
   201  			return false
   202  		}
   203  	}
   204  
   205  	return true
   206  }
   207  
   208  // expandPath returns the absolute path of dir, relative to base if dir is relative path.
   209  // base is expected to be an absolute path
   210  func expandPath(base, dir string) string {
   211  	if filepath.IsAbs(dir) {
   212  		return filepath.Clean(dir)
   213  	}
   214  
   215  	return filepath.Clean(filepath.Join(base, dir))
   216  }
   217  
   218  // isParentPath returns true if path is a child or a descendant of parent path.
   219  // Both inputs need to be absolute paths.
   220  func isParentPath(parent, path string) bool {
   221  	rel, err := filepath.Rel(parent, path)
   222  	return err == nil && !strings.HasPrefix(rel, "..")
   223  }
   224  
   225  func parseVolumeSpec(volBind, os string) (hostPath string, containerPath string, mode string, err error) {
   226  	if os == "windows" {
   227  		return parseVolumeSpecWindows(volBind)
   228  	}
   229  	return parseVolumeSpecLinux(volBind)
   230  }
   231  
   232  func parseVolumeSpecWindows(volBind string) (hostPath string, containerPath string, mode string, err error) {
   233  	parser := mounts.NewParser("windows")
   234  	m, err := parser.ParseMountRaw(volBind, "")
   235  	if err != nil {
   236  		return "", "", "", err
   237  	}
   238  
   239  	src := m.Source
   240  	if src == "" && strings.Contains(volBind, m.Name) {
   241  		src = m.Name
   242  	}
   243  
   244  	if src == "" {
   245  		return "", "", "", errors.New("missing host path")
   246  	}
   247  
   248  	if m.Destination == "" {
   249  		return "", "", "", errors.New("container path is empty")
   250  	}
   251  
   252  	return src, m.Destination, m.Mode, nil
   253  }
   254  
   255  func parseVolumeSpecLinux(volBind string) (hostPath string, containerPath string, mode string, err error) {
   256  	// using internal parser to preserve old parsing behavior.  Docker
   257  	// parser has additional validators (e.g. mode validity) and accepts invalid output (per Nomad),
   258  	// e.g. single path entry to be treated as a container path entry with an auto-generated host-path.
   259  	//
   260  	// Reconsider updating to use Docker parser when ready to make incompatible changes.
   261  	parts := strings.Split(volBind, ":")
   262  	if len(parts) < 2 {
   263  		return "", "", "", fmt.Errorf("not <src>:<destination> format")
   264  	}
   265  
   266  	m := ""
   267  	if len(parts) > 2 {
   268  		m = parts[2]
   269  	}
   270  
   271  	return parts[0], parts[1], m, nil
   272  }