open-nomad/drivers/docker/utils.go

282 lines
7.8 KiB
Go

package docker
import (
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/distribution/reference"
"github.com/docker/docker/registry"
docker "github.com/fsouza/go-dockerclient"
)
func parseDockerImage(image string) (repo, tag string) {
repo, tag = docker.ParseRepositoryTag(image)
if tag != "" {
return repo, tag
}
if i := strings.IndexRune(image, '@'); i > -1 { // Has digest (@sha256:...)
// when pulling images with a digest, the repository contains the sha hash, and the tag is empty
// see: https://github.com/fsouza/go-dockerclient/blob/master/image_test.go#L471
repo = image
} else {
tag = "latest"
}
return repo, tag
}
func dockerImageRef(repo string, tag string) string {
if tag == "" {
return repo
}
return fmt.Sprintf("%s:%s", repo, tag)
}
// loadDockerConfig loads the docker config at the specified path, returning an
// error if it couldn't be read.
func loadDockerConfig(file string) (*configfile.ConfigFile, error) {
f, err := os.Open(file)
if err != nil {
return nil, fmt.Errorf("Failed to open auth config file: %v, error: %v", file, err)
}
defer f.Close()
cfile := new(configfile.ConfigFile)
if err = cfile.LoadFromReader(f); err != nil {
return nil, fmt.Errorf("Failed to parse auth config file: %v", err)
}
return cfile, nil
}
// parseRepositoryInfo takes a repo and returns the Docker RepositoryInfo. This
// is useful for interacting with a Docker config object.
func parseRepositoryInfo(repo string) (*registry.RepositoryInfo, error) {
name, err := reference.ParseNormalizedNamed(repo)
if err != nil {
return nil, fmt.Errorf("Failed to parse named repo %q: %v", repo, err)
}
repoInfo, err := registry.ParseRepositoryInfo(name)
if err != nil {
return nil, fmt.Errorf("Failed to parse repository: %v", err)
}
return repoInfo, nil
}
// firstValidAuth tries a list of auth backends, returning first error or AuthConfiguration
func firstValidAuth(repo string, backends []authBackend) (*docker.AuthConfiguration, error) {
for _, backend := range backends {
auth, err := backend(repo)
if auth != nil || err != nil {
return auth, err
}
}
return nil, nil
}
// authFromTaskConfig generates an authBackend for any auth given in the task-configuration
func authFromTaskConfig(driverConfig *TaskConfig) authBackend {
return func(string) (*docker.AuthConfiguration, error) {
// If all auth fields are empty, return
if len(driverConfig.Auth.Username) == 0 && len(driverConfig.Auth.Password) == 0 && len(driverConfig.Auth.Email) == 0 && len(driverConfig.Auth.ServerAddr) == 0 {
return nil, nil
}
return &docker.AuthConfiguration{
Username: driverConfig.Auth.Username,
Password: driverConfig.Auth.Password,
Email: driverConfig.Auth.Email,
ServerAddress: driverConfig.Auth.ServerAddr,
}, nil
}
}
// authFromDockerConfig generate an authBackend for a dockercfg-compatible file.
// The authBacken can either be from explicit auth definitions or via credential
// helpers
func authFromDockerConfig(file string) authBackend {
return func(repo string) (*docker.AuthConfiguration, error) {
if file == "" {
return nil, nil
}
repoInfo, err := parseRepositoryInfo(repo)
if err != nil {
return nil, err
}
cfile, err := loadDockerConfig(file)
if err != nil {
return nil, err
}
return firstValidAuth(repo, []authBackend{
func(string) (*docker.AuthConfiguration, error) {
dockerAuthConfig := registry.ResolveAuthConfig(cfile.AuthConfigs, repoInfo.Index)
auth := &docker.AuthConfiguration{
Username: dockerAuthConfig.Username,
Password: dockerAuthConfig.Password,
Email: dockerAuthConfig.Email,
ServerAddress: dockerAuthConfig.ServerAddress,
}
if authIsEmpty(auth) {
return nil, nil
}
return auth, nil
},
authFromHelper(cfile.CredentialHelpers[registry.GetAuthConfigKey(repoInfo.Index)]),
authFromHelper(cfile.CredentialsStore),
})
}
}
// authFromHelper generates an authBackend for a docker-credentials-helper;
// A script taking the requested domain on input, outputting JSON with
// "Username" and "Secret"
func authFromHelper(helperName string) authBackend {
return func(repo string) (*docker.AuthConfiguration, error) {
if helperName == "" {
return nil, nil
}
helper := dockerAuthHelperPrefix + helperName
cmd := exec.Command(helper, "get")
repoInfo, err := parseRepositoryInfo(repo)
if err != nil {
return nil, err
}
// Ensure that the HTTPs prefix exists
repoAddr := fmt.Sprintf("https://%s", repoInfo.Index.Name)
cmd.Stdin = strings.NewReader(repoAddr)
output, err := cmd.Output()
if err != nil {
switch err.(type) {
default:
return nil, err
case *exec.ExitError:
return nil, fmt.Errorf("%s with input %q failed with stderr: %s", helper, repo, err.Error())
}
}
var response map[string]string
if err := json.Unmarshal(output, &response); err != nil {
return nil, err
}
auth := &docker.AuthConfiguration{
Username: response["Username"],
Password: response["Secret"],
}
if authIsEmpty(auth) {
return nil, nil
}
return auth, nil
}
}
// authIsEmpty returns if auth is nil or an empty structure
func authIsEmpty(auth *docker.AuthConfiguration) bool {
if auth == nil {
return false
}
return auth.Username == "" &&
auth.Password == "" &&
auth.Email == "" &&
auth.ServerAddress == ""
}
func validateCgroupPermission(s string) bool {
for _, c := range s {
switch c {
case 'r', 'w', 'm':
default:
return false
}
}
return true
}
// expandPath returns the absolute path of dir, relative to base if dir is relative path.
// base is expected to be an absolute path
func expandPath(base, dir string) string {
if os == "windows" {
pipeExp := regexp.MustCompile(`^` + rxPipe + `$`)
match := pipeExp.FindStringSubmatch(strings.ToLower(dir))
if len(match) == 1 {
// avoid resolving dot-segment in named pipe
return dir
}
}
if filepath.IsAbs(dir) {
return filepath.Clean(dir)
}
return filepath.Clean(filepath.Join(base, dir))
}
// isParentPath returns true if path is a child or a descendant of parent path.
// Both inputs need to be absolute paths.
func isParentPath(parent, path string) bool {
rel, err := filepath.Rel(parent, path)
return err == nil && !strings.HasPrefix(rel, "..")
}
func parseVolumeSpec(volBind, os string) (hostPath string, containerPath string, mode string, err error) {
if os == "windows" {
return parseVolumeSpecWindows(volBind)
}
return parseVolumeSpecLinux(volBind)
}
func parseVolumeSpecWindows(volBind string) (hostPath string, containerPath string, mode string, err error) {
parts, err := windowsSplitRawSpec(volBind, rxDestination)
if err != nil {
return "", "", "", fmt.Errorf("not <src>:<destination> format")
}
if len(parts) < 2 {
return "", "", "", fmt.Errorf("not <src>:<destination> format")
}
// Convert host mount path separators to match the host OS's separator
// so that relative paths are supported cross-platform regardless of
// what slash is used in the jobspec.
hostPath = filepath.FromSlash(parts[0])
containerPath = parts[1]
if len(parts) > 2 {
mode = parts[2]
}
return
}
func parseVolumeSpecLinux(volBind string) (hostPath string, containerPath string, mode string, err error) {
// using internal parser to preserve old parsing behavior. Docker
// parser has additional validators (e.g. mode validity) and accepts invalid output (per Nomad),
// e.g. single path entry to be treated as a container path entry with an auto-generated host-path.
//
// Reconsider updating to use Docker parser when ready to make incompatible changes.
parts := strings.Split(volBind, ":")
if len(parts) < 2 {
return "", "", "", fmt.Errorf("not <src>:<destination> format")
}
m := ""
if len(parts) > 2 {
m = parts[2]
}
return parts[0], parts[1], m, nil
}