forked from NYANDEV/forgejo
Display image size for multiarch container images (#23821)
Fixes #23771 Changes the display of different architectures for multiarch images to show the image size: ![grafik](https://user-images.githubusercontent.com/1666336/228781477-cc76c4d1-4728-434f-8a27-fc008790d924.png)
This commit is contained in:
parent
f5593d08dc
commit
fbd4eaceed
8 changed files with 198 additions and 22 deletions
|
@ -477,6 +477,8 @@ var migrations = []Migration{
|
||||||
NewMigration("Add version column to action_runner table", v1_20.AddVersionToActionRunner),
|
NewMigration("Add version column to action_runner table", v1_20.AddVersionToActionRunner),
|
||||||
// v249 -> v250
|
// v249 -> v250
|
||||||
NewMigration("Improve Action table indices v3", v1_20.ImproveActionTableIndices),
|
NewMigration("Improve Action table indices v3", v1_20.ImproveActionTableIndices),
|
||||||
|
// v250 -> v251
|
||||||
|
NewMigration("Change Container Metadata", v1_20.ChangeContainerMetadataMultiArch),
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCurrentDBVersion returns the current db version
|
// 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"`
|
DocumentationURL string `json:"documentation_url,omitempty"`
|
||||||
Labels map[string]string `json:"labels,omitempty"`
|
Labels map[string]string `json:"labels,omitempty"`
|
||||||
ImageLayers []string `json:"layer_creation,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
|
// ParseImageConfig parses the metadata of an image config
|
||||||
|
|
|
@ -46,7 +46,7 @@ func TestParseImageConfig(t *testing.T) {
|
||||||
},
|
},
|
||||||
metadata.Labels,
|
metadata.Labels,
|
||||||
)
|
)
|
||||||
assert.Empty(t, metadata.MultiArch)
|
assert.Empty(t, metadata.Manifests)
|
||||||
|
|
||||||
configHelm := `{"description":"` + description + `", "home": "` + projectURL + `", "sources": ["` + repositoryURL + `"], "maintainers":[{"name":"` + author + `"}]}`
|
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{
|
metadata := &container_module.Metadata{
|
||||||
Type: container_module.TypeOCI,
|
Type: container_module.TypeOCI,
|
||||||
MultiArch: make(map[string]string),
|
Manifests: make([]*container_module.Manifest, 0, len(index.Manifests)),
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, manifest := range 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,
|
OwnerID: mci.Owner.ID,
|
||||||
Image: mci.Image,
|
Image: mci.Image,
|
||||||
Digest: string(manifest.Digest),
|
Digest: string(manifest.Digest),
|
||||||
|
@ -246,7 +246,18 @@ func processImageManifestIndex(mci *manifestCreationInfo, buf *packages_module.H
|
||||||
return err
|
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)
|
pv, err := createPackageAndVersion(ctx, mci, metadata)
|
||||||
|
@ -369,8 +380,8 @@ func createPackageAndVersion(ctx context.Context, mci *manifestCreationInfo, met
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, digest := range metadata.MultiArch {
|
for _, manifest := range metadata.Manifests {
|
||||||
if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypeVersion, pv.ID, container_module.PropertyManifestReference, digest); err != nil {
|
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)
|
log.Error("Error setting package version property: %v", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,19 +23,27 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{if .PackageDescriptor.Metadata.MultiArch}}
|
{{if .PackageDescriptor.Metadata.Manifests}}
|
||||||
<h4 class="ui top attached header">{{.locale.Tr "packages.container.multi_arch"}}</h4>
|
<h4 class="ui top attached header">{{.locale.Tr "packages.container.multi_arch"}}</h4>
|
||||||
<div class="ui attached segment">
|
<div class="ui attached segment">
|
||||||
<div class="ui form">
|
<table class="ui very basic compact table">
|
||||||
{{range $arch, $digest := .PackageDescriptor.Metadata.MultiArch}}
|
<thead>
|
||||||
<div class="field">
|
<tr>
|
||||||
<label>{{svg "octicon-terminal"}} {{$arch}}</label>
|
<th>{{.locale.Tr "packages.container.digest"}}</th>
|
||||||
{{if eq $.PackageDescriptor.Metadata.Type "oci"}}
|
<th>{{.locale.Tr "packages.container.multi_arch"}}</th>
|
||||||
<div class="markup"><pre class="code-block"><code>docker pull {{$.RegistryHost}}/{{$.PackageDescriptor.Owner.LowerName}}/{{$.PackageDescriptor.Package.LowerName}}@{{$digest}}</code></pre></div>
|
<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}}
|
{{end}}
|
||||||
</div>
|
</tbody>
|
||||||
{{end}}
|
</table>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if .PackageDescriptor.Metadata.Description}}
|
{{if .PackageDescriptor.Metadata.Description}}
|
||||||
|
|
|
@ -62,7 +62,9 @@
|
||||||
{{template "package/metadata/rubygems" .}}
|
{{template "package/metadata/rubygems" .}}
|
||||||
{{template "package/metadata/swift" .}}
|
{{template "package/metadata/swift" .}}
|
||||||
{{template "package/metadata/vagrant" .}}
|
{{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>
|
<div class="item">{{svg "octicon-database" 16 "gt-mr-3"}} {{FileSize .PackageDescriptor.CalculateBlobSize}}</div>
|
||||||
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{if not (eq .PackageDescriptor.Package.Type "container")}}
|
{{if not (eq .PackageDescriptor.Package.Type "container")}}
|
||||||
<div class="ui divider"></div>
|
<div class="ui divider"></div>
|
||||||
|
|
|
@ -321,7 +321,7 @@ func TestPackageContainer(t *testing.T) {
|
||||||
metadata := pd.Metadata.(*container_module.Metadata)
|
metadata := pd.Metadata.(*container_module.Metadata)
|
||||||
assert.Equal(t, container_module.TypeOCI, metadata.Type)
|
assert.Equal(t, container_module.TypeOCI, metadata.Type)
|
||||||
assert.Len(t, metadata.ImageLayers, 2)
|
assert.Len(t, metadata.ImageLayers, 2)
|
||||||
assert.Empty(t, metadata.MultiArch)
|
assert.Empty(t, metadata.Manifests)
|
||||||
|
|
||||||
assert.Len(t, pd.Files, 3)
|
assert.Len(t, pd.Files, 3)
|
||||||
for _, pfd := range pd.Files {
|
for _, pfd := range pd.Files {
|
||||||
|
@ -462,10 +462,22 @@ func TestPackageContainer(t *testing.T) {
|
||||||
assert.IsType(t, &container_module.Metadata{}, pd.Metadata)
|
assert.IsType(t, &container_module.Metadata{}, pd.Metadata)
|
||||||
metadata := pd.Metadata.(*container_module.Metadata)
|
metadata := pd.Metadata.(*container_module.Metadata)
|
||||||
assert.Equal(t, container_module.TypeOCI, metadata.Type)
|
assert.Equal(t, container_module.TypeOCI, metadata.Type)
|
||||||
assert.Contains(t, metadata.MultiArch, "linux/arm/v7")
|
assert.Len(t, metadata.Manifests, 2)
|
||||||
assert.Equal(t, manifestDigest, metadata.MultiArch["linux/arm/v7"])
|
assert.Condition(t, func() bool {
|
||||||
assert.Contains(t, metadata.MultiArch, "linux/arm64/v8")
|
for _, m := range metadata.Manifests {
|
||||||
assert.Equal(t, untaggedManifestDigest, metadata.MultiArch["linux/arm64/v8"])
|
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.Len(t, pd.Files, 1)
|
||||||
assert.True(t, pd.Files[0].File.IsLead)
|
assert.True(t, pd.Files[0].File.IsLead)
|
||||||
|
|
Loading…
Reference in a new issue