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  }