github.com/alibaba/sealer@v0.8.6-0.20220430115802-37a2bdaa8173/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  
    24  	v1 "github.com/alibaba/sealer/types/api/v1"
    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  	"golang.org/x/sync/errgroup"
    37  
    38  	"github.com/alibaba/sealer/common"
    39  	"github.com/alibaba/sealer/logger"
    40  	"github.com/alibaba/sealer/pkg/image/distributionutil"
    41  	"github.com/alibaba/sealer/pkg/image/save/distributionpkg/proxy"
    42  	"github.com/alibaba/sealer/utils"
    43  )
    44  
    45  const (
    46  	HTTPS               = "https://"
    47  	HTTP                = "http://"
    48  	defaultProxyURL     = "https://registry-1.docker.io"
    49  	configRootDir       = "rootdirectory"
    50  	maxPullGoroutineNum = 2
    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  			logger.Warn("error occurs in display progressing, err: %s", err)
    71  		}
    72  	}()
    73  
    74  	//handle image name
    75  	for _, image := range images {
    76  		named, err := ParseNormalizedNamed(image, "")
    77  		if err != nil {
    78  			return fmt.Errorf("parse image name error: %v", err)
    79  		}
    80  		is.domainToImages[named.domain+named.repo] = append(is.domainToImages[named.domain+named.repo], named)
    81  		progress.Message(is.progressOut, "", fmt.Sprintf("Pulling image: %s", named.FullName()))
    82  	}
    83  
    84  	//perform image save ability
    85  	eg, _ := errgroup.WithContext(context.Background())
    86  	numCh := make(chan struct{}, maxPullGoroutineNum)
    87  	for _, nameds := range is.domainToImages {
    88  		tmpnameds := nameds
    89  		numCh <- struct{}{}
    90  		eg.Go(func() error {
    91  			defer func() {
    92  				<-numCh
    93  			}()
    94  			registry, err := NewProxyRegistry(is.ctx, dir, tmpnameds[0].domain)
    95  			if err != nil {
    96  				return fmt.Errorf("init registry error: %v", err)
    97  			}
    98  			err = is.save(tmpnameds, platform, registry)
    99  			if err != nil {
   100  				return fmt.Errorf("save domain %s image error: %v", tmpnameds[0].domain, err)
   101  			}
   102  			return nil
   103  		})
   104  	}
   105  	if err := eg.Wait(); err != nil {
   106  		return err
   107  	}
   108  	if len(images) != 0 {
   109  		progress.Message(is.progressOut, "", "Status: images save success")
   110  	}
   111  	return nil
   112  }
   113  
   114  func (is *DefaultImageSaver) SaveImagesWithAuth(imageList ImageListWithAuth, dir string, platform v1.Platform) error {
   115  	//init a pipe for display pull message
   116  	reader, writer := io.Pipe()
   117  	defer func() {
   118  		_ = reader.Close()
   119  		_ = writer.Close()
   120  	}()
   121  	is.progressOut = streamformatter.NewJSONProgressOutput(writer, false)
   122  	is.ctx = context.Background()
   123  	go func() {
   124  		err := dockerjsonmessage.DisplayJSONMessagesToStream(reader, dockerstreams.NewOut(common.StdOut), nil)
   125  		if err != nil && err != io.ErrClosedPipe {
   126  			logger.Warn("error occurs in display progressing, err: %s", err)
   127  		}
   128  	}()
   129  
   130  	//perform image save ability
   131  	eg, _ := errgroup.WithContext(context.Background())
   132  	numCh := make(chan struct{}, maxPullGoroutineNum)
   133  
   134  	//handle imageList
   135  	for _, section := range imageList {
   136  		for _, nameds := range section.Images {
   137  			tmpnameds := nameds
   138  			progress.Message(is.progressOut, "", fmt.Sprintf("Pulling image: %s", tmpnameds[0].FullName()))
   139  			numCh <- struct{}{}
   140  			eg.Go(func() error {
   141  				defer func() {
   142  					<-numCh
   143  				}()
   144  
   145  				registry, err := NewProxyRegistryWithAuth(is.ctx, section.Username, section.Password, dir, tmpnameds[0].domain)
   146  				if err != nil {
   147  					return fmt.Errorf("init registry error: %v", err)
   148  				}
   149  				err = is.save(tmpnameds, platform, registry)
   150  				if err != nil {
   151  					return fmt.Errorf("save domain %s image error: %v", tmpnameds[0], err)
   152  				}
   153  				return nil
   154  			})
   155  		}
   156  		if err := eg.Wait(); err != nil {
   157  			return err
   158  		}
   159  	}
   160  
   161  	if len(imageList) != 0 {
   162  		progress.Message(is.progressOut, "", "Status: images save success")
   163  	}
   164  	return nil
   165  }
   166  
   167  func (is *DefaultImageSaver) save(nameds []Named, platform v1.Platform, registry distribution.Namespace) error {
   168  	repo, err := is.getRepository(nameds[0], registry)
   169  	if err != nil {
   170  		return err
   171  	}
   172  
   173  	imageDigests, err := is.saveManifestAndGetDigest(nameds, repo, platform)
   174  	if err != nil {
   175  		return err
   176  	}
   177  
   178  	err = is.saveBlobs(imageDigests, repo)
   179  	if err != nil {
   180  		return err
   181  	}
   182  
   183  	return nil
   184  }
   185  
   186  func (is *DefaultImageSaver) getRepository(named Named, registry distribution.Namespace) (distribution.Repository, error) {
   187  	repoName, err := reference.WithName(named.Repo())
   188  	if err != nil {
   189  		return nil, fmt.Errorf("get repository name error: %v", err)
   190  	}
   191  	repo, err := registry.Repository(is.ctx, repoName)
   192  	if err != nil {
   193  		return nil, fmt.Errorf("get repository error: %v", err)
   194  	}
   195  	return repo, nil
   196  }
   197  
   198  func (is *DefaultImageSaver) saveManifestAndGetDigest(nameds []Named, repo distribution.Repository, platform v1.Platform) ([]digest.Digest, error) {
   199  	manifest, err := repo.Manifests(is.ctx, make([]distribution.ManifestServiceOption, 0)...)
   200  	if err != nil {
   201  		return nil, fmt.Errorf("get manifest service error: %v", err)
   202  	}
   203  	eg, _ := errgroup.WithContext(context.Background())
   204  	numCh := make(chan struct{}, maxPullGoroutineNum)
   205  	imageDigests := make([]digest.Digest, 0)
   206  	for _, named := range nameds {
   207  		tmpnamed := named
   208  		numCh <- struct{}{}
   209  		eg.Go(func() error {
   210  			defer func() {
   211  				<-numCh
   212  			}()
   213  
   214  			desc, err := repo.Tags(is.ctx).Get(is.ctx, tmpnamed.tag)
   215  			if err != nil {
   216  				return fmt.Errorf("get %s tag descriptor error: %v, try \"docker login\" if you are using a private registry", tmpnamed.repo, err)
   217  			}
   218  			imageDigest, err := is.handleManifest(manifest, desc.Digest, platform)
   219  			if err != nil {
   220  				return fmt.Errorf("get digest error: %v", err)
   221  			}
   222  			imageDigests = append(imageDigests, imageDigest)
   223  			return nil
   224  		})
   225  	}
   226  	if err := eg.Wait(); err != nil {
   227  		return nil, err
   228  	}
   229  
   230  	return imageDigests, nil
   231  }
   232  
   233  func (is *DefaultImageSaver) handleManifest(manifest distribution.ManifestService, imagedigest digest.Digest, platform v1.Platform) (digest.Digest, error) {
   234  	mani, err := manifest.Get(is.ctx, imagedigest, make([]distribution.ManifestServiceOption, 0)...)
   235  	if err != nil {
   236  		return digest.Digest(""), fmt.Errorf("get image manifest error: %v", err)
   237  	}
   238  	ct, p, err := mani.Payload()
   239  	if err != nil {
   240  		return digest.Digest(""), fmt.Errorf("failed to get image manifest payload: %v", err)
   241  	}
   242  	switch ct {
   243  	case manifestV2, manifestOCI:
   244  		return imagedigest, nil
   245  	case manifestList, manifestOCIIndex:
   246  		imageDigest, err := distributionutil.GetImageManifestDigest(p, platform)
   247  		if err != nil {
   248  			return digest.Digest(""), fmt.Errorf("get digest from manifest list error: %v", err)
   249  		}
   250  		return imageDigest, nil
   251  	case "":
   252  		//OCI image or image index - no media type in the content
   253  		//First see if it is a list
   254  		imageDigest, _ := distributionutil.GetImageManifestDigest(p, platform)
   255  		if imageDigest != "" {
   256  			return imageDigest, nil
   257  		}
   258  		//If not list, then assume it must be an image manifest
   259  		return imagedigest, nil
   260  	default:
   261  		return digest.Digest(""), fmt.Errorf("unrecognized manifest content type")
   262  	}
   263  }
   264  
   265  func (is *DefaultImageSaver) saveBlobs(imageDigests []digest.Digest, repo distribution.Repository) error {
   266  	manifest, err := repo.Manifests(is.ctx, make([]distribution.ManifestServiceOption, 0)...)
   267  	if err != nil {
   268  		return fmt.Errorf("get blob service error: %v", err)
   269  	}
   270  	eg, _ := errgroup.WithContext(context.Background())
   271  	numCh := make(chan struct{}, maxPullGoroutineNum)
   272  	blobLists := make([]digest.Digest, 0)
   273  
   274  	//get blob list
   275  	//each blob identified by a digest
   276  	for _, imageDigest := range imageDigests {
   277  		tmpImageDigest := imageDigest
   278  		numCh <- struct{}{}
   279  		eg.Go(func() error {
   280  			defer func() {
   281  				<-numCh
   282  			}()
   283  
   284  			blobListJSON, err := manifest.Get(is.ctx, tmpImageDigest, make([]distribution.ManifestServiceOption, 0)...)
   285  			if err != nil {
   286  				return err
   287  			}
   288  
   289  			blobList, err := distributionutil.GetBlobList(blobListJSON)
   290  			if err != nil {
   291  				return fmt.Errorf("get blob list error: %v", err)
   292  			}
   293  			blobLists = append(blobLists, blobList...)
   294  			return nil
   295  		})
   296  	}
   297  	if err = eg.Wait(); err != nil {
   298  		return err
   299  	}
   300  
   301  	//pull and save each blob
   302  	blobStore := repo.Blobs(is.ctx)
   303  	for _, blob := range blobLists {
   304  		tmpBlob := blob
   305  		numCh <- struct{}{}
   306  		eg.Go(func() error {
   307  			defer func() {
   308  				<-numCh
   309  			}()
   310  
   311  			simpleDgst := string(tmpBlob)[7:19]
   312  
   313  			_, err = blobStore.Stat(is.ctx, tmpBlob)
   314  			if err == nil { //blob already exist
   315  				progress.Update(is.progressOut, simpleDgst, "already exists")
   316  				return nil
   317  			}
   318  			reader, err := blobStore.Open(is.ctx, tmpBlob)
   319  			if err != nil {
   320  				return fmt.Errorf("get blob %s error: %v", tmpBlob, err)
   321  			}
   322  
   323  			size, err := reader.Seek(0, io.SeekEnd)
   324  			if err != nil {
   325  				return fmt.Errorf("seek end error when save blob %s: %v", tmpBlob, err)
   326  			}
   327  			_, err = reader.Seek(0, io.SeekStart)
   328  			if err != nil {
   329  				return fmt.Errorf("seek start error when save blob %s: %v", tmpBlob, err)
   330  			}
   331  			preader := progress.NewProgressReader(reader, is.progressOut, size, simpleDgst, "Downloading")
   332  
   333  			defer func() {
   334  				_ = reader.Close()
   335  				_ = preader.Close()
   336  				progress.Update(is.progressOut, simpleDgst, "Download complete")
   337  			}()
   338  
   339  			//store to local filesystem
   340  			//content, err := ioutil.ReadAll(preader)
   341  			bf := bufio.NewReader(preader)
   342  			if err != nil {
   343  				return fmt.Errorf("blob %s content error: %v", tmpBlob, err)
   344  			}
   345  			bw, err := blobStore.Create(is.ctx)
   346  			if err != nil {
   347  				return fmt.Errorf("failed to create blob store writer: %v", err)
   348  			}
   349  			if _, err = bf.WriteTo(bw); err != nil {
   350  				return fmt.Errorf("failed to write blob to service: %v", err)
   351  			}
   352  			_, err = bw.Commit(is.ctx, distribution.Descriptor{
   353  				MediaType: "",
   354  				Size:      bw.Size(),
   355  				Digest:    tmpBlob,
   356  			})
   357  			if err != nil {
   358  				return fmt.Errorf("store blob %s to local error: %v", tmpBlob, err)
   359  			}
   360  
   361  			return nil
   362  		})
   363  	}
   364  
   365  	if err := eg.Wait(); err != nil {
   366  		return err
   367  	}
   368  	return nil
   369  }
   370  
   371  func NewProxyRegistryWithAuth(ctx context.Context, username, password, rootdir, domain string) (distribution.Namespace, error) {
   372  	// set the URL of registry
   373  	proxyURL := HTTPS + domain
   374  	if domain == defaultDomain {
   375  		proxyURL = defaultProxyURL
   376  	}
   377  
   378  	config := configuration.Configuration{
   379  		Proxy: configuration.Proxy{
   380  			RemoteURL: proxyURL,
   381  			Username:  username,
   382  			Password:  password,
   383  		},
   384  		Storage: configuration.Storage{
   385  			driverName: configuration.Parameters{configRootDir: rootdir},
   386  		},
   387  	}
   388  	return newProxyRegistry(ctx, config)
   389  }
   390  
   391  func NewProxyRegistry(ctx context.Context, rootdir, domain string) (distribution.Namespace, error) {
   392  	// set the URL of registry
   393  	proxyURL := HTTPS + domain
   394  	if domain == defaultDomain {
   395  		proxyURL = defaultProxyURL
   396  	}
   397  
   398  	var defaultAuth = types.AuthConfig{ServerAddress: domain}
   399  	auth, err := utils.GetDockerAuthInfoFromDocker(domain)
   400  	//ignore err when is there is no username and password.
   401  	//regard it as a public registry
   402  	//only report parse error
   403  	if err != nil && auth != defaultAuth {
   404  		return nil, fmt.Errorf("get authentication info error: %v", err)
   405  	}
   406  
   407  	config := configuration.Configuration{
   408  		Proxy: configuration.Proxy{
   409  			RemoteURL: proxyURL,
   410  			Username:  auth.Username,
   411  			Password:  auth.Password,
   412  		},
   413  		Storage: configuration.Storage{
   414  			driverName: configuration.Parameters{configRootDir: rootdir},
   415  		},
   416  	}
   417  
   418  	return newProxyRegistry(ctx, config)
   419  }
   420  
   421  func newProxyRegistry(ctx context.Context, config configuration.Configuration) (distribution.Namespace, error) {
   422  	driver, err := factory.Create(config.Storage.Type(), config.Storage.Parameters())
   423  	if err != nil {
   424  		return nil, fmt.Errorf("create storage driver error: %v", err)
   425  	}
   426  
   427  	//create a local registry service
   428  	registry, err := storage.NewRegistry(ctx, driver, make([]storage.RegistryOption, 0)...)
   429  	if err != nil {
   430  		return nil, fmt.Errorf("create local registry error: %v", err)
   431  	}
   432  
   433  	proxyRegistry, err := proxy.NewRegistryPullThroughCache(ctx, registry, driver, config.Proxy)
   434  	if err != nil { // try http
   435  		logger.Warn("https error: %v, sealer try to use http", err)
   436  		config.Proxy.RemoteURL = strings.Replace(config.Proxy.RemoteURL, HTTPS, HTTP, 1)
   437  		proxyRegistry, err = proxy.NewRegistryPullThroughCache(ctx, registry, driver, config.Proxy)
   438  		if err != nil {
   439  			return nil, fmt.Errorf("create proxy registry error: %v", err)
   440  		}
   441  	}
   442  	return proxyRegistry, nil
   443  }