github.com/estesp/manifest-tool@v1.0.3/docker/inspect.go (about) 1 package docker 2 3 import ( 4 "encoding/json" 5 "errors" 6 "fmt" 7 "net/http" 8 "net/url" 9 "strings" 10 "syscall" 11 "time" 12 13 "github.com/docker/cli/cli/config" 14 "github.com/docker/distribution/manifest/manifestlist" 15 "github.com/docker/distribution/reference" 16 "github.com/docker/distribution/registry/api/errcode" 17 v2 "github.com/docker/distribution/registry/api/v2" 18 "github.com/docker/distribution/registry/client" 19 "github.com/docker/docker/api" 20 engineTypes "github.com/docker/docker/api/types" 21 registryTypes "github.com/docker/docker/api/types/registry" 22 "github.com/docker/docker/api/types/versions" 23 "github.com/docker/docker/distribution" 24 "github.com/docker/docker/image" 25 "github.com/docker/docker/registry" 26 "github.com/estesp/manifest-tool/types" 27 "github.com/sirupsen/logrus" 28 "golang.org/x/net/context" 29 ) 30 31 const ( 32 // DefaultHostname is the default built-in registry (DockerHub) 33 DefaultHostname = "docker.io" 34 // LegacyDefaultHostname is the old hostname used for DockerHub 35 LegacyDefaultHostname = "index.docker.io" 36 // DefaultRepoPrefix is the prefix used for official images in DockerHub 37 DefaultRepoPrefix = "library/" 38 ) 39 40 type existingTokenHandler struct { 41 token string 42 } 43 44 type dumbCredentialStore struct { 45 auth *engineTypes.AuthConfig 46 } 47 48 func (dcs dumbCredentialStore) Basic(*url.URL) (string, string) { 49 return dcs.auth.Username, dcs.auth.Password 50 } 51 52 func (th *existingTokenHandler) Scheme() string { 53 return "bearer" 54 } 55 56 func (th *existingTokenHandler) AuthorizeRequest(req *http.Request, params map[string]string) error { 57 req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", th.token)) 58 return nil 59 } 60 61 func (dcs dumbCredentialStore) RefreshToken(*url.URL, string) string { 62 return dcs.auth.IdentityToken 63 } 64 65 func (dcs dumbCredentialStore) SetRefreshToken(*url.URL, string, string) { 66 } 67 68 // fallbackError wraps an error that can possibly allow fallback to a different 69 // endpoint. 70 type fallbackError struct { 71 // err is the error being wrapped. 72 err error 73 // confirmedV2 is set to true if it was confirmed that the registry 74 // supports the v2 protocol. This is used to limit fallbacks to the v1 75 // protocol. 76 confirmedV2 bool 77 transportOK bool 78 } 79 80 // Error renders the FallbackError as a string. 81 func (f fallbackError) Error() string { 82 return f.err.Error() 83 } 84 85 type manifestFetcher interface { 86 Fetch(ctx context.Context, ref reference.Named) ([]types.ImageInspect, error) 87 } 88 89 func validateName(name string) error { 90 distref, err := reference.ParseNormalizedNamed(name) 91 if err != nil { 92 return err 93 } 94 hostname, _ := splitHostname(distref.String()) 95 if hostname == "" { 96 return fmt.Errorf("Please use a fully qualified repository name") 97 } 98 return nil 99 } 100 101 // splitHostname splits a repository name to hostname and remotename string. 102 // If no valid hostname is found, the default hostname is used. Repository name 103 // needs to be already validated before. 104 func splitHostname(name string) (hostname, remoteName string) { 105 i := strings.IndexRune(name, '/') 106 if i == -1 || (!strings.ContainsAny(name[:i], ".:") && name[:i] != "localhost") { 107 hostname, remoteName = DefaultHostname, name 108 } else { 109 hostname, remoteName = name[:i], name[i+1:] 110 } 111 if hostname == LegacyDefaultHostname { 112 hostname = DefaultHostname 113 } 114 if hostname == DefaultHostname && !strings.ContainsRune(remoteName, '/') { 115 remoteName = DefaultRepoPrefix + remoteName 116 } 117 return 118 } 119 120 func checkHTTPRedirect(req *http.Request, via []*http.Request) error { 121 if len(via) >= 10 { 122 return errors.New("stopped after 10 redirects") 123 } 124 125 if len(via) > 0 { 126 for headerName, headerVals := range via[0].Header { 127 if headerName != "Accept" && headerName != "Range" { 128 continue 129 } 130 for _, val := range headerVals { 131 // Don't add to redirected request if redirected 132 // request already has a header with the same 133 // name and value. 134 hasValue := false 135 for _, existingVal := range req.Header[headerName] { 136 if existingVal == val { 137 hasValue = true 138 break 139 } 140 } 141 if !hasValue { 142 req.Header.Add(headerName, val) 143 } 144 } 145 } 146 } 147 148 return nil 149 } 150 151 // GetImageData takes registry authentication information and a name of the image to return information about 152 func GetImageData(a *types.AuthInfo, name string, insecure, includeTags bool) ([]types.ImageInspect, *registry.RepositoryInfo, error) { 153 if err := validateName(name); err != nil { 154 return nil, nil, err 155 } 156 ref, err := reference.ParseNormalizedNamed(name) 157 if err != nil { 158 return nil, nil, err 159 } 160 repoInfo, err := registry.ParseRepositoryInfo(ref) 161 if err != nil { 162 return nil, nil, err 163 } 164 authConfig, err := getAuthConfig(a, repoInfo.Index) 165 if err != nil { 166 return nil, nil, err 167 } 168 if err := validateRepoName(repoInfo.Name.Name()); err != nil { 169 return nil, nil, err 170 } 171 options := registry.ServiceOptions{} 172 if insecure { 173 options.InsecureRegistries = append(options.InsecureRegistries, reference.Domain(repoInfo.Name)) 174 } 175 registryService, err := registry.NewService(options) 176 if err != nil { 177 return nil, nil, err 178 } 179 180 endpoints, err := registryService.LookupPullEndpoints(reference.Domain(repoInfo.Name)) 181 if err != nil { 182 return nil, nil, err 183 } 184 logrus.Debugf("endpoints: %v", endpoints) 185 186 var ( 187 ctx = context.Background() 188 lastErr error 189 discardNoSupportErrors bool 190 foundImages []types.ImageInspect 191 confirmedV2 bool 192 confirmedTLSRegistries = make(map[string]struct{}) 193 ) 194 195 for _, endpoint := range endpoints { 196 // make sure I can reach the registry, same as docker pull does 197 if endpoint.Version == registry.APIVersion1 { 198 logrus.Debugf("Skipping v1 endpoint %s; manifest list requires v2", endpoint.URL) 199 continue 200 } 201 if insecure && endpoint.URL.Scheme == "https" { 202 logrus.Debugf("Skipping https endpoint for insecure registry") 203 continue 204 } 205 206 if endpoint.URL.Scheme != "https" { 207 if _, confirmedTLS := confirmedTLSRegistries[endpoint.URL.Host]; confirmedTLS { 208 logrus.Debugf("Skipping non-TLS endpoint %s for host/port that appears to use TLS", endpoint.URL) 209 continue 210 } 211 } 212 if insecure { 213 endpoint.TLSConfig.InsecureSkipVerify = true 214 } 215 216 logrus.Debugf("Trying to fetch image manifest of %s repository from %s %s", repoInfo.Name.Name(), endpoint.URL, endpoint.Version) 217 218 fetcher, err := newManifestFetcher(endpoint, repoInfo, authConfig, registryService, includeTags) 219 if err != nil { 220 lastErr = err 221 continue 222 } 223 224 if foundImages, err = fetcher.Fetch(ctx, ref); err != nil { 225 // Was this fetch cancelled? If so, don't try to fall back. 226 fallback := false 227 select { 228 case <-ctx.Done(): 229 default: 230 if fallbackErr, ok := err.(fallbackError); ok { 231 fallback = true 232 confirmedV2 = confirmedV2 || fallbackErr.confirmedV2 233 if fallbackErr.transportOK && endpoint.URL.Scheme == "https" { 234 confirmedTLSRegistries[endpoint.URL.Host] = struct{}{} 235 } 236 err = fallbackErr.err 237 } 238 } 239 if fallback { 240 if _, ok := err.(distribution.ErrNoSupport); !ok { 241 // Because we found an error that's not ErrNoSupport, discard all subsequent ErrNoSupport errors. 242 discardNoSupportErrors = true 243 // save the current error 244 lastErr = err 245 } else if !discardNoSupportErrors { 246 // Save the ErrNoSupport error, because it's either the first error or all encountered errors 247 // were also ErrNoSupport errors. 248 lastErr = err 249 } 250 continue 251 } 252 logrus.Infof("Not continuing with pull after error: %v", err) 253 return nil, nil, err 254 } 255 256 return foundImages, repoInfo, nil 257 } 258 259 if lastErr == nil { 260 lastErr = fmt.Errorf("no endpoints found for %s", ref.String()) 261 } 262 263 return nil, nil, lastErr 264 } 265 266 func newManifestFetcher(endpoint registry.APIEndpoint, repoInfo *registry.RepositoryInfo, authConfig engineTypes.AuthConfig, registryService registry.Service, includeTags bool) (manifestFetcher, error) { 267 switch endpoint.Version { 268 case registry.APIVersion2: 269 return &v2ManifestFetcher{ 270 endpoint: endpoint, 271 authConfig: authConfig, 272 service: registryService, 273 repoInfo: repoInfo, 274 includeTags: includeTags, 275 }, nil 276 case registry.APIVersion1: 277 return &v1ManifestFetcher{ 278 endpoint: endpoint, 279 authConfig: authConfig, 280 service: registryService, 281 repoInfo: repoInfo, 282 }, nil 283 } 284 return nil, fmt.Errorf("unknown version %d for registry %s", endpoint.Version, endpoint.URL) 285 } 286 287 func getAuthConfig(a *types.AuthInfo, index *registryTypes.IndexInfo) (engineTypes.AuthConfig, error) { 288 289 var ( 290 username = a.Username 291 password = a.Password 292 cfg = a.DockerCfg 293 defAuthConfig = engineTypes.AuthConfig{ 294 Username: a.Username, 295 Password: a.Password, 296 Email: "stub@example.com", 297 } 298 ) 299 300 if username != "" && password != "" { 301 return defAuthConfig, nil 302 } 303 304 confFile, err := config.Load(cfg) 305 if err != nil { 306 return engineTypes.AuthConfig{}, err 307 } 308 authConfig := registry.ResolveAuthConfig(confFile.AuthConfigs, index) 309 logrus.Debugf("authConfig for %s: %v", index.Name, authConfig.Username) 310 311 return authConfig, nil 312 } 313 314 func validateRepoName(name string) error { 315 if name == "" { 316 return fmt.Errorf("Repository name can't be empty") 317 } 318 if name == api.NoBaseImageSpecifier { 319 return fmt.Errorf("'%s' is a reserved name", api.NoBaseImageSpecifier) 320 } 321 return nil 322 } 323 324 func makeImageInspect(img *image.Image, tag string, mfInfo manifestInfo, mediaType string, tagList []string) *types.ImageInspect { 325 var digest string 326 if err := mfInfo.digest.Validate(); err == nil { 327 digest = mfInfo.digest.String() 328 } 329 330 // for manifest lists, we only want to display the basic info that this is 331 // a manifest list and its digest information: 332 if mediaType == manifestlist.MediaTypeManifestList { 333 return &types.ImageInspect{ 334 MediaType: mediaType, 335 Digest: digest, 336 } 337 } 338 339 var digests []string 340 for _, blobDigest := range mfInfo.blobDigests { 341 digests = append(digests, blobDigest.String()) 342 } 343 return &types.ImageInspect{ 344 Size: mfInfo.length, 345 MediaType: mediaType, 346 Tag: tag, 347 Digest: digest, 348 RepoTags: tagList, 349 Comment: img.Comment, 350 Created: img.Created.Format(time.RFC3339Nano), 351 ContainerConfig: &img.ContainerConfig, 352 DockerVersion: img.DockerVersion, 353 Author: img.Author, 354 Config: img.Config, 355 Architecture: img.Architecture, 356 Os: img.OS, 357 OSVersion: img.OSVersion, 358 OSFeatures: img.OSFeatures, 359 References: digests, 360 Layers: mfInfo.layers, 361 Platform: mfInfo.platform, 362 CanonicalJSON: mfInfo.jsonBytes, 363 } 364 } 365 366 func makeRawConfigFromV1Config(imageJSON []byte, rootfs *image.RootFS, history []image.History) (map[string]*json.RawMessage, error) { 367 var dver struct { 368 DockerVersion string `json:"docker_version"` 369 } 370 371 if err := json.Unmarshal(imageJSON, &dver); err != nil { 372 return nil, err 373 } 374 375 useFallback := versions.LessThan(dver.DockerVersion, "1.8.3") 376 377 if useFallback { 378 var v1Image image.V1Image 379 err := json.Unmarshal(imageJSON, &v1Image) 380 if err != nil { 381 return nil, err 382 } 383 imageJSON, err = json.Marshal(v1Image) 384 if err != nil { 385 return nil, err 386 } 387 } 388 389 var c map[string]*json.RawMessage 390 if err := json.Unmarshal(imageJSON, &c); err != nil { 391 return nil, err 392 } 393 394 c["rootfs"] = rawJSON(rootfs) 395 c["history"] = rawJSON(history) 396 397 return c, nil 398 } 399 400 func rawJSON(value interface{}) *json.RawMessage { 401 jsonval, err := json.Marshal(value) 402 if err != nil { 403 return nil 404 } 405 return (*json.RawMessage)(&jsonval) 406 } 407 408 func continueOnError(err error) bool { 409 switch v := err.(type) { 410 case errcode.Errors: 411 if len(v) == 0 { 412 return true 413 } 414 return continueOnError(v[0]) 415 case distribution.ErrNoSupport: 416 return continueOnError(v.Err) 417 case errcode.Error: 418 return shouldV2Fallback(v) 419 case *client.UnexpectedHTTPResponseError: 420 return true 421 case ImageConfigPullError: 422 return false 423 case error: 424 return !strings.Contains(err.Error(), strings.ToLower(syscall.ENOSPC.Error())) 425 } 426 // let's be nice and fallback if the error is a completely 427 // unexpected one. 428 // If new errors have to be handled in some way, please 429 // add them to the switch above. 430 return true 431 } 432 433 // shouldV2Fallback returns true if this error is a reason to fall back to v1. 434 func shouldV2Fallback(err errcode.Error) bool { 435 switch err.Code { 436 case errcode.ErrorCodeUnauthorized, v2.ErrorCodeManifestUnknown, v2.ErrorCodeNameUnknown: 437 return true 438 } 439 return false 440 }