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