mirror of
				https://github.com/go-gitea/gitea
				synced 2025-10-31 19:38:23 +00:00 
			
		
		
		
	Display image size for multiarch container images (#23821)
Fixes #23771 Changes the display of different architectures for multiarch images to show the image size: 
This commit is contained in:
		| @@ -477,6 +477,8 @@ var migrations = []Migration{ | ||||
| 	NewMigration("Add version column to action_runner table", v1_20.AddVersionToActionRunner), | ||||
| 	// v249 -> v250 | ||||
| 	NewMigration("Improve Action table indices v3", v1_20.ImproveActionTableIndices), | ||||
| 	// v250 -> v251 | ||||
| 	NewMigration("Change Container Metadata", v1_20.ChangeContainerMetadataMultiArch), | ||||
| } | ||||
|  | ||||
| // GetCurrentDBVersion returns the current db version | ||||
|   | ||||
							
								
								
									
										135
									
								
								models/migrations/v1_20/v250.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										135
									
								
								models/migrations/v1_20/v250.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,135 @@ | ||||
| // Copyright 2023 The Gitea Authors. All rights reserved. | ||||
| // SPDX-License-Identifier: MIT | ||||
|  | ||||
| package v1_20 //nolint | ||||
|  | ||||
| import ( | ||||
| 	"strings" | ||||
|  | ||||
| 	"code.gitea.io/gitea/modules/json" | ||||
|  | ||||
| 	"xorm.io/xorm" | ||||
| ) | ||||
|  | ||||
| func ChangeContainerMetadataMultiArch(x *xorm.Engine) error { | ||||
| 	sess := x.NewSession() | ||||
| 	defer sess.Close() | ||||
|  | ||||
| 	if err := sess.Begin(); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	type PackageVersion struct { | ||||
| 		ID           int64  `xorm:"pk"` | ||||
| 		MetadataJSON string `xorm:"metadata_json"` | ||||
| 	} | ||||
|  | ||||
| 	type PackageBlob struct{} | ||||
|  | ||||
| 	// Get all relevant packages (manifest list images have a container.manifest.reference property) | ||||
|  | ||||
| 	var pvs []*PackageVersion | ||||
| 	err := sess. | ||||
| 		Table("package_version"). | ||||
| 		Select("id, metadata_json"). | ||||
| 		Where("id IN (SELECT DISTINCT ref_id FROM package_property WHERE ref_type = 0 AND name = 'container.manifest.reference')"). | ||||
| 		Find(&pvs) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	type MetadataOld struct { | ||||
| 		Type             string            `json:"type"` | ||||
| 		IsTagged         bool              `json:"is_tagged"` | ||||
| 		Platform         string            `json:"platform,omitempty"` | ||||
| 		Description      string            `json:"description,omitempty"` | ||||
| 		Authors          []string          `json:"authors,omitempty"` | ||||
| 		Licenses         string            `json:"license,omitempty"` | ||||
| 		ProjectURL       string            `json:"project_url,omitempty"` | ||||
| 		RepositoryURL    string            `json:"repository_url,omitempty"` | ||||
| 		DocumentationURL string            `json:"documentation_url,omitempty"` | ||||
| 		Labels           map[string]string `json:"labels,omitempty"` | ||||
| 		ImageLayers      []string          `json:"layer_creation,omitempty"` | ||||
| 		MultiArch        map[string]string `json:"multiarch,omitempty"` | ||||
| 	} | ||||
|  | ||||
| 	type Manifest struct { | ||||
| 		Platform string `json:"platform"` | ||||
| 		Digest   string `json:"digest"` | ||||
| 		Size     int64  `json:"size"` | ||||
| 	} | ||||
|  | ||||
| 	type MetadataNew struct { | ||||
| 		Type             string            `json:"type"` | ||||
| 		IsTagged         bool              `json:"is_tagged"` | ||||
| 		Platform         string            `json:"platform,omitempty"` | ||||
| 		Description      string            `json:"description,omitempty"` | ||||
| 		Authors          []string          `json:"authors,omitempty"` | ||||
| 		Licenses         string            `json:"license,omitempty"` | ||||
| 		ProjectURL       string            `json:"project_url,omitempty"` | ||||
| 		RepositoryURL    string            `json:"repository_url,omitempty"` | ||||
| 		DocumentationURL string            `json:"documentation_url,omitempty"` | ||||
| 		Labels           map[string]string `json:"labels,omitempty"` | ||||
| 		ImageLayers      []string          `json:"layer_creation,omitempty"` | ||||
| 		Manifests        []*Manifest       `json:"manifests,omitempty"` | ||||
| 	} | ||||
|  | ||||
| 	for _, pv := range pvs { | ||||
| 		var old *MetadataOld | ||||
| 		if err := json.Unmarshal([]byte(pv.MetadataJSON), &old); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 		// Calculate the size of every contained manifest | ||||
|  | ||||
| 		manifests := make([]*Manifest, 0, len(old.MultiArch)) | ||||
| 		for platform, digest := range old.MultiArch { | ||||
| 			size, err := sess. | ||||
| 				Table("package_blob"). | ||||
| 				Join("INNER", "package_file", "package_blob.id = package_file.blob_id"). | ||||
| 				Join("INNER", "package_version pv", "pv.id = package_file.version_id"). | ||||
| 				Join("INNER", "package_version pv2", "pv2.package_id = pv.package_id"). | ||||
| 				Where("pv.lower_version = ? AND pv2.id = ?", strings.ToLower(digest), pv.ID). | ||||
| 				SumInt(new(PackageBlob), "size") | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
|  | ||||
| 			manifests = append(manifests, &Manifest{ | ||||
| 				Platform: platform, | ||||
| 				Digest:   digest, | ||||
| 				Size:     size, | ||||
| 			}) | ||||
| 		} | ||||
|  | ||||
| 		// Convert to new metadata format | ||||
|  | ||||
| 		new := &MetadataNew{ | ||||
| 			Type:             old.Type, | ||||
| 			IsTagged:         old.IsTagged, | ||||
| 			Platform:         old.Platform, | ||||
| 			Description:      old.Description, | ||||
| 			Authors:          old.Authors, | ||||
| 			Licenses:         old.Licenses, | ||||
| 			ProjectURL:       old.ProjectURL, | ||||
| 			RepositoryURL:    old.RepositoryURL, | ||||
| 			DocumentationURL: old.DocumentationURL, | ||||
| 			Labels:           old.Labels, | ||||
| 			ImageLayers:      old.ImageLayers, | ||||
| 			Manifests:        manifests, | ||||
| 		} | ||||
|  | ||||
| 		metadataJSON, err := json.Marshal(new) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 		pv.MetadataJSON = string(metadataJSON) | ||||
|  | ||||
| 		if _, err := sess.ID(pv.ID).Update(pv); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return sess.Commit() | ||||
| } | ||||
| @@ -62,7 +62,13 @@ type Metadata struct { | ||||
| 	DocumentationURL string            `json:"documentation_url,omitempty"` | ||||
| 	Labels           map[string]string `json:"labels,omitempty"` | ||||
| 	ImageLayers      []string          `json:"layer_creation,omitempty"` | ||||
| 	MultiArch        map[string]string `json:"multiarch,omitempty"` | ||||
| 	Manifests        []*Manifest       `json:"manifests,omitempty"` | ||||
| } | ||||
|  | ||||
| type Manifest struct { | ||||
| 	Platform string `json:"platform"` | ||||
| 	Digest   string `json:"digest"` | ||||
| 	Size     int64  `json:"size"` | ||||
| } | ||||
|  | ||||
| // ParseImageConfig parses the metadata of an image config | ||||
|   | ||||
| @@ -46,7 +46,7 @@ func TestParseImageConfig(t *testing.T) { | ||||
| 		}, | ||||
| 		metadata.Labels, | ||||
| 	) | ||||
| 	assert.Empty(t, metadata.MultiArch) | ||||
| 	assert.Empty(t, metadata.Manifests) | ||||
|  | ||||
| 	configHelm := `{"description":"` + description + `", "home": "` + projectURL + `", "sources": ["` + repositoryURL + `"], "maintainers":[{"name":"` + author + `"}]}` | ||||
|  | ||||
|   | ||||
| @@ -217,7 +217,7 @@ func processImageManifestIndex(mci *manifestCreationInfo, buf *packages_module.H | ||||
|  | ||||
| 		metadata := &container_module.Metadata{ | ||||
| 			Type:      container_module.TypeOCI, | ||||
| 			MultiArch: make(map[string]string), | ||||
| 			Manifests: make([]*container_module.Manifest, 0, len(index.Manifests)), | ||||
| 		} | ||||
|  | ||||
| 		for _, manifest := range index.Manifests { | ||||
| @@ -233,7 +233,7 @@ func processImageManifestIndex(mci *manifestCreationInfo, buf *packages_module.H | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			_, err := container_model.GetContainerBlob(ctx, &container_model.BlobSearchOptions{ | ||||
| 			pfd, err := container_model.GetContainerBlob(ctx, &container_model.BlobSearchOptions{ | ||||
| 				OwnerID:    mci.Owner.ID, | ||||
| 				Image:      mci.Image, | ||||
| 				Digest:     string(manifest.Digest), | ||||
| @@ -246,7 +246,18 @@ func processImageManifestIndex(mci *manifestCreationInfo, buf *packages_module.H | ||||
| 				return err | ||||
| 			} | ||||
|  | ||||
| 			metadata.MultiArch[platform] = string(manifest.Digest) | ||||
| 			size, err := packages_model.CalculateFileSize(ctx, &packages_model.PackageFileSearchOptions{ | ||||
| 				VersionID: pfd.File.VersionID, | ||||
| 			}) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
|  | ||||
| 			metadata.Manifests = append(metadata.Manifests, &container_module.Manifest{ | ||||
| 				Platform: platform, | ||||
| 				Digest:   string(manifest.Digest), | ||||
| 				Size:     size, | ||||
| 			}) | ||||
| 		} | ||||
|  | ||||
| 		pv, err := createPackageAndVersion(ctx, mci, metadata) | ||||
| @@ -369,8 +380,8 @@ func createPackageAndVersion(ctx context.Context, mci *manifestCreationInfo, met | ||||
| 			return nil, err | ||||
| 		} | ||||
| 	} | ||||
| 	for _, digest := range metadata.MultiArch { | ||||
| 		if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypeVersion, pv.ID, container_module.PropertyManifestReference, digest); err != nil { | ||||
| 	for _, manifest := range metadata.Manifests { | ||||
| 		if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypeVersion, pv.ID, container_module.PropertyManifestReference, manifest.Digest); err != nil { | ||||
| 			log.Error("Error setting package version property: %v", err) | ||||
| 			return nil, err | ||||
| 		} | ||||
|   | ||||
| @@ -23,19 +23,27 @@ | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	{{if .PackageDescriptor.Metadata.MultiArch}} | ||||
| 	{{if .PackageDescriptor.Metadata.Manifests}} | ||||
| 		<h4 class="ui top attached header">{{.locale.Tr "packages.container.multi_arch"}}</h4> | ||||
| 		<div class="ui attached segment"> | ||||
| 			<div class="ui form"> | ||||
| 			{{range $arch, $digest := .PackageDescriptor.Metadata.MultiArch}} | ||||
| 				<div class="field"> | ||||
| 					<label>{{svg "octicon-terminal"}} {{$arch}}</label> | ||||
| 					{{if eq $.PackageDescriptor.Metadata.Type "oci"}} | ||||
| 					<div class="markup"><pre class="code-block"><code>docker pull {{$.RegistryHost}}/{{$.PackageDescriptor.Owner.LowerName}}/{{$.PackageDescriptor.Package.LowerName}}@{{$digest}}</code></pre></div> | ||||
| 			<table class="ui very basic compact table"> | ||||
| 				<thead> | ||||
| 					<tr> | ||||
| 						<th>{{.locale.Tr "packages.container.digest"}}</th> | ||||
| 						<th>{{.locale.Tr "packages.container.multi_arch"}}</th> | ||||
| 						<th>{{.locale.Tr "admin.packages.size"}}</th> | ||||
| 					</tr> | ||||
| 				</thead> | ||||
| 				<tbody> | ||||
| 					{{range .PackageDescriptor.Metadata.Manifests}} | ||||
| 					<tr> | ||||
| 						<td><a href="{{$.PackageDescriptor.PackageWebLink}}/{{PathEscape .Digest}}">{{.Digest}}</a></td> | ||||
| 						<td>{{.Platform}}</td> | ||||
| 						<td>{{FileSize .Size}}</td> | ||||
| 					</tr> | ||||
| 					{{end}} | ||||
| 				</div> | ||||
| 			{{end}} | ||||
| 			</div> | ||||
| 				</tbody> | ||||
| 			</table> | ||||
| 		</div> | ||||
| 	{{end}} | ||||
| 	{{if .PackageDescriptor.Metadata.Description}} | ||||
|   | ||||
| @@ -62,7 +62,9 @@ | ||||
| 							{{template "package/metadata/rubygems" .}} | ||||
| 							{{template "package/metadata/swift" .}} | ||||
| 							{{template "package/metadata/vagrant" .}} | ||||
| 							{{if not (and (eq .PackageDescriptor.Package.Type "container") .PackageDescriptor.Metadata.Manifests)}} | ||||
| 							<div class="item">{{svg "octicon-database" 16 "gt-mr-3"}} {{FileSize .PackageDescriptor.CalculateBlobSize}}</div> | ||||
| 							{{end}} | ||||
| 						</div> | ||||
| 						{{if not (eq .PackageDescriptor.Package.Type "container")}} | ||||
| 							<div class="ui divider"></div> | ||||
|   | ||||
| @@ -321,7 +321,7 @@ func TestPackageContainer(t *testing.T) { | ||||
| 						metadata := pd.Metadata.(*container_module.Metadata) | ||||
| 						assert.Equal(t, container_module.TypeOCI, metadata.Type) | ||||
| 						assert.Len(t, metadata.ImageLayers, 2) | ||||
| 						assert.Empty(t, metadata.MultiArch) | ||||
| 						assert.Empty(t, metadata.Manifests) | ||||
|  | ||||
| 						assert.Len(t, pd.Files, 3) | ||||
| 						for _, pfd := range pd.Files { | ||||
| @@ -462,10 +462,22 @@ func TestPackageContainer(t *testing.T) { | ||||
| 				assert.IsType(t, &container_module.Metadata{}, pd.Metadata) | ||||
| 				metadata := pd.Metadata.(*container_module.Metadata) | ||||
| 				assert.Equal(t, container_module.TypeOCI, metadata.Type) | ||||
| 				assert.Contains(t, metadata.MultiArch, "linux/arm/v7") | ||||
| 				assert.Equal(t, manifestDigest, metadata.MultiArch["linux/arm/v7"]) | ||||
| 				assert.Contains(t, metadata.MultiArch, "linux/arm64/v8") | ||||
| 				assert.Equal(t, untaggedManifestDigest, metadata.MultiArch["linux/arm64/v8"]) | ||||
| 				assert.Len(t, metadata.Manifests, 2) | ||||
| 				assert.Condition(t, func() bool { | ||||
| 					for _, m := range metadata.Manifests { | ||||
| 						switch m.Platform { | ||||
| 						case "linux/arm/v7": | ||||
| 							assert.Equal(t, manifestDigest, m.Digest) | ||||
| 							assert.EqualValues(t, 1524, m.Size) | ||||
| 						case "linux/arm64/v8": | ||||
| 							assert.Equal(t, untaggedManifestDigest, m.Digest) | ||||
| 							assert.EqualValues(t, 1514, m.Size) | ||||
| 						default: | ||||
| 							return false | ||||
| 						} | ||||
| 					} | ||||
| 					return true | ||||
| 				}) | ||||
|  | ||||
| 				assert.Len(t, pd.Files, 1) | ||||
| 				assert.True(t, pd.Files[0].File.IsLead) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user