package docker import ( "encoding/json" "fmt" "os" "os/exec" "path/filepath" "regexp" "runtime" "strings" "github.com/docker/cli/cli/config/configfile" "github.com/docker/cli/cli/config/types" "github.com/docker/distribution/reference" registrytypes "github.com/docker/docker/api/types/registry" "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 := registryResolveAuthConfig(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 runtime.GOOS == "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 : format") } if len(parts) < 2 { return "", "", "", fmt.Errorf("not : 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 : format") } m := "" if len(parts) > 2 { m = parts[2] } return parts[0], parts[1], m, nil } // ResolveAuthConfig matches an auth configuration to a server address or a URL // copied from https://github.com/moby/moby/blob/ca20bc4214e6a13a5f134fb0d2f67c38065283a8/registry/auth.go#L217-L235 // but with the CLI types.AuthConfig type rather than api/types func registryResolveAuthConfig(authConfigs map[string]types.AuthConfig, index *registrytypes.IndexInfo) types.AuthConfig { configKey := registry.GetAuthConfigKey(index) // First try the happy case if c, found := authConfigs[configKey]; found || index.Official { return c } // Maybe they have a legacy config file, we will iterate the keys converting // them to the new format and testing for r, ac := range authConfigs { if configKey == registry.ConvertToHostname(r) { return ac } } // When all else fails, return an empty auth config return types.AuthConfig{} }