From 010e584b3f256ca68b8544cada9976ebc98f62bd Mon Sep 17 00:00:00 2001 From: Pragyesh Mishra Date: Mon, 9 Sep 2024 12:55:44 +0000 Subject: [PATCH] fix digest count (#2674) * fix digest count * fix digest count * fix digest count * Merge branch 'main' of https://git0.harness.io/l7B_kbSEQD2wjrM7PShm5w/PROD/Harness_Commons/gitness into AH-293-fix * fix digest count --- .../controller/metadata/artifact_mapper.go | 2 +- .../metadata/get_artifacts_versions.go | 61 ++++++++++++-- registry/app/api/openapi/api.yaml | 1 - .../contracts/artifact/services.gen.go | 82 +++++++++---------- .../openapi/contracts/artifact/types.gen.go | 2 +- registry/app/store/database/tag.go | 13 ++- registry/types/tag.go | 4 + 7 files changed, 115 insertions(+), 50 deletions(-) diff --git a/registry/app/api/controller/metadata/artifact_mapper.go b/registry/app/api/controller/metadata/artifact_mapper.go index 49602139d..cee535f08 100644 --- a/registry/app/api/controller/metadata/artifact_mapper.go +++ b/registry/app/api/controller/metadata/artifact_mapper.go @@ -73,10 +73,10 @@ func GetTagMetadata( registryURL string, ) []artifactapi.ArtifactVersionMetadata { artifactVersionMetadataList := []artifactapi.ArtifactVersionMetadata{} - digestCount := int64(1) for _, tag := range *tags { modifiedAt := GetTimeInMs(tag.ModifiedAt) size := GetImageSize(tag.Size) + digestCount := tag.DigestCount isLatestVersion := latestTag == tag.Name command := GetPullCommand(rootIdentifier, regIdentifier, image, tag.Name, string(tag.PackageType), registryURL) packageType, err := toPackageType(string(tag.PackageType)) diff --git a/registry/app/api/controller/metadata/get_artifacts_versions.go b/registry/app/api/controller/metadata/get_artifacts_versions.go index 5dd9e230c..1bff64662 100644 --- a/registry/app/api/controller/metadata/get_artifacts_versions.go +++ b/registry/app/api/controller/metadata/get_artifacts_versions.go @@ -16,12 +16,20 @@ package metadata import ( "context" + "fmt" "net/http" apiauth "github.com/harness/gitness/app/api/auth" "github.com/harness/gitness/app/api/request" "github.com/harness/gitness/registry/app/api/openapi/contracts/artifact" + ml "github.com/harness/gitness/registry/app/manifest/manifestlist" + os "github.com/harness/gitness/registry/app/manifest/ocischema" + s2 "github.com/harness/gitness/registry/app/manifest/schema2" + "github.com/harness/gitness/registry/app/pkg/docker" + "github.com/harness/gitness/registry/types" "github.com/harness/gitness/types/enum" + + "github.com/rs/zerolog/log" ) func (c *APIController) GetAllArtifactVersions( @@ -73,11 +81,11 @@ func (c *APIController) GetAllArtifactVersions( ) if err != nil { - return artifact.GetAllArtifactVersions500JSONResponse{ - InternalServerErrorJSONResponse: artifact.InternalServerErrorJSONResponse( - *GetErrorResponse(http.StatusInternalServerError, err.Error()), - ), - }, nil + return throw500Error(err) + } + err = setDigestCount(ctx, *tags) + if err != nil { + return throw500Error(err) } return artifact.GetAllArtifactVersions200JSONResponse{ @@ -87,3 +95,46 @@ func (c *APIController) GetAllArtifactVersions( ), }, nil } + +func setDigestCount(ctx context.Context, tags []types.TagMetadata) error { + for i := range tags { + err := setDigestCountInTagMetadata(ctx, &tags[i]) + if err != nil { + return err + } + } + return nil +} + +func setDigestCountInTagMetadata(ctx context.Context, t *types.TagMetadata) error { + m := types.Manifest{ + SchemaVersion: t.SchemaVersion, + MediaType: t.MediaType, + NonConformant: t.NonConformant, + Payload: t.Payload, + } + manifest, err := docker.DBManifestToManifest(&m) + if err != nil { + log.Ctx(ctx).Error().Stack().Err(err).Msg("Failed to convert DBManifest to Manifest") + return err + } + switch reqManifest := manifest.(type) { + case *s2.DeserializedManifest, *os.DeserializedManifest: + t.DigestCount = 1 + case *ml.DeserializedManifestList: + t.DigestCount = len(reqManifest.Manifests) + default: + err = fmt.Errorf("unknown manifest type: %T", manifest) + log.Ctx(ctx).Error().Stack().Err(err).Msg("Failed to set digest count") + } + return nil +} + +func throw500Error(err error) (artifact.GetAllArtifactVersionsResponseObject, error) { + wrappedErr := fmt.Errorf("internal server error: %w", err) + return artifact.GetAllArtifactVersions500JSONResponse{ + InternalServerErrorJSONResponse: artifact.InternalServerErrorJSONResponse( + *GetErrorResponse(http.StatusInternalServerError, wrappedErr.Error()), + ), + }, nil +} diff --git a/registry/app/api/openapi/api.yaml b/registry/app/api/openapi/api.yaml index 322402dde..c246110bf 100644 --- a/registry/app/api/openapi/api.yaml +++ b/registry/app/api/openapi/api.yaml @@ -1070,7 +1070,6 @@ components: type: string digestCount: type: integer - format: int64 pullCommand: type: string downloadsCount: diff --git a/registry/app/api/openapi/contracts/artifact/services.gen.go b/registry/app/api/openapi/contracts/artifact/services.gen.go index 495f915d6..f13910304 100644 --- a/registry/app/api/openapi/contracts/artifact/services.gen.go +++ b/registry/app/api/openapi/contracts/artifact/services.gen.go @@ -3704,47 +3704,47 @@ var swaggerSpec = []string{ "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", - "5dfl/wIAAP//tvNQvIqOAAA=", + "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", + "y6/L/wUAAP//B0v/XHmOAAA=", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/registry/app/api/openapi/contracts/artifact/types.gen.go b/registry/app/api/openapi/contracts/artifact/types.gen.go index 11b5e416a..296d259f1 100644 --- a/registry/app/api/openapi/contracts/artifact/types.gen.go +++ b/registry/app/api/openapi/contracts/artifact/types.gen.go @@ -106,7 +106,7 @@ type ArtifactSummary struct { // ArtifactVersionMetadata Artifact Version Metadata type ArtifactVersionMetadata struct { - DigestCount *int64 `json:"digestCount,omitempty"` + DigestCount *int `json:"digestCount,omitempty"` DownloadsCount *int64 `json:"downloadsCount,omitempty"` IslatestVersion *bool `json:"islatestVersion,omitempty"` LastModified *string `json:"lastModified,omitempty"` diff --git a/registry/app/store/database/tag.go b/registry/app/store/database/tag.go index 56f4e74c3..0d4094f96 100644 --- a/registry/app/store/database/tag.go +++ b/registry/app/store/database/tag.go @@ -86,6 +86,10 @@ type tagMetadataDB struct { DigestCount int `db:"digest_count"` IsLatestVersion bool `db:"latest_version"` ModifiedAt int64 `db:"modified_at"` + SchemaVersion int `db:"manifest_schema_version"` + NonConformant bool `db:"manifest_non_conformant"` + Payload []byte `db:"manifest_payload"` + MediaType string `db:"mt_media_type"` } type tagDetailDB struct { @@ -717,11 +721,14 @@ func (t tagDao) GetAllTagsByRepoAndImage( ) (*[]types.TagMetadata, error) { q := databaseg.Builder.Select( "t.tag_name as name, m.manifest_total_size as size,"+ - " r.registry_package_type as package_type, t.tag_updated_at as modified_at", + " r.registry_package_type as package_type, t.tag_updated_at as modified_at, "+ + " m.manifest_schema_version, m.manifest_non_conformant, m.manifest_payload, "+ + " mt.mt_media_type ", ). From("tags t"). Join("registries r ON t.tag_registry_id = r.registry_id"). Join("manifests m ON t.tag_manifest_id = m.manifest_id"). + Join("media_types mt ON mt.mt_id = m.manifest_media_type_id"). Where( "r.registry_parent_id = ? AND r.registry_name = ? AND t.tag_image_name = ?", parentID, repoKey, image, @@ -919,6 +926,10 @@ func (t tagDao) mapToTagMetadata( DigestCount: dst.DigestCount, IsLatestVersion: dst.IsLatestVersion, ModifiedAt: time.UnixMilli(dst.ModifiedAt), + SchemaVersion: dst.SchemaVersion, + NonConformant: dst.NonConformant, + MediaType: dst.MediaType, + Payload: dst.Payload, }, nil } diff --git a/registry/types/tag.go b/registry/types/tag.go index 35f08d14f..b1080d368 100644 --- a/registry/types/tag.go +++ b/registry/types/tag.go @@ -51,6 +51,10 @@ type TagMetadata struct { DigestCount int IsLatestVersion bool ModifiedAt time.Time + SchemaVersion int + NonConformant bool + Payload Payload + MediaType string } type TagDetail struct {