code.gitea.io/gitea@v1.21.7/routers/api/packages/container/manifest.go (about)

     1  // Copyright 2022 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package container
     5  
     6  import (
     7  	"context"
     8  	"errors"
     9  	"fmt"
    10  	"io"
    11  	"os"
    12  	"strings"
    13  
    14  	"code.gitea.io/gitea/models/db"
    15  	packages_model "code.gitea.io/gitea/models/packages"
    16  	container_model "code.gitea.io/gitea/models/packages/container"
    17  	user_model "code.gitea.io/gitea/models/user"
    18  	"code.gitea.io/gitea/modules/json"
    19  	"code.gitea.io/gitea/modules/log"
    20  	packages_module "code.gitea.io/gitea/modules/packages"
    21  	container_module "code.gitea.io/gitea/modules/packages/container"
    22  	"code.gitea.io/gitea/modules/util"
    23  	notify_service "code.gitea.io/gitea/services/notify"
    24  	packages_service "code.gitea.io/gitea/services/packages"
    25  
    26  	digest "github.com/opencontainers/go-digest"
    27  	oci "github.com/opencontainers/image-spec/specs-go/v1"
    28  )
    29  
    30  func isValidMediaType(mt string) bool {
    31  	return strings.HasPrefix(mt, "application/vnd.docker.") || strings.HasPrefix(mt, "application/vnd.oci.")
    32  }
    33  
    34  func isImageManifestMediaType(mt string) bool {
    35  	return strings.EqualFold(mt, oci.MediaTypeImageManifest) || strings.EqualFold(mt, "application/vnd.docker.distribution.manifest.v2+json")
    36  }
    37  
    38  func isImageIndexMediaType(mt string) bool {
    39  	return strings.EqualFold(mt, oci.MediaTypeImageIndex) || strings.EqualFold(mt, "application/vnd.docker.distribution.manifest.list.v2+json")
    40  }
    41  
    42  // manifestCreationInfo describes a manifest to create
    43  type manifestCreationInfo struct {
    44  	MediaType  string
    45  	Owner      *user_model.User
    46  	Creator    *user_model.User
    47  	Image      string
    48  	Reference  string
    49  	IsTagged   bool
    50  	Properties map[string]string
    51  }
    52  
    53  func processManifest(ctx context.Context, mci *manifestCreationInfo, buf *packages_module.HashedBuffer) (string, error) {
    54  	var index oci.Index
    55  	if err := json.NewDecoder(buf).Decode(&index); err != nil {
    56  		return "", err
    57  	}
    58  
    59  	if index.SchemaVersion != 2 {
    60  		return "", errUnsupported.WithMessage("Schema version is not supported")
    61  	}
    62  
    63  	if _, err := buf.Seek(0, io.SeekStart); err != nil {
    64  		return "", err
    65  	}
    66  
    67  	if !isValidMediaType(mci.MediaType) {
    68  		mci.MediaType = index.MediaType
    69  		if !isValidMediaType(mci.MediaType) {
    70  			return "", errManifestInvalid.WithMessage("MediaType not recognized")
    71  		}
    72  	}
    73  
    74  	if isImageManifestMediaType(mci.MediaType) {
    75  		return processImageManifest(ctx, mci, buf)
    76  	} else if isImageIndexMediaType(mci.MediaType) {
    77  		return processImageManifestIndex(ctx, mci, buf)
    78  	}
    79  	return "", errManifestInvalid
    80  }
    81  
    82  func processImageManifest(ctx context.Context, mci *manifestCreationInfo, buf *packages_module.HashedBuffer) (string, error) {
    83  	manifestDigest := ""
    84  
    85  	err := func() error {
    86  		var manifest oci.Manifest
    87  		if err := json.NewDecoder(buf).Decode(&manifest); err != nil {
    88  			return err
    89  		}
    90  
    91  		if _, err := buf.Seek(0, io.SeekStart); err != nil {
    92  			return err
    93  		}
    94  
    95  		ctx, committer, err := db.TxContext(ctx)
    96  		if err != nil {
    97  			return err
    98  		}
    99  		defer committer.Close()
   100  
   101  		configDescriptor, err := container_model.GetContainerBlob(ctx, &container_model.BlobSearchOptions{
   102  			OwnerID: mci.Owner.ID,
   103  			Image:   mci.Image,
   104  			Digest:  string(manifest.Config.Digest),
   105  		})
   106  		if err != nil {
   107  			return err
   108  		}
   109  
   110  		configReader, err := packages_module.NewContentStore().Get(packages_module.BlobHash256Key(configDescriptor.Blob.HashSHA256))
   111  		if err != nil {
   112  			return err
   113  		}
   114  		defer configReader.Close()
   115  
   116  		metadata, err := container_module.ParseImageConfig(manifest.Config.MediaType, configReader)
   117  		if err != nil {
   118  			return err
   119  		}
   120  
   121  		blobReferences := make([]*blobReference, 0, 1+len(manifest.Layers))
   122  
   123  		blobReferences = append(blobReferences, &blobReference{
   124  			Digest:       manifest.Config.Digest,
   125  			MediaType:    manifest.Config.MediaType,
   126  			File:         configDescriptor,
   127  			ExpectedSize: manifest.Config.Size,
   128  		})
   129  
   130  		for _, layer := range manifest.Layers {
   131  			pfd, err := container_model.GetContainerBlob(ctx, &container_model.BlobSearchOptions{
   132  				OwnerID: mci.Owner.ID,
   133  				Image:   mci.Image,
   134  				Digest:  string(layer.Digest),
   135  			})
   136  			if err != nil {
   137  				return err
   138  			}
   139  
   140  			blobReferences = append(blobReferences, &blobReference{
   141  				Digest:       layer.Digest,
   142  				MediaType:    layer.MediaType,
   143  				File:         pfd,
   144  				ExpectedSize: layer.Size,
   145  			})
   146  		}
   147  
   148  		pv, err := createPackageAndVersion(ctx, mci, metadata)
   149  		if err != nil {
   150  			return err
   151  		}
   152  
   153  		uploadVersion, err := packages_model.GetInternalVersionByNameAndVersion(ctx, mci.Owner.ID, packages_model.TypeContainer, mci.Image, container_model.UploadVersion)
   154  		if err != nil && err != packages_model.ErrPackageNotExist {
   155  			return err
   156  		}
   157  
   158  		for _, ref := range blobReferences {
   159  			if err := createFileFromBlobReference(ctx, pv, uploadVersion, ref); err != nil {
   160  				return err
   161  			}
   162  		}
   163  
   164  		pb, created, digest, err := createManifestBlob(ctx, mci, pv, buf)
   165  		removeBlob := false
   166  		defer func() {
   167  			if removeBlob {
   168  				contentStore := packages_module.NewContentStore()
   169  				if err := contentStore.Delete(packages_module.BlobHash256Key(pb.HashSHA256)); err != nil {
   170  					log.Error("Error deleting package blob from content store: %v", err)
   171  				}
   172  			}
   173  		}()
   174  		if err != nil {
   175  			removeBlob = created
   176  			return err
   177  		}
   178  
   179  		if err := committer.Commit(); err != nil {
   180  			removeBlob = created
   181  			return err
   182  		}
   183  
   184  		if err := notifyPackageCreate(ctx, mci.Creator, pv); err != nil {
   185  			return err
   186  		}
   187  
   188  		manifestDigest = digest
   189  
   190  		return nil
   191  	}()
   192  	if err != nil {
   193  		return "", err
   194  	}
   195  
   196  	return manifestDigest, nil
   197  }
   198  
   199  func processImageManifestIndex(ctx context.Context, mci *manifestCreationInfo, buf *packages_module.HashedBuffer) (string, error) {
   200  	manifestDigest := ""
   201  
   202  	err := func() error {
   203  		var index oci.Index
   204  		if err := json.NewDecoder(buf).Decode(&index); err != nil {
   205  			return err
   206  		}
   207  
   208  		if _, err := buf.Seek(0, io.SeekStart); err != nil {
   209  			return err
   210  		}
   211  
   212  		ctx, committer, err := db.TxContext(ctx)
   213  		if err != nil {
   214  			return err
   215  		}
   216  		defer committer.Close()
   217  
   218  		metadata := &container_module.Metadata{
   219  			Type:      container_module.TypeOCI,
   220  			Manifests: make([]*container_module.Manifest, 0, len(index.Manifests)),
   221  		}
   222  
   223  		for _, manifest := range index.Manifests {
   224  			if !isImageManifestMediaType(manifest.MediaType) {
   225  				return errManifestInvalid
   226  			}
   227  
   228  			platform := container_module.DefaultPlatform
   229  			if manifest.Platform != nil {
   230  				platform = fmt.Sprintf("%s/%s", manifest.Platform.OS, manifest.Platform.Architecture)
   231  				if manifest.Platform.Variant != "" {
   232  					platform = fmt.Sprintf("%s/%s", platform, manifest.Platform.Variant)
   233  				}
   234  			}
   235  
   236  			pfd, err := container_model.GetContainerBlob(ctx, &container_model.BlobSearchOptions{
   237  				OwnerID:    mci.Owner.ID,
   238  				Image:      mci.Image,
   239  				Digest:     string(manifest.Digest),
   240  				IsManifest: true,
   241  			})
   242  			if err != nil {
   243  				if err == container_model.ErrContainerBlobNotExist {
   244  					return errManifestBlobUnknown
   245  				}
   246  				return err
   247  			}
   248  
   249  			size, err := packages_model.CalculateFileSize(ctx, &packages_model.PackageFileSearchOptions{
   250  				VersionID: pfd.File.VersionID,
   251  			})
   252  			if err != nil {
   253  				return err
   254  			}
   255  
   256  			metadata.Manifests = append(metadata.Manifests, &container_module.Manifest{
   257  				Platform: platform,
   258  				Digest:   string(manifest.Digest),
   259  				Size:     size,
   260  			})
   261  		}
   262  
   263  		pv, err := createPackageAndVersion(ctx, mci, metadata)
   264  		if err != nil {
   265  			return err
   266  		}
   267  
   268  		pb, created, digest, err := createManifestBlob(ctx, mci, pv, buf)
   269  		removeBlob := false
   270  		defer func() {
   271  			if removeBlob {
   272  				contentStore := packages_module.NewContentStore()
   273  				if err := contentStore.Delete(packages_module.BlobHash256Key(pb.HashSHA256)); err != nil {
   274  					log.Error("Error deleting package blob from content store: %v", err)
   275  				}
   276  			}
   277  		}()
   278  		if err != nil {
   279  			removeBlob = created
   280  			return err
   281  		}
   282  
   283  		if err := committer.Commit(); err != nil {
   284  			removeBlob = created
   285  			return err
   286  		}
   287  
   288  		if err := notifyPackageCreate(ctx, mci.Creator, pv); err != nil {
   289  			return err
   290  		}
   291  
   292  		manifestDigest = digest
   293  
   294  		return nil
   295  	}()
   296  	if err != nil {
   297  		return "", err
   298  	}
   299  
   300  	return manifestDigest, nil
   301  }
   302  
   303  func notifyPackageCreate(ctx context.Context, doer *user_model.User, pv *packages_model.PackageVersion) error {
   304  	pd, err := packages_model.GetPackageDescriptor(ctx, pv)
   305  	if err != nil {
   306  		return err
   307  	}
   308  
   309  	notify_service.PackageCreate(ctx, doer, pd)
   310  
   311  	return nil
   312  }
   313  
   314  func createPackageAndVersion(ctx context.Context, mci *manifestCreationInfo, metadata *container_module.Metadata) (*packages_model.PackageVersion, error) {
   315  	created := true
   316  	p := &packages_model.Package{
   317  		OwnerID:   mci.Owner.ID,
   318  		Type:      packages_model.TypeContainer,
   319  		Name:      strings.ToLower(mci.Image),
   320  		LowerName: strings.ToLower(mci.Image),
   321  	}
   322  	var err error
   323  	if p, err = packages_model.TryInsertPackage(ctx, p); err != nil {
   324  		if err == packages_model.ErrDuplicatePackage {
   325  			created = false
   326  		} else {
   327  			log.Error("Error inserting package: %v", err)
   328  			return nil, err
   329  		}
   330  	}
   331  
   332  	if created {
   333  		if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypePackage, p.ID, container_module.PropertyRepository, strings.ToLower(mci.Owner.LowerName+"/"+mci.Image)); err != nil {
   334  			log.Error("Error setting package property: %v", err)
   335  			return nil, err
   336  		}
   337  	}
   338  
   339  	metadata.IsTagged = mci.IsTagged
   340  
   341  	metadataJSON, err := json.Marshal(metadata)
   342  	if err != nil {
   343  		return nil, err
   344  	}
   345  
   346  	_pv := &packages_model.PackageVersion{
   347  		PackageID:    p.ID,
   348  		CreatorID:    mci.Creator.ID,
   349  		Version:      strings.ToLower(mci.Reference),
   350  		LowerVersion: strings.ToLower(mci.Reference),
   351  		MetadataJSON: string(metadataJSON),
   352  	}
   353  	var pv *packages_model.PackageVersion
   354  	if pv, err = packages_model.GetOrInsertVersion(ctx, _pv); err != nil {
   355  		if err == packages_model.ErrDuplicatePackageVersion {
   356  			if err := packages_service.DeletePackageVersionAndReferences(ctx, pv); err != nil {
   357  				return nil, err
   358  			}
   359  
   360  			// keep download count on overwrite
   361  			_pv.DownloadCount = pv.DownloadCount
   362  
   363  			if pv, err = packages_model.GetOrInsertVersion(ctx, _pv); err != nil {
   364  				log.Error("Error inserting package: %v", err)
   365  				return nil, err
   366  			}
   367  		} else {
   368  			log.Error("Error inserting package: %v", err)
   369  			return nil, err
   370  		}
   371  	}
   372  
   373  	if err := packages_service.CheckCountQuotaExceeded(ctx, mci.Creator, mci.Owner); err != nil {
   374  		return nil, err
   375  	}
   376  
   377  	if mci.IsTagged {
   378  		if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypeVersion, pv.ID, container_module.PropertyManifestTagged, ""); err != nil {
   379  			log.Error("Error setting package version property: %v", err)
   380  			return nil, err
   381  		}
   382  	}
   383  	for _, manifest := range metadata.Manifests {
   384  		if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypeVersion, pv.ID, container_module.PropertyManifestReference, manifest.Digest); err != nil {
   385  			log.Error("Error setting package version property: %v", err)
   386  			return nil, err
   387  		}
   388  	}
   389  
   390  	return pv, nil
   391  }
   392  
   393  type blobReference struct {
   394  	Digest       digest.Digest
   395  	MediaType    string
   396  	Name         string
   397  	File         *packages_model.PackageFileDescriptor
   398  	ExpectedSize int64
   399  	IsLead       bool
   400  }
   401  
   402  func createFileFromBlobReference(ctx context.Context, pv, uploadVersion *packages_model.PackageVersion, ref *blobReference) error {
   403  	if ref.File.Blob.Size != ref.ExpectedSize {
   404  		return errSizeInvalid
   405  	}
   406  
   407  	if ref.Name == "" {
   408  		ref.Name = strings.ToLower(fmt.Sprintf("sha256_%s", ref.File.Blob.HashSHA256))
   409  	}
   410  
   411  	pf := &packages_model.PackageFile{
   412  		VersionID: pv.ID,
   413  		BlobID:    ref.File.Blob.ID,
   414  		Name:      ref.Name,
   415  		LowerName: ref.Name,
   416  		IsLead:    ref.IsLead,
   417  	}
   418  	var err error
   419  	if pf, err = packages_model.TryInsertFile(ctx, pf); err != nil {
   420  		if err == packages_model.ErrDuplicatePackageFile {
   421  			// Skip this blob because the manifest contains the same filesystem layer multiple times.
   422  			return nil
   423  		}
   424  		log.Error("Error inserting package file: %v", err)
   425  		return err
   426  	}
   427  
   428  	props := map[string]string{
   429  		container_module.PropertyMediaType: ref.MediaType,
   430  		container_module.PropertyDigest:    string(ref.Digest),
   431  	}
   432  	for name, value := range props {
   433  		if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypeFile, pf.ID, name, value); err != nil {
   434  			log.Error("Error setting package file property: %v", err)
   435  			return err
   436  		}
   437  	}
   438  
   439  	// Remove the file from the blob upload version
   440  	if uploadVersion != nil && ref.File.File != nil && uploadVersion.ID == ref.File.File.VersionID {
   441  		if err := packages_service.DeletePackageFile(ctx, ref.File.File); err != nil {
   442  			return err
   443  		}
   444  	}
   445  
   446  	return nil
   447  }
   448  
   449  func createManifestBlob(ctx context.Context, mci *manifestCreationInfo, pv *packages_model.PackageVersion, buf *packages_module.HashedBuffer) (*packages_model.PackageBlob, bool, string, error) {
   450  	pb, exists, err := packages_model.GetOrInsertBlob(ctx, packages_service.NewPackageBlob(buf))
   451  	if err != nil {
   452  		log.Error("Error inserting package blob: %v", err)
   453  		return nil, false, "", err
   454  	}
   455  	// FIXME: Workaround to be removed in v1.20
   456  	// https://github.com/go-gitea/gitea/issues/19586
   457  	if exists {
   458  		err = packages_module.NewContentStore().Has(packages_module.BlobHash256Key(pb.HashSHA256))
   459  		if err != nil && (errors.Is(err, util.ErrNotExist) || errors.Is(err, os.ErrNotExist)) {
   460  			log.Debug("Package registry inconsistent: blob %s does not exist on file system", pb.HashSHA256)
   461  			exists = false
   462  		}
   463  	}
   464  	if !exists {
   465  		contentStore := packages_module.NewContentStore()
   466  		if err := contentStore.Save(packages_module.BlobHash256Key(pb.HashSHA256), buf, buf.Size()); err != nil {
   467  			log.Error("Error saving package blob in content store: %v", err)
   468  			return nil, false, "", err
   469  		}
   470  	}
   471  
   472  	manifestDigest := digestFromHashSummer(buf)
   473  	err = createFileFromBlobReference(ctx, pv, nil, &blobReference{
   474  		Digest:       digest.Digest(manifestDigest),
   475  		MediaType:    mci.MediaType,
   476  		Name:         container_model.ManifestFilename,
   477  		File:         &packages_model.PackageFileDescriptor{Blob: pb},
   478  		ExpectedSize: pb.Size,
   479  		IsLead:       true,
   480  	})
   481  
   482  	return pb, !exists, manifestDigest, err
   483  }