github.com/sealerio/sealer@v0.11.1-0.20240507115618-f4f89c5853ae/pkg/image/save/save.go (about) 1 // Copyright © 2021 Alibaba Group Holding Ltd. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package save 16 17 import ( 18 "bufio" 19 "context" 20 "fmt" 21 "io" 22 "strings" 23 "sync" 24 25 "github.com/distribution/distribution/v3" 26 "github.com/distribution/distribution/v3/configuration" 27 "github.com/distribution/distribution/v3/reference" 28 "github.com/distribution/distribution/v3/registry/storage" 29 "github.com/distribution/distribution/v3/registry/storage/driver/factory" 30 dockerstreams "github.com/docker/cli/cli/streams" 31 "github.com/docker/docker/api/types" 32 dockerjsonmessage "github.com/docker/docker/pkg/jsonmessage" 33 "github.com/docker/docker/pkg/progress" 34 "github.com/docker/docker/pkg/streamformatter" 35 "github.com/opencontainers/go-digest" 36 "github.com/sealerio/sealer/common" 37 "github.com/sealerio/sealer/pkg/client/docker/auth" 38 "github.com/sealerio/sealer/pkg/image/save/distributionpkg/proxy" 39 v1 "github.com/sealerio/sealer/types/api/v1" 40 "github.com/sirupsen/logrus" 41 "golang.org/x/sync/errgroup" 42 ) 43 44 const ( 45 HTTPS = "https://" 46 HTTP = "http://" 47 defaultProxyURL = "https://registry-1.docker.io" 48 configRootDir = "rootdirectory" 49 maxPullGoroutineNum = 2 50 maxRetryTime = 3 51 52 manifestV2 = "application/vnd.docker.distribution.manifest.v2+json" 53 manifestOCI = "application/vnd.oci.image.manifest.v1+json" 54 manifestList = "application/vnd.docker.distribution.manifest.list.v2+json" 55 manifestOCIIndex = "application/vnd.oci.image.index.v1+json" 56 ) 57 58 func (is *DefaultImageSaver) SaveImages(images []string, dir string, platform v1.Platform) error { 59 //init a pipe for display pull message 60 reader, writer := io.Pipe() 61 defer func() { 62 _ = reader.Close() 63 _ = writer.Close() 64 }() 65 is.progressOut = streamformatter.NewJSONProgressOutput(writer, false) 66 67 go func() { 68 err := dockerjsonmessage.DisplayJSONMessagesToStream(reader, dockerstreams.NewOut(common.StdOut), nil) 69 if err != nil && err != io.ErrClosedPipe { 70 logrus.Warnf("error occurs in display progressing, err: %s", err) 71 } 72 }() 73 74 existFlag := make(map[string]struct{}) 75 //handle image name 76 for _, image := range images { 77 named, err := ParseNormalizedNamed(image, "") 78 if err != nil { 79 return fmt.Errorf("failed to parse image name:: %v", err) 80 } 81 82 //check if image is duplicate 83 if _, exist := existFlag[named.FullName()]; exist { 84 continue 85 } else { 86 existFlag[named.FullName()] = struct{}{} 87 } 88 89 //check if image exist in disk 90 if err := is.isImageExist(named, dir, platform); err == nil { 91 continue 92 } 93 is.domainToImages[named.domain+named.repo] = append(is.domainToImages[named.domain+named.repo], named) 94 progress.Message(is.progressOut, "", fmt.Sprintf("Pulling image: %s", named.FullName())) 95 } 96 97 //perform image save ability 98 eg, _ := errgroup.WithContext(context.Background()) 99 numCh := make(chan struct{}, maxPullGoroutineNum) 100 for _, nameds := range is.domainToImages { 101 tmpnameds := nameds 102 numCh <- struct{}{} 103 eg.Go(func() error { 104 defer func() { 105 <-numCh 106 }() 107 registry, err := NewProxyRegistry(is.ctx, dir, tmpnameds[0].domain) 108 if err != nil { 109 return fmt.Errorf("failed to init registry: %v", err) 110 } 111 err = is.save(tmpnameds, platform, registry) 112 if err != nil { 113 return fmt.Errorf("failed to save domain %s image: %v", tmpnameds[0].domain, err) 114 } 115 return nil 116 }) 117 } 118 if err := eg.Wait(); err != nil { 119 return err 120 } 121 if len(images) != 0 { 122 progress.Message(is.progressOut, "", "Status: images save success") 123 } 124 return nil 125 } 126 127 // isImageExist check if an image exist in local 128 func (is *DefaultImageSaver) isImageExist(named Named, dir string, platform v1.Platform) error { 129 config := configuration.Configuration{ 130 Storage: configuration.Storage{ 131 driverName: configuration.Parameters{configRootDir: dir}, 132 }, 133 } 134 registry, err := newRegistry(is.ctx, config) 135 if err != nil { 136 return err 137 } 138 139 repo, err := is.getRepository(named, registry) 140 if err != nil { 141 return err 142 } 143 144 blobList, err := is.getLocalDigest(named, repo, platform) 145 if err != nil { 146 return err 147 } 148 149 eg, _ := errgroup.WithContext(context.Background()) 150 numCh := make(chan struct{}, maxPullGoroutineNum) 151 for _, blob := range blobList { 152 numCh <- struct{}{} 153 tmpblob := blob 154 eg.Go(func() error { 155 defer func() { 156 <-numCh 157 }() 158 _, err := registry.BlobStatter().Stat(is.ctx, tmpblob) 159 if err != nil { 160 return err 161 } 162 return nil 163 }) 164 } 165 166 if err := eg.Wait(); err != nil { 167 return err 168 } 169 return nil 170 } 171 172 // newRegistry init a local registry service 173 func newRegistry(ctx context.Context, config configuration.Configuration) (distribution.Namespace, error) { 174 driver, err := factory.Create(config.Storage.Type(), config.Storage.Parameters()) 175 if err != nil { 176 return nil, fmt.Errorf("failed to create storage driver: %v", err) 177 } 178 179 //create a local registry service 180 registry, err := storage.NewRegistry(ctx, driver, make([]storage.RegistryOption, 0)...) 181 if err != nil { 182 return nil, fmt.Errorf("failed to create local registry: %v", err) 183 } 184 return registry, nil 185 } 186 187 // getLocalDigest get local image digest list 188 func (is *DefaultImageSaver) getLocalDigest(named Named, repo distribution.Repository, platform v1.Platform) ([]digest.Digest, error) { 189 manifest, err := repo.Manifests(is.ctx, make([]distribution.ManifestServiceOption, 0)...) 190 if err != nil { 191 return nil, fmt.Errorf("failed to get manifest service: %v", err) 192 } 193 194 tagService := repo.Tags(is.ctx) 195 desc, err := tagService.Get(is.ctx, named.Tag()) 196 if err != nil { 197 return nil, fmt.Errorf("failed to get %s tag descriptor in local: %v", named.repo, err) 198 } 199 200 imageDigest, err := is.handleManifest(manifest, desc.Digest, platform) 201 if err != nil { 202 return nil, fmt.Errorf("failed to get digest: %v", err) 203 } 204 205 blobListJSON, err := manifest.Get(is.ctx, imageDigest, make([]distribution.ManifestServiceOption, 0)...) 206 if err != nil { 207 return nil, err 208 } 209 210 blobList, err := getBlobList(blobListJSON) 211 if err != nil { 212 return nil, fmt.Errorf("failed to get blob list: %v", err) 213 } 214 return blobList, nil 215 } 216 217 func (is *DefaultImageSaver) SaveImagesWithAuth(imageList ImageListWithAuth, dir string, platform v1.Platform) error { 218 //init a pipe for display pull message 219 reader, writer := io.Pipe() 220 defer func() { 221 _ = reader.Close() 222 _ = writer.Close() 223 }() 224 is.progressOut = streamformatter.NewJSONProgressOutput(writer, false) 225 is.ctx = context.Background() 226 go func() { 227 err := dockerjsonmessage.DisplayJSONMessagesToStream(reader, dockerstreams.NewOut(common.StdOut), nil) 228 if err != nil && err != io.ErrClosedPipe { 229 logrus.Warnf("error occurs in display progressing, err: %s", err) 230 } 231 }() 232 233 //perform image save ability 234 eg, _ := errgroup.WithContext(context.Background()) 235 numCh := make(chan struct{}, maxPullGoroutineNum) 236 237 //handle imageList 238 for _, section := range imageList { 239 for _, nameds := range section.Images { 240 tmpnameds := nameds 241 progress.Message(is.progressOut, "", fmt.Sprintf("Pulling image: %s", tmpnameds[0].FullName())) 242 numCh <- struct{}{} 243 eg.Go(func() error { 244 defer func() { 245 <-numCh 246 }() 247 if err := is.download(dir, platform, section, tmpnameds, maxRetryTime); err != nil { 248 return err 249 } 250 return nil 251 }) 252 } 253 if err := eg.Wait(); err != nil { 254 return err 255 } 256 } 257 258 if len(imageList) != 0 { 259 progress.Message(is.progressOut, "", "Status: images save success") 260 } 261 return nil 262 } 263 264 func (is *DefaultImageSaver) download(dir string, platform v1.Platform, section Section, nameds []Named, retryTime int) error { 265 registry, err := NewProxyRegistryWithAuth(is.ctx, section.Username, section.Password, dir, nameds[0].domain) 266 if err != nil { 267 return fmt.Errorf("failed to init registry: %v", err) 268 } 269 err = is.save(nameds, platform, registry) 270 if err != nil { 271 return fmt.Errorf("failed to save domain %s image: %v", nameds[0], err) 272 } 273 274 // double check whether the image is unbroken 275 var imageExistError error 276 var imageExistErrorNamed Named 277 for _, named := range nameds { 278 imageExistError = is.isImageExist(named, dir, platform) 279 if imageExistError != nil { 280 imageExistErrorNamed = named 281 break 282 } 283 } 284 if imageExistError == nil { 285 return nil 286 } 287 if retryTime <= 0 { 288 return imageExistError 289 } 290 // retry to download 291 progress.Message(is.progressOut, "", fmt.Sprintf("Retry: failed to save image(%s) and retry it", imageExistErrorNamed.FullName())) 292 return is.download(dir, platform, section, nameds, retryTime-1) 293 } 294 295 // TODO: support retry mechanism here 296 func (is *DefaultImageSaver) save(nameds []Named, platform v1.Platform, registry distribution.Namespace) error { 297 repo, err := is.getRepository(nameds[0], registry) 298 if err != nil { 299 return err 300 } 301 302 imageDigests, err := is.saveManifestAndGetDigest(nameds, repo, platform) 303 if err != nil { 304 return err 305 } 306 307 err = is.saveBlobs(imageDigests, repo) 308 if err != nil { 309 return err 310 } 311 312 return nil 313 } 314 315 func (is *DefaultImageSaver) getRepository(named Named, registry distribution.Namespace) (distribution.Repository, error) { 316 repoName, err := reference.WithName(named.Repo()) 317 if err != nil { 318 return nil, fmt.Errorf("failed to get repository name: %v", err) 319 } 320 repo, err := registry.Repository(is.ctx, repoName) 321 if err != nil { 322 return nil, fmt.Errorf("failed to get repository: %v", err) 323 } 324 return repo, nil 325 } 326 327 func (is *DefaultImageSaver) saveManifestAndGetDigest(nameds []Named, repo distribution.Repository, platform v1.Platform) ([]digest.Digest, error) { 328 manifest, err := repo.Manifests(is.ctx, make([]distribution.ManifestServiceOption, 0)...) 329 if err != nil { 330 return nil, fmt.Errorf("failed to get manifest service: %v", err) 331 } 332 333 var ( 334 // lock protects imageDigests 335 lock sync.Mutex 336 imageDigests = make([]digest.Digest, 0) 337 numCh = make(chan struct{}, maxPullGoroutineNum) 338 ) 339 340 eg, _ := errgroup.WithContext(context.Background()) 341 342 for _, named := range nameds { 343 tmpnamed := named 344 numCh <- struct{}{} 345 eg.Go(func() error { 346 defer func() { 347 <-numCh 348 }() 349 350 desc, err := repo.Tags(is.ctx).Get(is.ctx, tmpnamed.tag) 351 if err != nil { 352 return fmt.Errorf("failed to get %s tag descriptor: %v. Try \"docker login\" if you are using a private registry", tmpnamed.repo, err) 353 } 354 imageDigest, err := is.handleManifest(manifest, desc.Digest, platform) 355 if err != nil { 356 return fmt.Errorf("failed to get digest: %v", err) 357 } 358 359 lock.Lock() 360 defer lock.Unlock() 361 imageDigests = append(imageDigests, imageDigest) 362 return nil 363 }) 364 } 365 if err := eg.Wait(); err != nil { 366 return nil, err 367 } 368 369 return imageDigests, nil 370 } 371 372 func (is *DefaultImageSaver) handleManifest(manifest distribution.ManifestService, imagedigest digest.Digest, platform v1.Platform) (digest.Digest, error) { 373 mani, err := manifest.Get(is.ctx, imagedigest, make([]distribution.ManifestServiceOption, 0)...) 374 if err != nil { 375 return "", fmt.Errorf("failed to get image manifest: %v", err) 376 } 377 ct, p, err := mani.Payload() 378 if err != nil { 379 return "", fmt.Errorf("failed to get image manifest payload: %v", err) 380 } 381 switch ct { 382 case manifestV2, manifestOCI: 383 return imagedigest, nil 384 case manifestList, manifestOCIIndex: 385 imageDigest, err := getImageManifestDigest(p, platform) 386 if err != nil { 387 return "", fmt.Errorf("failed to get digest from manifest list: %v", err) 388 } 389 return imageDigest, nil 390 case "": 391 //OCI image or image index - no media type in the content 392 //First see if it is a list 393 imageDigest, _ := getImageManifestDigest(p, platform) 394 if imageDigest != "" { 395 return imageDigest, nil 396 } 397 //If not list, then assume it must be an image manifest 398 return imagedigest, nil 399 default: 400 return "", fmt.Errorf("unrecognized manifest content type") 401 } 402 } 403 404 func (is *DefaultImageSaver) saveBlobs(imageDigests []digest.Digest, repo distribution.Repository) error { 405 manifest, err := repo.Manifests(is.ctx, make([]distribution.ManifestServiceOption, 0)...) 406 if err != nil { 407 return fmt.Errorf("failed to get blob service: %v", err) 408 } 409 410 var ( 411 // lock protects blobLists 412 lock sync.Mutex 413 blobLists = make([]digest.Digest, 0) 414 numCh = make(chan struct{}, maxPullGoroutineNum) 415 ) 416 417 eg, _ := errgroup.WithContext(context.Background()) 418 419 //get blob list 420 //each blob identified by a digest 421 for _, imageDigest := range imageDigests { 422 tmpImageDigest := imageDigest 423 numCh <- struct{}{} 424 eg.Go(func() error { 425 defer func() { 426 <-numCh 427 }() 428 429 blobListJSON, err := manifest.Get(is.ctx, tmpImageDigest, make([]distribution.ManifestServiceOption, 0)...) 430 if err != nil { 431 return err 432 } 433 434 blobList, err := getBlobList(blobListJSON) 435 if err != nil { 436 return fmt.Errorf("failed to get blob list: %v", err) 437 } 438 439 lock.Lock() 440 defer lock.Unlock() 441 blobLists = append(blobLists, blobList...) 442 return nil 443 }) 444 } 445 if err = eg.Wait(); err != nil { 446 return err 447 } 448 449 //pull and save each blob 450 blobStore := repo.Blobs(is.ctx) 451 for _, blob := range blobLists { 452 tmpBlob := blob 453 numCh <- struct{}{} 454 eg.Go(func() error { 455 defer func() { 456 <-numCh 457 }() 458 459 if len(string(tmpBlob)) < 19 { 460 return nil 461 } 462 simpleDgst := string(tmpBlob)[7:19] 463 464 _, err = blobStore.Stat(is.ctx, tmpBlob) 465 if err == nil { //blob already exist 466 progress.Update(is.progressOut, simpleDgst, "already exists") 467 return nil 468 } 469 reader, err := blobStore.Open(is.ctx, tmpBlob) 470 if err != nil { 471 return fmt.Errorf("failed to get blob %s: %v", tmpBlob, err) 472 } 473 474 size, err := reader.Seek(0, io.SeekEnd) 475 if err != nil { 476 return fmt.Errorf("seek end error when save blob %s: %v", tmpBlob, err) 477 } 478 _, err = reader.Seek(0, io.SeekStart) 479 if err != nil { 480 return fmt.Errorf("failed to seek start when save blob %s: %v", tmpBlob, err) 481 } 482 preader := progress.NewProgressReader(reader, is.progressOut, size, simpleDgst, "Downloading") 483 484 defer func() { 485 _ = reader.Close() 486 _ = preader.Close() 487 progress.Update(is.progressOut, simpleDgst, "Download complete") 488 }() 489 490 //store to local filesystem 491 //content, err := ioutil.ReadAll(preader) 492 bf := bufio.NewReader(preader) 493 if err != nil { 494 return fmt.Errorf("blob %s content error: %v", tmpBlob, err) 495 } 496 bw, err := blobStore.Create(is.ctx) 497 if err != nil { 498 return fmt.Errorf("failed to create blob store writer: %v", err) 499 } 500 if _, err = bf.WriteTo(bw); err != nil { 501 return fmt.Errorf("failed to write blob to service: %v", err) 502 } 503 _, err = bw.Commit(is.ctx, distribution.Descriptor{ 504 MediaType: "", 505 Size: bw.Size(), 506 Digest: tmpBlob, 507 }) 508 if err != nil { 509 return fmt.Errorf("failed to store blob %s to local: %v", tmpBlob, err) 510 } 511 512 return nil 513 }) 514 } 515 516 if err := eg.Wait(); err != nil { 517 return err 518 } 519 return nil 520 } 521 522 func NewProxyRegistryWithAuth(ctx context.Context, username, password, rootdir, domain string) (distribution.Namespace, error) { 523 // set the URL of registry 524 proxyURL := HTTPS + domain 525 if domain == defaultDomain { 526 proxyURL = defaultProxyURL 527 } 528 529 config := configuration.Configuration{ 530 Proxy: configuration.Proxy{ 531 RemoteURL: proxyURL, 532 Username: username, 533 Password: password, 534 }, 535 Storage: configuration.Storage{ 536 driverName: configuration.Parameters{configRootDir: rootdir}, 537 }, 538 } 539 return newProxyRegistry(ctx, config) 540 } 541 542 func NewProxyRegistry(ctx context.Context, rootdir, domain string) (distribution.Namespace, error) { 543 // set the URL of registry 544 proxyURL := HTTPS + domain 545 if domain == defaultDomain { 546 proxyURL = defaultProxyURL 547 } 548 549 svc, err := auth.NewDockerAuthService() 550 if err != nil { 551 return nil, fmt.Errorf("failed to read default auth file: %v", err) 552 } 553 defaultAuth := types.AuthConfig{ServerAddress: domain} 554 authConfig, err := svc.GetAuthByDomain(domain) 555 //ignore err when is there is no username and password. 556 //regard it as a public registry 557 //only report parse error 558 if err != nil && authConfig != defaultAuth { 559 return nil, fmt.Errorf("failed to get authentication info: %v", err) 560 } 561 562 config := configuration.Configuration{ 563 Proxy: configuration.Proxy{ 564 RemoteURL: proxyURL, 565 Username: authConfig.Username, 566 Password: authConfig.Password, 567 }, 568 Storage: configuration.Storage{ 569 driverName: configuration.Parameters{configRootDir: rootdir}, 570 }, 571 } 572 573 return newProxyRegistry(ctx, config) 574 } 575 576 func newProxyRegistry(ctx context.Context, config configuration.Configuration) (distribution.Namespace, error) { 577 driver, err := factory.Create(config.Storage.Type(), config.Storage.Parameters()) 578 if err != nil { 579 return nil, fmt.Errorf("failed to create storage driver: %v", err) 580 } 581 582 //create a local registry service 583 registry, err := storage.NewRegistry(ctx, driver, make([]storage.RegistryOption, 0)...) 584 if err != nil { 585 return nil, fmt.Errorf("failed to create local registry: %v", err) 586 } 587 588 proxyRegistry, err := proxy.NewRegistryPullThroughCache(ctx, registry, driver, config.Proxy) 589 if err != nil { // try http 590 logrus.Warnf("https error: %v, sealer try to use http", err) 591 config.Proxy.RemoteURL = strings.Replace(config.Proxy.RemoteURL, HTTPS, HTTP, 1) 592 proxyRegistry, err = proxy.NewRegistryPullThroughCache(ctx, registry, driver, config.Proxy) 593 if err != nil { 594 return nil, fmt.Errorf("failed to create proxy registry: %v", err) 595 } 596 } 597 return proxyRegistry, nil 598 }