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 }