github.com/cvmfs/docker-graphdriver@v0.0.0-20181206110523-155ec6df0521/repository-manager/lib/image.go (about) 1 package lib 2 3 import ( 4 "compress/gzip" 5 "encoding/json" 6 "fmt" 7 "io" 8 "io/ioutil" 9 "net/http" 10 "os" 11 "path/filepath" 12 "reflect" 13 "strings" 14 "sync" 15 16 "github.com/docker/docker/image" 17 "github.com/olekukonko/tablewriter" 18 log "github.com/sirupsen/logrus" 19 20 da "github.com/cvmfs/docker-graphdriver/repository-manager/docker-api" 21 ) 22 23 type ManifestRequest struct { 24 Image Image 25 Password string 26 } 27 28 type Image struct { 29 Id int 30 User string 31 Scheme string 32 Registry string 33 Repository string 34 Tag string 35 Digest string 36 IsThin bool 37 Manifest *da.Manifest 38 } 39 40 func (i Image) GetSimpleName() string { 41 name := fmt.Sprintf("%s/%s", i.Registry, i.Repository) 42 if i.Tag == "" { 43 return name 44 } else { 45 return name + ":" + i.Tag 46 } 47 } 48 49 func (i Image) WholeName() string { 50 root := fmt.Sprintf("%s://%s/%s", i.Scheme, i.Registry, i.Repository) 51 if i.Tag != "" { 52 root = fmt.Sprintf("%s:%s", root, i.Tag) 53 } 54 if i.Digest != "" { 55 root = fmt.Sprintf("%s@%s", root, i.Digest) 56 } 57 return root 58 } 59 60 func (i Image) GetManifestUrl() string { 61 url := fmt.Sprintf("%s://%s/v2/%s/manifests/", i.Scheme, i.Registry, i.Repository) 62 if i.Digest != "" { 63 url = fmt.Sprintf("%s%s", url, i.Digest) 64 } else { 65 url = fmt.Sprintf("%s%s", url, i.Tag) 66 } 67 return url 68 } 69 70 func (i Image) GetReference() string { 71 if i.Digest == "" && i.Tag != "" { 72 return ":" + i.Tag 73 } 74 if i.Digest != "" && i.Tag == "" { 75 return "@" + i.Digest 76 } 77 if i.Digest != "" && i.Tag != "" { 78 return ":" + i.Tag + "@" + i.Digest 79 } 80 panic("Image wrong format, missing both tag and digest") 81 } 82 83 func (i Image) GetSimpleReference() string { 84 if i.Tag != "" { 85 return i.Tag 86 } 87 if i.Digest != "" { 88 return i.Digest 89 } 90 panic("Image wrong format, missing both tag and digest") 91 } 92 93 func (img Image) PrintImage(machineFriendly, csv_header bool) { 94 if machineFriendly { 95 if csv_header { 96 fmt.Printf("name,user,scheme,registry,repository,tag,digest,is_thin\n") 97 } 98 fmt.Printf("%s,%s,%s,%s,%s,%s,%s,%s\n", 99 img.WholeName(), img.User, img.Scheme, 100 img.Registry, img.Repository, 101 img.Tag, img.Digest, 102 fmt.Sprint(img.IsThin)) 103 } else { 104 table := tablewriter.NewWriter(os.Stdout) 105 table.SetAlignment(tablewriter.ALIGN_LEFT) 106 table.SetHeader([]string{"Key", "Value"}) 107 table.Append([]string{"Name", img.WholeName()}) 108 table.Append([]string{"User", img.User}) 109 table.Append([]string{"Scheme", img.Scheme}) 110 table.Append([]string{"Registry", img.Registry}) 111 table.Append([]string{"Repository", img.Repository}) 112 table.Append([]string{"Tag", img.Tag}) 113 table.Append([]string{"Digest", img.Digest}) 114 var is_thin string 115 if img.IsThin { 116 is_thin = "true" 117 } else { 118 is_thin = "false" 119 } 120 table.Append([]string{"IsThin", is_thin}) 121 table.Render() 122 } 123 } 124 125 func (img Image) GetManifest() (da.Manifest, error) { 126 if img.Manifest != nil { 127 return *img.Manifest, nil 128 } 129 bytes, err := img.getByteManifest() 130 if err != nil { 131 return da.Manifest{}, err 132 } 133 var manifest da.Manifest 134 err = json.Unmarshal(bytes, &manifest) 135 if err != nil { 136 return manifest, err 137 } 138 if reflect.DeepEqual(da.Manifest{}, manifest) { 139 return manifest, fmt.Errorf("Got empty manifest") 140 } 141 img.Manifest = &manifest 142 return manifest, nil 143 } 144 145 func (img Image) GetChanges() (changes []string, err error) { 146 user := img.User 147 pass, err := getPassword() 148 if err != nil { 149 LogE(err).Warning("Unable to get the credential for downloading the configuration blog, trying anonymously") 150 user = "" 151 pass = "" 152 } 153 154 changes = []string{"ENV CVMFS_IMAGE true"} 155 manifest, err := img.GetManifest() 156 if err != nil { 157 LogE(err).Warning("Impossible to retrieve the manifest of the image, not changes set") 158 return 159 } 160 configUrl := fmt.Sprintf("%s://%s/v2/%s/blobs/%s", 161 img.Scheme, img.Registry, img.Repository, manifest.Config.Digest) 162 token, err := firstRequestForAuth(configUrl, user, pass) 163 if err != nil { 164 LogE(err).Warning("Impossible to retrieve the token for getting the changes from the repository, not changes set") 165 return 166 } 167 client := &http.Client{} 168 req, err := http.NewRequest("GET", configUrl, nil) 169 if err != nil { 170 LogE(err).Warning("Impossible to create a request for getting the changes no chnages set.") 171 return 172 } 173 req.Header.Set("Authorization", token) 174 req.Header.Set("Accept", "application/vnd.docker.distribution.manifest.v2+json") 175 176 resp, err := client.Do(req) 177 defer resp.Body.Close() 178 body, err := ioutil.ReadAll(resp.Body) 179 if err != nil { 180 LogE(err).Warning("Error in reading the body from the configuration, no change set") 181 return 182 } 183 184 var config image.Image 185 err = json.Unmarshal(body, &config) 186 if err != nil { 187 LogE(err).Warning("Error in unmarshaling the configuration of the image") 188 return 189 } 190 env := config.Config.Env 191 192 if len(env) > 0 { 193 for _, e := range env { 194 envs := strings.SplitN(e, "=", 2) 195 if len(envs) != 2 { 196 continue 197 } 198 change := fmt.Sprintf("ENV %s=\"%s\"", envs[0], envs[1]) 199 changes = append(changes, change) 200 } 201 } 202 203 cmd := config.Config.Cmd 204 205 if len(cmd) > 0 { 206 for _, c := range cmd { 207 changes = append(changes, fmt.Sprintf("CMD %s", c)) 208 } 209 } 210 211 return 212 } 213 214 func (img Image) GetSingularityLocation() string { 215 return fmt.Sprintf("docker://%s/%s%s", img.Registry, img.Repository, img.GetReference()) 216 } 217 218 func GetSingularityPathFromManifest(manifest da.Manifest) string { 219 digest := strings.Split(manifest.Config.Digest, ":")[1] 220 return filepath.Join(".flat", digest[0:2], digest) 221 } 222 223 // here is where in the FS we are going to store the singularity image 224 func (img Image) GetSingularityPath() (string, error) { 225 manifest, err := img.GetManifest() 226 if err != nil { 227 LogE(err).Error("Error in getting the manifest to figureout the singularity path") 228 return "", err 229 } 230 return GetSingularityPathFromManifest(manifest), nil 231 } 232 233 type Singularity struct { 234 Image *Image 235 TempDirectory string 236 } 237 238 func (img Image) DownloadSingularityDirectory(rootPath string) (sing Singularity, err error) { 239 dir, err := ioutil.TempDir(rootPath, "singularity_buffer") 240 if err != nil { 241 LogE(err).Error("Error in creating temporary directory for singularity") 242 return 243 244 } 245 singularityTempCache, err := ioutil.TempDir("", "tempDirSingularityCache") 246 if err != nil { 247 LogE(err).Error("Error in creating temporary directory for singularity cache") 248 return 249 } 250 defer os.RemoveAll(singularityTempCache) 251 err = ExecCommand("singularity", "build", "--sandbox", dir, img.GetSingularityLocation()).Env( 252 "SINGULARITY_CACHEDIR", singularityTempCache).Start() 253 if err != nil { 254 LogE(err).Error("Error in downloading the singularity image") 255 return 256 } 257 258 Log().Info("Successfully download the singularity image") 259 return Singularity{Image: &img, TempDirectory: dir}, nil 260 } 261 262 func (s Singularity) IngestIntoCVMFS(CVMFSRepo string) error { 263 symlinkPath := filepath.Join(s.Image.Registry, s.Image.Repository+":"+s.Image.GetSimpleReference()) 264 singularityPath, err := s.Image.GetSingularityPath() 265 if err != nil { 266 LogE(err).Error( 267 "Error in ingesting singularity image into CVMFS, unable to get where save the image") 268 return err 269 } 270 271 err = IngestIntoCVMFS(CVMFSRepo, singularityPath, s.TempDirectory) 272 if err != nil { 273 // if there is an error ingest does not remove the folder. 274 // we do want to remove the folder anyway 275 os.RemoveAll(s.TempDirectory) 276 return err 277 } 278 279 for _, dir := range []string{ 280 filepath.Dir(singularityPath), 281 singularityPath} { 282 283 err = CreateCatalogIntoDir(CVMFSRepo, dir) 284 if err != nil { 285 LogE(err).WithFields(log.Fields{ 286 "directory": dir}).Error( 287 "Impossible to create subcatalog in super-directory.") 288 } else { 289 Log().WithFields(log.Fields{ 290 "directory": dir}).Info( 291 "Created subcatalog in directory") 292 } 293 294 } 295 296 // lets create the symlink 297 err = CreateSymlinkIntoCVMFS(CVMFSRepo, symlinkPath, singularityPath) 298 if err != nil { 299 LogE(err).Error("Error in creating the symlink for the singularity Image") 300 return err 301 } 302 return nil 303 } 304 305 func (img Image) getByteManifest() ([]byte, error) { 306 pass, err := getPassword() 307 if err != nil { 308 LogE(err).Warning("Unable to retrieve the password, trying to get the manifest anonymously.") 309 return img.getAnonymousManifest() 310 } 311 return img.getManifestWithPassword(pass) 312 } 313 314 func (img Image) getAnonymousManifest() ([]byte, error) { 315 return getManifestWithUsernameAndPassword(img, "", "") 316 } 317 318 func (img Image) getManifestWithPassword(password string) ([]byte, error) { 319 return getManifestWithUsernameAndPassword(img, img.User, password) 320 } 321 322 func getManifestWithUsernameAndPassword(img Image, user, pass string) ([]byte, error) { 323 324 url := img.GetManifestUrl() 325 326 token, err := firstRequestForAuth(url, user, pass) 327 if err != nil { 328 LogE(err).Error("Error in getting the authentication token") 329 return nil, err 330 } 331 332 client := &http.Client{} 333 req, err := http.NewRequest("GET", url, nil) 334 if err != nil { 335 LogE(err).Error("Impossible to create a HTTP request") 336 return nil, err 337 } 338 339 req.Header.Set("Authorization", token) 340 req.Header.Set("Accept", "application/vnd.docker.distribution.manifest.v2+json") 341 342 resp, err := client.Do(req) 343 if err != nil { 344 LogE(err).Error("Error in making the HTTP request") 345 return nil, err 346 } 347 defer resp.Body.Close() 348 body, err := ioutil.ReadAll(resp.Body) 349 if err != nil { 350 LogE(err).Error("Error in reading the second http response") 351 return nil, err 352 } 353 return body, nil 354 } 355 356 func firstRequestForAuth(url, user, pass string) (token string, err error) { 357 resp, err := http.Get(url) 358 if err != nil { 359 LogE(err).Error("Error in making the first request for auth") 360 return "", err 361 } 362 defer resp.Body.Close() 363 if resp.StatusCode != 401 { 364 log.WithFields(log.Fields{ 365 "status code": resp.StatusCode, 366 }).Info("Expected status code 401, print body anyway.") 367 body, err := ioutil.ReadAll(resp.Body) 368 if err != nil { 369 LogE(err).Error("Error in reading the first http response") 370 } 371 fmt.Println(string(body)) 372 return "", err 373 } 374 WwwAuthenticate := resp.Header["Www-Authenticate"][0] 375 token, err = requestAuthToken(WwwAuthenticate, user, pass) 376 if err != nil { 377 LogE(err).Error("Error in getting the authentication token") 378 return "", err 379 } 380 return token, nil 381 382 } 383 384 func getLayerUrl(img Image, layer da.Layer) string { 385 return fmt.Sprintf("%s://%s/v2/%s/blobs/%s", 386 img.Scheme, img.Registry, img.Repository, layer.Digest) 387 } 388 389 type downloadedLayer struct { 390 Name string 391 Path string 392 } 393 394 func (img Image) GetLayers(layersChan chan<- downloadedLayer, manifestChan chan<- string, stopGettingLayers <-chan bool, rootPath string) error { 395 defer close(layersChan) 396 defer close(manifestChan) 397 398 user := img.User 399 pass, err := getPassword() 400 if err != nil { 401 LogE(err).Warning("Unable to retrieve the password, trying to get the layers anonymously.") 402 user = "" 403 pass = "" 404 } 405 406 // then we try to get the manifest from our database 407 manifest, err := img.GetManifest() 408 if err != nil { 409 LogE(err).Warn("Error in getting the manifest") 410 return err 411 } 412 413 // A first request is used to get the authentication 414 firstLayer := manifest.Layers[0] 415 layerUrl := getLayerUrl(img, firstLayer) 416 token, err := firstRequestForAuth(layerUrl, user, pass) 417 if err != nil { 418 return err 419 } 420 421 var wg sync.WaitGroup 422 defer wg.Wait() 423 // at this point we iterate each layer and we download it. 424 for _, layer := range manifest.Layers { 425 wg.Add(1) 426 go func(layer da.Layer) { 427 defer wg.Done() 428 Log().WithFields(log.Fields{"layer": layer.Digest}).Info("Start working on layer") 429 toSend, err := img.downloadLayer(layer, token, rootPath) 430 if err != nil { 431 LogE(err).Error("Error in downloading a layer") 432 return 433 } 434 layersChan <- toSend 435 }(layer) 436 } 437 438 // finally we marshal the manifest and store it into a file 439 manifestBytes, err := json.Marshal(manifest) 440 if err != nil { 441 LogE(err).Error("Error in marshaling the manifest") 442 return err 443 } 444 manifestPath := filepath.Join(rootPath, "manifest.json") 445 err = ioutil.WriteFile(manifestPath, manifestBytes, 0666) 446 if err != nil { 447 LogE(err).Error("Error in writing the manifest to file") 448 return err 449 } 450 // ship the manifest file 451 manifestChan <- manifestPath 452 return nil 453 } 454 455 func (img Image) downloadLayer(layer da.Layer, token, rootPath string) (toSend downloadedLayer, err error) { 456 user := img.User 457 pass, err := getPassword() 458 if err != nil { 459 LogE(err).Warning("Unable to retrieve the password, trying to get the layers anonymously.") 460 user = "" 461 pass = "" 462 } 463 layerUrl := getLayerUrl(img, layer) 464 if token == "" { 465 token, err = firstRequestForAuth(layerUrl, user, pass) 466 if err != nil { 467 return 468 } 469 } 470 for i := 0; i <= 5; i++ { 471 err = nil 472 client := &http.Client{} 473 req, err := http.NewRequest("GET", layerUrl, nil) 474 if err != nil { 475 LogE(err).Error("Impossible to create the HTTP request.") 476 break 477 } 478 req.Header.Set("Authorization", token) 479 resp, err := client.Do(req) 480 Log().WithFields(log.Fields{"layer": layer.Digest}).Info("Make request for layer") 481 if err != nil { 482 break 483 } 484 if 200 <= resp.StatusCode && resp.StatusCode < 300 { 485 486 gread, err := gzip.NewReader(resp.Body) 487 if err != nil { 488 LogE(err).Warning("Error in creating the zip to unzip the layer") 489 continue 490 } 491 492 tmpFile, err := ioutil.TempFile(rootPath, "layer.*.tar") 493 if err != nil { 494 LogE(err).Warning("Error in creating buffer temp layer") 495 continue 496 } 497 defer tmpFile.Close() 498 499 _, err = io.Copy(tmpFile, gread) 500 if err != nil { 501 LogE(err).Warning("Error in copying the layer into the temp file") 502 os.Remove(tmpFile.Name()) 503 continue 504 } 505 506 toSend = downloadedLayer{Name: layer.Digest, Path: tmpFile.Name()} 507 return toSend, nil 508 509 } else { 510 Log().Warning("Received status code ", resp.StatusCode) 511 err = fmt.Errorf("Layer not received, status code: %s", resp.StatusCode) 512 } 513 } 514 return 515 516 } 517 518 func parseBearerToken(token string) (realm string, options map[string]string, err error) { 519 options = make(map[string]string) 520 args := token[7:] 521 keyValue := strings.Split(args, ",") 522 for _, kv := range keyValue { 523 splitted := strings.Split(kv, "=") 524 if len(splitted) != 2 { 525 err = fmt.Errorf("Wrong formatting of the token") 526 return 527 } 528 splitted[1] = strings.Trim(splitted[1], `"`) 529 if splitted[0] == "realm" { 530 realm = splitted[1] 531 } else { 532 options[splitted[0]] = splitted[1] 533 } 534 } 535 return 536 } 537 538 func requestAuthToken(token, user, pass string) (authToken string, err error) { 539 realm, options, err := parseBearerToken(token) 540 if err != nil { 541 return 542 } 543 req, err := http.NewRequest("GET", realm, nil) 544 if err != nil { 545 return 546 } 547 548 query := req.URL.Query() 549 for k, v := range options { 550 query.Add(k, v) 551 } 552 if user != "" && pass != "" { 553 query.Add("offline_token", "true") 554 req.SetBasicAuth(user, pass) 555 } 556 req.URL.RawQuery = query.Encode() 557 558 client := &http.Client{} 559 resp, err := client.Do(req) 560 defer resp.Body.Close() 561 562 if resp.StatusCode >= 400 { 563 err = fmt.Errorf("Authorization error %s", resp.Status) 564 return 565 } 566 567 var jsonResp map[string]interface{} 568 err = json.NewDecoder(resp.Body).Decode(&jsonResp) 569 if err != nil { 570 return 571 } 572 authTokenInterface, ok := jsonResp["token"] 573 if ok { 574 authToken = "Bearer " + authTokenInterface.(string) 575 } else { 576 err = fmt.Errorf("Didn't get the token key from the server") 577 return 578 } 579 return 580 }