mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 21:28:11 +09:00 
			
		
		
		
	Fix some package registry problems (#34759)
1. Fix #33787 2. Fix container image display
This commit is contained in:
		| @@ -92,8 +92,8 @@ func DeletePropertyByID(ctx context.Context, propertyID int64) error { | ||||
| 	return err | ||||
| } | ||||
|  | ||||
| // DeletePropertyByName deletes properties by name | ||||
| func DeletePropertyByName(ctx context.Context, refType PropertyType, refID int64, name string) error { | ||||
| // DeletePropertiesByName deletes properties by name | ||||
| func DeletePropertiesByName(ctx context.Context, refType PropertyType, refID int64, name string) error { | ||||
| 	_, err := db.GetEngine(ctx).Where("ref_type = ? AND ref_id = ? AND name = ?", refType, refID, name).Delete(&PackageProperty{}) | ||||
| 	return err | ||||
| } | ||||
|   | ||||
| @@ -28,8 +28,7 @@ func NewContentStore() *ContentStore { | ||||
| 	return contentStore | ||||
| } | ||||
|  | ||||
| // Get gets a package blob | ||||
| func (s *ContentStore) Get(key BlobHash256Key) (storage.Object, error) { | ||||
| func (s *ContentStore) OpenBlob(key BlobHash256Key) (storage.Object, error) { | ||||
| 	return s.store.Open(KeyToRelativePath(key)) | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -23,6 +23,7 @@ import ( | ||||
| 	"code.gitea.io/gitea/modules/util" | ||||
| 	notify_service "code.gitea.io/gitea/services/notify" | ||||
| 	packages_service "code.gitea.io/gitea/services/packages" | ||||
| 	container_service "code.gitea.io/gitea/services/packages/container" | ||||
|  | ||||
| 	"github.com/opencontainers/go-digest" | ||||
| 	oci "github.com/opencontainers/image-spec/specs-go/v1" | ||||
| @@ -84,12 +85,11 @@ func processOciImageManifest(ctx context.Context, mci *manifestCreationInfo, buf | ||||
| 	manifestDigest := "" | ||||
|  | ||||
| 	err := func() error { | ||||
| 		var manifest oci.Manifest | ||||
| 		if err := json.NewDecoder(buf).Decode(&manifest); err != nil { | ||||
| 		manifest, configDescriptor, metadata, err := container_service.ParseManifestMetadata(ctx, buf, mci.Owner.ID, mci.Image) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 		if _, err := buf.Seek(0, io.SeekStart); err != nil { | ||||
| 		if _, err = buf.Seek(0, io.SeekStart); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| @@ -99,28 +99,7 @@ func processOciImageManifest(ctx context.Context, mci *manifestCreationInfo, buf | ||||
| 		} | ||||
| 		defer committer.Close() | ||||
|  | ||||
| 		configDescriptor, err := container_model.GetContainerBlob(ctx, &container_model.BlobSearchOptions{ | ||||
| 			OwnerID: mci.Owner.ID, | ||||
| 			Image:   mci.Image, | ||||
| 			Digest:  string(manifest.Config.Digest), | ||||
| 		}) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 		configReader, err := packages_module.NewContentStore().Get(packages_module.BlobHash256Key(configDescriptor.Blob.HashSHA256)) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		defer configReader.Close() | ||||
|  | ||||
| 		metadata, err := container_module.ParseImageConfig(manifest.Config.MediaType, configReader) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 		blobReferences := make([]*blobReference, 0, 1+len(manifest.Layers)) | ||||
|  | ||||
| 		blobReferences = append(blobReferences, &blobReference{ | ||||
| 			Digest:       manifest.Config.Digest, | ||||
| 			MediaType:    manifest.Config.MediaType, | ||||
| @@ -388,19 +367,16 @@ func createPackageAndVersion(ctx context.Context, mci *manifestCreationInfo, met | ||||
| 			return nil, err | ||||
| 		} | ||||
| 	} else { | ||||
| 		props, err := packages_model.GetPropertiesByName(ctx, packages_model.PropertyTypeVersion, pv.ID, container_module.PropertyManifestTagged) | ||||
| 		if err != nil { | ||||
| 		if err = packages_model.DeletePropertiesByName(ctx, packages_model.PropertyTypeVersion, pv.ID, container_module.PropertyManifestTagged); err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		for _, prop := range props { | ||||
| 			if err = packages_model.DeletePropertyByID(ctx, prop.ID); err != nil { | ||||
| 				return nil, err | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if err = packages_model.DeletePropertiesByName(ctx, packages_model.PropertyTypeVersion, pv.ID, container_module.PropertyManifestReference); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	for _, manifest := range metadata.Manifests { | ||||
| 		if err = packages_model.InsertOrUpdateProperty(ctx, packages_model.PropertyTypeVersion, pv.ID, container_module.PropertyManifestReference, manifest.Digest); err != nil { | ||||
| 		if _, err = packages_model.InsertProperty(ctx, packages_model.PropertyTypeVersion, pv.ID, container_module.PropertyManifestReference, manifest.Digest); err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 	} | ||||
|   | ||||
| @@ -4,6 +4,8 @@ | ||||
| package user | ||||
|  | ||||
| import ( | ||||
| 	gocontext "context" | ||||
| 	"errors" | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
|  | ||||
| @@ -20,6 +22,7 @@ import ( | ||||
| 	"code.gitea.io/gitea/modules/optional" | ||||
| 	alpine_module "code.gitea.io/gitea/modules/packages/alpine" | ||||
| 	arch_module "code.gitea.io/gitea/modules/packages/arch" | ||||
| 	container_module "code.gitea.io/gitea/modules/packages/container" | ||||
| 	debian_module "code.gitea.io/gitea/modules/packages/debian" | ||||
| 	rpm_module "code.gitea.io/gitea/modules/packages/rpm" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| @@ -31,6 +34,7 @@ import ( | ||||
| 	"code.gitea.io/gitea/services/context" | ||||
| 	"code.gitea.io/gitea/services/forms" | ||||
| 	packages_service "code.gitea.io/gitea/services/packages" | ||||
| 	container_service "code.gitea.io/gitea/services/packages/container" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| @@ -162,6 +166,24 @@ func RedirectToLastVersion(ctx *context.Context) { | ||||
| 	ctx.Redirect(pd.VersionWebLink()) | ||||
| } | ||||
|  | ||||
| func viewPackageContainerImage(ctx gocontext.Context, pd *packages_model.PackageDescriptor, digest string) (*container_module.Metadata, error) { | ||||
| 	manifestBlob, err := container_model.GetContainerBlob(ctx, &container_model.BlobSearchOptions{ | ||||
| 		OwnerID: pd.Owner.ID, | ||||
| 		Image:   pd.Package.LowerName, | ||||
| 		Digest:  digest, | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	manifestReader, err := packages_service.OpenBlobStream(manifestBlob.Blob) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	defer manifestReader.Close() | ||||
| 	_, _, metadata, err := container_service.ParseManifestMetadata(ctx, manifestReader, pd.Owner.ID, pd.Package.LowerName) | ||||
| 	return metadata, err | ||||
| } | ||||
|  | ||||
| // ViewPackageVersion displays a single package version | ||||
| func ViewPackageVersion(ctx *context.Context) { | ||||
| 	if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil { | ||||
| @@ -169,6 +191,7 @@ func ViewPackageVersion(ctx *context.Context) { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	versionSub := ctx.PathParam("version_sub") | ||||
| 	pd := ctx.Package.Descriptor | ||||
| 	ctx.Data["Title"] = pd.Package.Name | ||||
| 	ctx.Data["IsPackagesPage"] = true | ||||
| @@ -180,6 +203,9 @@ func ViewPackageVersion(ctx *context.Context) { | ||||
| 	} | ||||
| 	ctx.Data["PackageRegistryHost"] = registryHostURL.Host | ||||
|  | ||||
| 	var pvs []*packages_model.PackageVersion | ||||
| 	pvsTotal := int64(0) | ||||
|  | ||||
| 	switch pd.Package.Type { | ||||
| 	case packages_model.TypeAlpine: | ||||
| 		branches := make(container.Set[string]) | ||||
| @@ -257,21 +283,26 @@ func ViewPackageVersion(ctx *context.Context) { | ||||
|  | ||||
| 		ctx.Data["Groups"] = util.Sorted(groups.Values()) | ||||
| 		ctx.Data["Architectures"] = util.Sorted(architectures.Values()) | ||||
| 	} | ||||
|  | ||||
| 	var ( | ||||
| 		total int64 | ||||
| 		pvs   []*packages_model.PackageVersion | ||||
| 	) | ||||
| 	switch pd.Package.Type { | ||||
| 	case packages_model.TypeContainer: | ||||
| 		pvs, total, err = container_model.SearchImageTags(ctx, &container_model.ImageTagsSearchOptions{ | ||||
| 		imageMetadata := pd.Metadata | ||||
| 		if versionSub != "" { | ||||
| 			imageMetadata, err = viewPackageContainerImage(ctx, pd, versionSub) | ||||
| 			if errors.Is(err, util.ErrNotExist) { | ||||
| 				ctx.NotFound(nil) | ||||
| 				return | ||||
| 			} else if err != nil { | ||||
| 				ctx.ServerError("viewPackageContainerImage", err) | ||||
| 				return | ||||
| 			} | ||||
| 		} | ||||
| 		ctx.Data["ContainerImageMetadata"] = imageMetadata | ||||
| 		pvs, pvsTotal, err = container_model.SearchImageTags(ctx, &container_model.ImageTagsSearchOptions{ | ||||
| 			Paginator: db.NewAbsoluteListOptions(0, 5), | ||||
| 			PackageID: pd.Package.ID, | ||||
| 			IsTagged:  true, | ||||
| 		}) | ||||
| 	default: | ||||
| 		pvs, total, err = packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{ | ||||
| 		pvs, pvsTotal, err = packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{ | ||||
| 			Paginator:  db.NewAbsoluteListOptions(0, 5), | ||||
| 			PackageID:  pd.Package.ID, | ||||
| 			IsInternal: optional.Some(false), | ||||
| @@ -283,7 +314,7 @@ func ViewPackageVersion(ctx *context.Context) { | ||||
| 	} | ||||
|  | ||||
| 	ctx.Data["LatestVersions"] = pvs | ||||
| 	ctx.Data["TotalVersionCount"] = total | ||||
| 	ctx.Data["TotalVersionCount"] = pvsTotal | ||||
|  | ||||
| 	ctx.Data["CanWritePackages"] = ctx.Package.AccessMode >= perm.AccessModeWrite || ctx.IsUserSiteAdmin() | ||||
|  | ||||
|   | ||||
| @@ -1012,6 +1012,7 @@ func registerWebRoutes(m *web.Router) { | ||||
| 					m.Get("/versions", user.ListPackageVersions) | ||||
| 					m.Group("/{version}", func() { | ||||
| 						m.Get("", user.ViewPackageVersion) | ||||
| 						m.Get("/{version_sub}", user.ViewPackageVersion) | ||||
| 						m.Get("/files/{fileid}", user.DownloadPackageFile) | ||||
| 						m.Group("/settings", func() { | ||||
| 							m.Get("", user.PackageSettings) | ||||
|   | ||||
| @@ -5,11 +5,17 @@ package container | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"io" | ||||
| 	"strings" | ||||
|  | ||||
| 	packages_model "code.gitea.io/gitea/models/packages" | ||||
| 	container_service "code.gitea.io/gitea/models/packages/container" | ||||
| 	user_model "code.gitea.io/gitea/models/user" | ||||
| 	"code.gitea.io/gitea/modules/json" | ||||
| 	"code.gitea.io/gitea/modules/packages" | ||||
| 	container_module "code.gitea.io/gitea/modules/packages/container" | ||||
|  | ||||
| 	"github.com/opencontainers/image-spec/specs-go/v1" | ||||
| ) | ||||
|  | ||||
| // UpdateRepositoryNames updates the repository name property for all packages of the specific owner | ||||
| @@ -22,7 +28,7 @@ func UpdateRepositoryNames(ctx context.Context, owner *user_model.User, newOwner | ||||
| 	newOwnerName = strings.ToLower(newOwnerName) | ||||
|  | ||||
| 	for _, p := range ps { | ||||
| 		if err := packages_model.DeletePropertyByName(ctx, packages_model.PropertyTypePackage, p.ID, container_module.PropertyRepository); err != nil { | ||||
| 		if err := packages_model.DeletePropertiesByName(ctx, packages_model.PropertyTypePackage, p.ID, container_module.PropertyRepository); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| @@ -33,3 +39,26 @@ func UpdateRepositoryNames(ctx context.Context, owner *user_model.User, newOwner | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func ParseManifestMetadata(ctx context.Context, rd io.Reader, ownerID int64, imageName string) (*v1.Manifest, *packages_model.PackageFileDescriptor, *container_module.Metadata, error) { | ||||
| 	var manifest v1.Manifest | ||||
| 	if err := json.NewDecoder(rd).Decode(&manifest); err != nil { | ||||
| 		return nil, nil, nil, err | ||||
| 	} | ||||
| 	configDescriptor, err := container_service.GetContainerBlob(ctx, &container_service.BlobSearchOptions{ | ||||
| 		OwnerID: ownerID, | ||||
| 		Image:   imageName, | ||||
| 		Digest:  string(manifest.Config.Digest), | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		return nil, nil, nil, err | ||||
| 	} | ||||
|  | ||||
| 	configReader, err := packages.NewContentStore().OpenBlob(packages.BlobHash256Key(configDescriptor.Blob.HashSHA256)) | ||||
| 	if err != nil { | ||||
| 		return nil, nil, nil, err | ||||
| 	} | ||||
| 	defer configReader.Close() | ||||
| 	metadata, err := container_module.ParseImageConfig(manifest.Config.MediaType, configReader) | ||||
| 	return &manifest, configDescriptor, metadata, err | ||||
| } | ||||
|   | ||||
| @@ -599,6 +599,12 @@ func GetPackageFileStream(ctx context.Context, pf *packages_model.PackageFile) ( | ||||
| 	return GetPackageBlobStream(ctx, pf, pb, nil) | ||||
| } | ||||
|  | ||||
| func OpenBlobStream(pb *packages_model.PackageBlob) (io.ReadSeekCloser, error) { | ||||
| 	cs := packages_module.NewContentStore() | ||||
| 	key := packages_module.BlobHash256Key(pb.HashSHA256) | ||||
| 	return cs.OpenBlob(key) | ||||
| } | ||||
|  | ||||
| // GetPackageBlobStream returns the content of the specific package blob | ||||
| // If the storage supports direct serving and it's enabled, only the direct serving url is returned. | ||||
| func GetPackageBlobStream(ctx context.Context, pf *packages_model.PackageFile, pb *packages_model.PackageBlob, serveDirectReqParams url.Values) (io.ReadSeekCloser, *url.URL, *packages_model.PackageFile, error) { | ||||
| @@ -617,7 +623,7 @@ func GetPackageBlobStream(ctx context.Context, pf *packages_model.PackageFile, p | ||||
| 		} | ||||
| 	} | ||||
| 	if u == nil { | ||||
| 		s, err = cs.Get(key) | ||||
| 		s, err = cs.OpenBlob(key) | ||||
| 	} | ||||
|  | ||||
| 	if err == nil { | ||||
|   | ||||
| @@ -49,7 +49,11 @@ | ||||
| 						{{/* "unknown/unknown" is attestation-manifest, so we should skip it */}} | ||||
| 						{{if ne .Platform "unknown/unknown"}} | ||||
| 						<tr> | ||||
| 							<td><a class="tw-font-mono" href="{{$.PackageDescriptor.PackageWebLink}}/{{PathEscape .Digest}}">{{StringUtils.TrimPrefix .Digest "sha256:" | ShortSha}}</a></td> | ||||
| 							<td> | ||||
| 								<a class="tw-font-mono" href="{{$.PackageDescriptor.PackageWebLink}}/{{$.PackageDescriptor.Version.LowerVersion}}/{{PathEscape .Digest}}"> | ||||
| 									{{StringUtils.TrimPrefix .Digest "sha256:" | ShortSha}} | ||||
| 								</a> | ||||
| 							</td> | ||||
| 							<td>{{.Platform}}</td> | ||||
| 							<td>{{FileSize .Size}}</td> | ||||
| 						</tr> | ||||
| @@ -65,12 +69,24 @@ | ||||
| 			{{.PackageDescriptor.Metadata.Description}} | ||||
| 		</div> | ||||
| 	{{end}} | ||||
| 	{{if .PackageDescriptor.Metadata.ImageLayers}} | ||||
| 		<h4 class="ui top attached header">{{ctx.Locale.Tr "packages.container.layers"}}</h4> | ||||
|  | ||||
| 	{{/* a container manifest may contain sub manifests, so here we try to display some information of the sub manifest, | ||||
| 		not perfect, just better than before */}} | ||||
| 	{{$imageMetadata := .ContainerImageMetadata}} | ||||
| 	{{if $imageMetadata.ImageLayers}} | ||||
| 		<h4 class="ui top attached header flex-text-block"> | ||||
| 			{{ctx.Locale.Tr "packages.container.layers"}} | ||||
| 			{{/* only show the platform if the image metadata is not the package's, which means that it is a sub manifest */}} | ||||
| 			{{if ne .ContainerImageMetadata .PackageDescriptor.Metadata}} | ||||
| 				<span class="tw-text-sm flex-text-inline" title="{{ctx.Locale.Tr "packages.container.details.platform"}}"> | ||||
| 					({{svg "octicon-cpu" 12}} {{.ContainerImageMetadata.Platform}}) | ||||
| 				</span> | ||||
| 			{{end}} | ||||
| 		</h4> | ||||
| 		<div class="ui attached segment tw-break-anywhere"> | ||||
| 			<table class="ui very basic compact table"> | ||||
| 				<tbody> | ||||
| 					{{range .PackageDescriptor.Metadata.ImageLayers}} | ||||
| 					{{range $imageMetadata.ImageLayers}} | ||||
| 						<tr> | ||||
| 							<td>{{.}}</td> | ||||
| 						</tr> | ||||
| @@ -79,7 +95,7 @@ | ||||
| 			</table> | ||||
| 		</div> | ||||
| 	{{end}} | ||||
| 	{{if .PackageDescriptor.Metadata.Labels}} | ||||
| 	{{if $imageMetadata.Labels}} | ||||
| 		<h4 class="ui top attached header">{{ctx.Locale.Tr "packages.container.labels"}}</h4> | ||||
| 		<div class="ui attached segment"> | ||||
| 			<table class="ui very basic compact table"> | ||||
| @@ -90,7 +106,7 @@ | ||||
| 					</tr> | ||||
| 				</thead> | ||||
| 				<tbody> | ||||
| 					{{range $key, $value := .PackageDescriptor.Metadata.Labels}} | ||||
| 					{{range $key, $value := $imageMetadata.Labels}} | ||||
| 						<tr> | ||||
| 							<td class="tw-align-top">{{$key}}</td> | ||||
| 							<td class="tw-break-anywhere">{{$value}}</td> | ||||
|   | ||||
| @@ -4,7 +4,7 @@ | ||||
| 		<div class="ui form"> | ||||
| 			<div class="field"> | ||||
| 				<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.pypi.install"}}</label> | ||||
| 				<div class="markup"><pre class="code-block"><code>pip install --index-url <origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/pypi/simple/"></origin-url> {{.PackageDescriptor.Package.Name}}</code></pre></div> | ||||
| 				<div class="markup"><pre class="code-block"><code>pip install --index-url <origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/pypi/simple/"></origin-url> --extra-index-url https://pypi.org/ {{.PackageDescriptor.Package.Name}}</code></pre></div> | ||||
| 			</div> | ||||
| 			<div class="field"> | ||||
| 				<label>{{ctx.Locale.Tr "packages.registry.documentation" "PyPI" "https://docs.gitea.com/usage/packages/pypi/"}}</label> | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| <div class="issue-title-header"> | ||||
| 	{{$packageVersionLink := print $.PackageDescriptor.PackageWebLink "/" (PathEscape .PackageDescriptor.Version.LowerVersion)}} | ||||
| 	<h1>{{.PackageDescriptor.Package.Name}} ({{.PackageDescriptor.Version.Version}})</h1> | ||||
| 	<div> | ||||
| 		{{$timeStr := DateUtils.TimeSince .PackageDescriptor.Version.CreatedUnix}} | ||||
| @@ -74,7 +75,7 @@ | ||||
| 		<div class="ui relaxed list"> | ||||
| 			{{range .PackageDescriptor.Files}} | ||||
| 			<div class="item"> | ||||
| 				<a href="{{$.Link}}/files/{{.File.ID}}">{{.File.Name}}</a> | ||||
| 				<a href="{{$packageVersionLink}}/files/{{.File.ID}}">{{.File.Name}}</a> | ||||
| 				<span class="text small file-size">{{FileSize .Blob.Size}}</span> | ||||
| 			</div> | ||||
| 			{{end}} | ||||
| @@ -98,7 +99,7 @@ | ||||
| 			<div class="item">{{svg "octicon-issue-opened"}} <a href="{{.PackageDescriptor.Repository.Link}}/issues">{{ctx.Locale.Tr "repo.issues"}}</a></div> | ||||
| 			{{end}} | ||||
| 			{{if .CanWritePackages}} | ||||
| 			<div class="item">{{svg "octicon-tools"}} <a href="{{.Link}}/settings">{{ctx.Locale.Tr "repo.settings"}}</a></div> | ||||
| 			<div class="item">{{svg "octicon-tools"}} <a href="{{$packageVersionLink}}/settings">{{ctx.Locale.Tr "repo.settings"}}</a></div> | ||||
| 			{{end}} | ||||
| 		</div> | ||||
| 		{{end}} | ||||
|   | ||||
| @@ -562,8 +562,7 @@ func TestPackageContainer(t *testing.T) { | ||||
| 				assert.ElementsMatch(t, []string{strings.ToLower(user.LowerName + "/" + image)}, getAllByName(pd.PackageProperties, container_module.PropertyRepository)) | ||||
| 				assert.True(t, has(pd.VersionProperties, container_module.PropertyManifestTagged)) | ||||
|  | ||||
| 				// only the last manifest digest is associated with the version (OCI builders will push the index manifest digest as the final step) | ||||
| 				assert.ElementsMatch(t, []string{untaggedManifestDigest}, getAllByName(pd.VersionProperties, container_module.PropertyManifestReference)) | ||||
| 				assert.ElementsMatch(t, []string{manifestDigest, untaggedManifestDigest}, getAllByName(pd.VersionProperties, container_module.PropertyManifestReference)) | ||||
|  | ||||
| 				assert.IsType(t, &container_module.Metadata{}, pd.Metadata) | ||||
| 				metadata := pd.Metadata.(*container_module.Metadata) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user