github.com/Ilhicas/nomad@v1.0.4-0.20210304152020-e86851182bc3/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 cmd.Stdin = strings.NewReader(repoInfo.Index.Name) 157 output, err := cmd.Output() 158 if err != nil { 159 switch err.(type) { 160 default: 161 return nil, err 162 case *exec.ExitError: 163 return nil, fmt.Errorf("%s with input %q failed with stderr: %s", helper, repo, err.Error()) 164 } 165 } 166 167 var response map[string]string 168 if err := json.Unmarshal(output, &response); err != nil { 169 return nil, err 170 } 171 172 auth := &docker.AuthConfiguration{ 173 Username: response["Username"], 174 Password: response["Secret"], 175 } 176 177 if authIsEmpty(auth) { 178 return nil, nil 179 } 180 return auth, nil 181 } 182 } 183 184 // authIsEmpty returns if auth is nil or an empty structure 185 func authIsEmpty(auth *docker.AuthConfiguration) bool { 186 if auth == nil { 187 return false 188 } 189 return auth.Username == "" && 190 auth.Password == "" && 191 auth.Email == "" && 192 auth.ServerAddress == "" 193 } 194 195 func validateCgroupPermission(s string) bool { 196 for _, c := range s { 197 switch c { 198 case 'r', 'w', 'm': 199 default: 200 return false 201 } 202 } 203 204 return true 205 } 206 207 // expandPath returns the absolute path of dir, relative to base if dir is relative path. 208 // base is expected to be an absolute path 209 func expandPath(base, dir string) string { 210 if runtime.GOOS == "windows" { 211 pipeExp := regexp.MustCompile(`^` + rxPipe + `$`) 212 match := pipeExp.FindStringSubmatch(strings.ToLower(dir)) 213 214 if len(match) == 1 { 215 // avoid resolving dot-segment in named pipe 216 return dir 217 } 218 } 219 220 if filepath.IsAbs(dir) { 221 return filepath.Clean(dir) 222 } 223 224 return filepath.Clean(filepath.Join(base, dir)) 225 } 226 227 // isParentPath returns true if path is a child or a descendant of parent path. 228 // Both inputs need to be absolute paths. 229 func isParentPath(parent, path string) bool { 230 rel, err := filepath.Rel(parent, path) 231 return err == nil && !strings.HasPrefix(rel, "..") 232 } 233 234 func parseVolumeSpec(volBind, os string) (hostPath string, containerPath string, mode string, err error) { 235 if os == "windows" { 236 return parseVolumeSpecWindows(volBind) 237 } 238 return parseVolumeSpecLinux(volBind) 239 } 240 241 func parseVolumeSpecWindows(volBind string) (hostPath string, containerPath string, mode string, err error) { 242 parts, err := windowsSplitRawSpec(volBind, rxDestination) 243 if err != nil { 244 return "", "", "", fmt.Errorf("not <src>:<destination> format") 245 } 246 247 if len(parts) < 2 { 248 return "", "", "", fmt.Errorf("not <src>:<destination> format") 249 } 250 251 // Convert host mount path separators to match the host OS's separator 252 // so that relative paths are supported cross-platform regardless of 253 // what slash is used in the jobspec. 254 hostPath = filepath.FromSlash(parts[0]) 255 containerPath = parts[1] 256 257 if len(parts) > 2 { 258 mode = parts[2] 259 } 260 261 return 262 } 263 264 func parseVolumeSpecLinux(volBind string) (hostPath string, containerPath string, mode string, err error) { 265 // using internal parser to preserve old parsing behavior. Docker 266 // parser has additional validators (e.g. mode validity) and accepts invalid output (per Nomad), 267 // e.g. single path entry to be treated as a container path entry with an auto-generated host-path. 268 // 269 // Reconsider updating to use Docker parser when ready to make incompatible changes. 270 parts := strings.Split(volBind, ":") 271 if len(parts) < 2 { 272 return "", "", "", fmt.Errorf("not <src>:<destination> format") 273 } 274 275 m := "" 276 if len(parts) > 2 { 277 m = parts[2] 278 } 279 280 return parts[0], parts[1], m, nil 281 } 282 283 // ResolveAuthConfig matches an auth configuration to a server address or a URL 284 // copied from https://github.com/moby/moby/blob/ca20bc4214e6a13a5f134fb0d2f67c38065283a8/registry/auth.go#L217-L235 285 // but with the CLI types.AuthConfig type rather than api/types 286 func registryResolveAuthConfig(authConfigs map[string]types.AuthConfig, index *registrytypes.IndexInfo) types.AuthConfig { 287 configKey := registry.GetAuthConfigKey(index) 288 // First try the happy case 289 if c, found := authConfigs[configKey]; found || index.Official { 290 return c 291 } 292 293 // Maybe they have a legacy config file, we will iterate the keys converting 294 // them to the new format and testing 295 for r, ac := range authConfigs { 296 if configKey == registry.ConvertToHostname(r) { 297 return ac 298 } 299 } 300 301 // When all else fails, return an empty auth config 302 return types.AuthConfig{} 303 }