github.com/buildpacks/pack@v0.33.3-0.20240516162812-884dd1837311/pkg/image/fetcher.go (about)

     1  package image
     2  
     3  import (
     4  	"context"
     5  	"encoding/base64"
     6  	"encoding/json"
     7  	"io"
     8  	"strings"
     9  
    10  	"github.com/buildpacks/imgutil/layout"
    11  	"github.com/buildpacks/imgutil/layout/sparse"
    12  
    13  	"github.com/buildpacks/imgutil"
    14  	"github.com/buildpacks/imgutil/local"
    15  	"github.com/buildpacks/imgutil/remote"
    16  	"github.com/buildpacks/lifecycle/auth"
    17  	"github.com/docker/docker/api/types/image"
    18  	"github.com/docker/docker/client"
    19  	"github.com/docker/docker/pkg/jsonmessage"
    20  	"github.com/google/go-containerregistry/pkg/authn"
    21  	"github.com/pkg/errors"
    22  
    23  	pname "github.com/buildpacks/pack/internal/name"
    24  	"github.com/buildpacks/pack/internal/style"
    25  	"github.com/buildpacks/pack/internal/term"
    26  	"github.com/buildpacks/pack/pkg/logging"
    27  )
    28  
    29  // FetcherOption is a type of function that mutate settings on the client.
    30  // Values in these functions are set through currying.
    31  type FetcherOption func(c *Fetcher)
    32  
    33  type LayoutOption struct {
    34  	Path   string
    35  	Sparse bool
    36  }
    37  
    38  // WithRegistryMirrors supply your own mirrors for registry.
    39  func WithRegistryMirrors(registryMirrors map[string]string) FetcherOption {
    40  	return func(c *Fetcher) {
    41  		c.registryMirrors = registryMirrors
    42  	}
    43  }
    44  
    45  func WithKeychain(keychain authn.Keychain) FetcherOption {
    46  	return func(c *Fetcher) {
    47  		c.keychain = keychain
    48  	}
    49  }
    50  
    51  type DockerClient interface {
    52  	local.DockerClient
    53  	ImagePull(ctx context.Context, ref string, options image.PullOptions) (io.ReadCloser, error)
    54  }
    55  
    56  type Fetcher struct {
    57  	docker          DockerClient
    58  	logger          logging.Logger
    59  	registryMirrors map[string]string
    60  	keychain        authn.Keychain
    61  }
    62  
    63  type FetchOptions struct {
    64  	Daemon       bool
    65  	Platform     string
    66  	PullPolicy   PullPolicy
    67  	LayoutOption LayoutOption
    68  }
    69  
    70  func NewFetcher(logger logging.Logger, docker DockerClient, opts ...FetcherOption) *Fetcher {
    71  	fetcher := &Fetcher{
    72  		logger:   logger,
    73  		docker:   docker,
    74  		keychain: authn.DefaultKeychain,
    75  	}
    76  
    77  	for _, opt := range opts {
    78  		opt(fetcher)
    79  	}
    80  
    81  	return fetcher
    82  }
    83  
    84  var ErrNotFound = errors.New("not found")
    85  
    86  func (f *Fetcher) Fetch(ctx context.Context, name string, options FetchOptions) (imgutil.Image, error) {
    87  	name, err := pname.TranslateRegistry(name, f.registryMirrors, f.logger)
    88  	if err != nil {
    89  		return nil, err
    90  	}
    91  
    92  	if (options.LayoutOption != LayoutOption{}) {
    93  		return f.fetchLayoutImage(name, options.LayoutOption)
    94  	}
    95  
    96  	if !options.Daemon {
    97  		return f.fetchRemoteImage(name)
    98  	}
    99  
   100  	switch options.PullPolicy {
   101  	case PullNever:
   102  		img, err := f.fetchDaemonImage(name)
   103  		return img, err
   104  	case PullIfNotPresent:
   105  		img, err := f.fetchDaemonImage(name)
   106  		if err == nil || !errors.Is(err, ErrNotFound) {
   107  			return img, err
   108  		}
   109  	}
   110  
   111  	f.logger.Debugf("Pulling image %s", style.Symbol(name))
   112  	if err = f.pullImage(ctx, name, options.Platform); err != nil {
   113  		// sample error from docker engine:
   114  		// image with reference <image> was found but does not match the specified platform: wanted linux/amd64, actual: linux
   115  		if strings.Contains(err.Error(), "does not match the specified platform") {
   116  			err = f.pullImage(ctx, name, "")
   117  		}
   118  	}
   119  	if err != nil && !errors.Is(err, ErrNotFound) {
   120  		return nil, err
   121  	}
   122  
   123  	return f.fetchDaemonImage(name)
   124  }
   125  
   126  func (f *Fetcher) CheckReadAccess(repo string, options FetchOptions) bool {
   127  	if !options.Daemon || options.PullPolicy == PullAlways {
   128  		return f.checkRemoteReadAccess(repo)
   129  	}
   130  	if _, err := f.fetchDaemonImage(repo); err != nil {
   131  		if errors.Is(err, ErrNotFound) {
   132  			// Image doesn't exist in the daemon
   133  			// 	Pull Never: should fail
   134  			// 	Pull If Not Present: need to check the registry
   135  			if options.PullPolicy == PullNever {
   136  				return false
   137  			}
   138  			return f.checkRemoteReadAccess(repo)
   139  		}
   140  		f.logger.Debugf("failed reading image '%s' from the daemon, error: %s", repo, err.Error())
   141  		return false
   142  	}
   143  	return true
   144  }
   145  
   146  func (f *Fetcher) checkRemoteReadAccess(repo string) bool {
   147  	img, err := remote.NewImage(repo, f.keychain)
   148  	if err != nil {
   149  		f.logger.Debugf("failed accessing remote image %s, error: %s", repo, err.Error())
   150  		return false
   151  	}
   152  	if ok, err := img.CheckReadAccess(); ok {
   153  		f.logger.Debugf("CheckReadAccess succeeded for the run image %s", repo)
   154  		return true
   155  	} else {
   156  		f.logger.Debugf("CheckReadAccess failed for the run image %s, error: %s", repo, err.Error())
   157  		return false
   158  	}
   159  }
   160  
   161  func (f *Fetcher) fetchDaemonImage(name string) (imgutil.Image, error) {
   162  	image, err := local.NewImage(name, f.docker, local.FromBaseImage(name))
   163  	if err != nil {
   164  		return nil, err
   165  	}
   166  
   167  	if !image.Found() {
   168  		return nil, errors.Wrapf(ErrNotFound, "image %s does not exist on the daemon", style.Symbol(name))
   169  	}
   170  
   171  	return image, nil
   172  }
   173  
   174  func (f *Fetcher) fetchRemoteImage(name string) (imgutil.Image, error) {
   175  	image, err := remote.NewImage(name, f.keychain, remote.FromBaseImage(name))
   176  	if err != nil {
   177  		return nil, err
   178  	}
   179  
   180  	if !image.Found() {
   181  		return nil, errors.Wrapf(ErrNotFound, "image %s does not exist in registry", style.Symbol(name))
   182  	}
   183  
   184  	return image, nil
   185  }
   186  
   187  func (f *Fetcher) fetchLayoutImage(name string, options LayoutOption) (imgutil.Image, error) {
   188  	var (
   189  		image imgutil.Image
   190  		err   error
   191  	)
   192  
   193  	v1Image, err := remote.NewV1Image(name, f.keychain)
   194  	if err != nil {
   195  		return nil, err
   196  	}
   197  
   198  	if options.Sparse {
   199  		image, err = sparse.NewImage(options.Path, v1Image)
   200  	} else {
   201  		image, err = layout.NewImage(options.Path, layout.FromBaseImageInstance(v1Image))
   202  	}
   203  
   204  	if err != nil {
   205  		return nil, err
   206  	}
   207  
   208  	err = image.Save()
   209  	if err != nil {
   210  		return nil, err
   211  	}
   212  
   213  	return image, nil
   214  }
   215  
   216  func (f *Fetcher) pullImage(ctx context.Context, imageID string, platform string) error {
   217  	regAuth, err := f.registryAuth(imageID)
   218  	if err != nil {
   219  		return err
   220  	}
   221  
   222  	rc, err := f.docker.ImagePull(ctx, imageID, image.PullOptions{RegistryAuth: regAuth, Platform: platform})
   223  	if err != nil {
   224  		if client.IsErrNotFound(err) {
   225  			return errors.Wrapf(ErrNotFound, "image %s does not exist on the daemon", style.Symbol(imageID))
   226  		}
   227  
   228  		return err
   229  	}
   230  
   231  	writer := logging.GetWriterForLevel(f.logger, logging.InfoLevel)
   232  	termFd, isTerm := term.IsTerminal(writer)
   233  
   234  	err = jsonmessage.DisplayJSONMessagesStream(rc, &colorizedWriter{writer}, termFd, isTerm, nil)
   235  	if err != nil {
   236  		return err
   237  	}
   238  
   239  	return rc.Close()
   240  }
   241  
   242  func (f *Fetcher) registryAuth(ref string) (string, error) {
   243  	_, a, err := auth.ReferenceForRepoName(f.keychain, ref)
   244  	if err != nil {
   245  		return "", errors.Wrapf(err, "resolve auth for ref %s", ref)
   246  	}
   247  	authConfig, err := a.Authorization()
   248  	if err != nil {
   249  		return "", err
   250  	}
   251  
   252  	dataJSON, err := json.Marshal(authConfig)
   253  	if err != nil {
   254  		return "", err
   255  	}
   256  
   257  	return base64.StdEncoding.EncodeToString(dataJSON), nil
   258  }
   259  
   260  type colorizedWriter struct {
   261  	writer io.Writer
   262  }
   263  
   264  type colorFunc = func(string, ...interface{}) string
   265  
   266  func (w *colorizedWriter) Write(p []byte) (n int, err error) {
   267  	msg := string(p)
   268  	colorizers := map[string]colorFunc{
   269  		"Waiting":           style.Waiting,
   270  		"Pulling fs layer":  style.Waiting,
   271  		"Downloading":       style.Working,
   272  		"Download complete": style.Working,
   273  		"Extracting":        style.Working,
   274  		"Pull complete":     style.Complete,
   275  		"Already exists":    style.Complete,
   276  		"=":                 style.ProgressBar,
   277  		">":                 style.ProgressBar,
   278  	}
   279  	for pattern, colorize := range colorizers {
   280  		msg = strings.ReplaceAll(msg, pattern, colorize(pattern))
   281  	}
   282  	return w.writer.Write([]byte(msg))
   283  }