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 }