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