Commit b1ddb5e2 authored by Wenkai Yin's avatar Wenkai Yin
Browse files

Implement the icon API to get the icon of artifact



Implement the icon API to get the icon of artifact
Signed-off-by: default avatarWenkai Yin <yinw@vmware.com>
parent 205f4f66
......@@ -1206,6 +1206,27 @@ paths:
$ref: '#/responses/404'
'500':
$ref: '#/responses/500'
/icons/{digest}:
get:
summary: Get artifact icon
description: Get the artifact icon with the specified digest. As the original icon image is resized and encoded before returning, the parameter "digest" in the path doesn't match the hash of the returned content
tags:
- icon
operationId: getIcon
parameters:
- $ref: '#/parameters/requestId'
- $ref: '#/parameters/digest'
responses:
'200':
description: Success
schema:
$ref: '#/definitions/Icon'
'400':
$ref: '#/responses/400'
'404':
$ref: '#/responses/404'
'500':
$ref: '#/responses/500'
parameters:
query:
name: q
......@@ -1244,6 +1265,12 @@ parameters:
description: The reference of the artifact, can be digest or tag
required: true
type: string
digest:
name: digest
in: path
description: The digest of the resource
required: true
type: string
tagName:
name: tag_name
in: path
......@@ -1451,6 +1478,9 @@ definitions:
type: integer
format: int64
description: The size of the artifact
icon:
type: string
description: The digest of the icon
push_time:
type: string
format: date-time
......@@ -1919,3 +1949,12 @@ definitions:
type: boolean
default:
type: boolean
Icon:
type: object
properties:
content-type:
type: string
description: The content type of the icon
content:
type: string
description: The base64 encoded content of the icon
......@@ -115,8 +115,6 @@ ALTER TABLE schedule DROP COLUMN IF EXISTS status;
UPDATE registry SET type = 'quay' WHERE type = 'quay-io';
ALTER TABLE artifact ADD COLUMN icon varchar(255);
CREATE TABLE IF NOT EXISTS data_migrations (
version int
);
......@@ -128,3 +126,18 @@ INSERT INTO data_migrations (version) VALUES (
END
);
ALTER TABLE schema_migrations DROP COLUMN IF EXISTS data_version;
ALTER TABLE artifact ADD COLUMN IF NOT EXISTS icon varchar(255);
UPDATE artifact
SET icon=(
CASE
WHEN type='IMAGE' THEN
'sha256:0048162a053eef4d4ce3fe7518615bef084403614f8bca43b40ae2e762e11e06'
WHEN type='CHART' THEN
'sha256:61cf3a178ff0f75bf08a25d96b75cf7355dc197749a9f128ed3ef34b0df05518'
WHEN type='CNAB' THEN
'sha256:089bdda265c14d8686111402c8ad629e8177a1ceb7dcd0f7f39b6480f623b3bd'
ELSE
'sha256:da834479c923584f4cbcdecc0dac61f32bef1d51e8aae598cf16bd154efab49f'
END);
......@@ -8,6 +8,7 @@ COPY ./make/photon/core/entrypoint.sh /harbor/
COPY ./make/photon/core/harbor_core /harbor/
COPY ./src/core/views /harbor/views
COPY ./make/migrations /harbor/migrations
COPY ./icons /harbor/icons
RUN chown -R harbor:harbor /etc/pki/tls/certs \
&& chown harbor:harbor /harbor/entrypoint.sh && chmod u+x /harbor/entrypoint.sh \
......
......@@ -21,6 +21,7 @@ import (
ps "github.com/goharbor/harbor/src/controller/artifact/processor"
"github.com/goharbor/harbor/src/controller/artifact/processor/base"
"github.com/goharbor/harbor/src/controller/icon"
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/lib/log"
"github.com/goharbor/harbor/src/pkg/artifact"
......@@ -57,6 +58,14 @@ type processor struct {
chartOperator chart.Operator
}
func (p *processor) AbstractMetadata(ctx context.Context, artifact *artifact.Artifact, manifest []byte) error {
if err := p.ManifestProcessor.AbstractMetadata(ctx, artifact, manifest); err != nil {
return err
}
artifact.Icon = icon.DigestOfIconChart
return nil
}
func (p *processor) AbstractAddition(ctx context.Context, artifact *artifact.Artifact, addition string) (*ps.Addition, error) {
if addition != AdditionTypeValues && addition != AdditionTypeReadme && addition != AdditionTypeDependencies {
return nil, errors.New(nil).WithCode(errors.BadRequestCode).
......
......@@ -15,11 +15,14 @@
package chart
import (
"bytes"
"github.com/docker/distribution"
"github.com/goharbor/harbor/src/controller/artifact/processor/base"
"github.com/goharbor/harbor/src/controller/icon"
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/pkg/artifact"
chartserver "github.com/goharbor/harbor/src/pkg/chart"
"github.com/goharbor/harbor/src/testing/mock"
"github.com/goharbor/harbor/src/testing/pkg/chart"
"github.com/goharbor/harbor/src/testing/pkg/registry"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
......@@ -30,6 +33,33 @@ import (
"testing"
)
var (
chartManifest = `{"schemaVersion":2,"config":{"mediaType":"application/vnd.cncf.helm.config.v1+json","digest":"sha256:76a59ebef39013bf7b57e411629b569a5175590024f31eeaaa577a0f8da9e523","size":528},"layers":[{"mediaType":"application/tar+gzip","digest":"sha256:0bd64cfb958b68c71b46597e22185a41e784dc96e04090bc7d2a480b704c3b65","size":12607}]}`
chartYaml = `{
"name":"redis",
"home": "http://redis.io/",
"sources": [
"https://github.com/bitnami/bitnami-docker-redis"
],
"version": "3.2.5",
"description": "Open source, advanced key-value store. It is often referred to as a data structure server since keys can contain strings, hashes, lists, sets and sorted sets.",
"keywords": [
"redis",
"keyvalue",
"database"
],
"maintainers": [
{
"name": "bitnami-bot",
"email":"containers@bitnami.com"
}
],
"icon": "https://bitnami.com/assets/stacks/redis/img/redis-stack-220x234.png",
"apiVersion": "v1",
"appVersion": "4.0.9"
}`
)
type processorTestSuite struct {
suite.Suite
processor *processor
......@@ -46,41 +76,20 @@ func (p *processorTestSuite) SetupTest() {
p.processor.ManifestProcessor = &base.ManifestProcessor{RegCli: p.regCli}
}
func (p *processorTestSuite) TestAbstractMetadata() {
artifact := &artifact.Artifact{}
p.regCli.On("PullBlob", mock.Anything, mock.Anything).Return(0, ioutil.NopCloser(bytes.NewReader([]byte(chartYaml))), nil)
err := p.processor.AbstractMetadata(nil, artifact, []byte(chartManifest))
p.Require().Nil(err)
p.Equal(icon.DigestOfIconChart, artifact.Icon)
p.regCli.AssertExpectations(p.T())
}
func (p *processorTestSuite) TestAbstractAddition() {
// unknown addition
_, err := p.processor.AbstractAddition(nil, nil, "unknown_addition")
p.True(errors.IsErr(err, errors.BadRequestCode))
chartManifest := `{"schemaVersion":2,"config":{"mediaType":"application/vnd.cncf.helm.config.v1+json","digest":"sha256:76a59ebef39013bf7b57e411629b569a5175590024f31eeaaa577a0f8da9e523","size":528},"layers":[{"mediaType":"application/tar+gzip","digest":"sha256:0bd64cfb958b68c71b46597e22185a41e784dc96e04090bc7d2a480b704c3b65","size":12607}]}`
chartYaml := `{
“name”:“redis”,
“home”:“http://redis.io/",
“sources”:[
“https://github.com/bitnami/bitnami-docker-redis"
],
“version”:“3.2.5",
“description”:“Open source, advanced key-value store. It is often referred to as a data structure server since keys can contain strings, hashes, lists, sets and sorted sets.“,
“keywords”:[
“redis”,
“keyvalue”,
“database”
],
“maintainers”:[
{
“name”:“bitnami-bot”,
“email”:“containers@bitnami.com"
}
],
“icon”:“https://bitnami.com/assets/stacks/redis/img/redis-stack-220x234.png",
“apiVersion”:“v1”,
“appVersion”:“4.0.9”
}`
chartDetails := &chartserver.VersionDetails{
Dependencies: []*helm_chart.Dependency{
{
......
......@@ -19,6 +19,7 @@ import (
ps "github.com/goharbor/harbor/src/controller/artifact/processor"
"github.com/goharbor/harbor/src/controller/artifact/processor/base"
"github.com/goharbor/harbor/src/controller/icon"
"github.com/goharbor/harbor/src/lib/log"
"github.com/goharbor/harbor/src/pkg/artifact"
)
......@@ -46,6 +47,7 @@ type processor struct {
}
func (p *processor) AbstractMetadata(ctx context.Context, art *artifact.Artifact, manifest []byte) error {
art.Icon = icon.DigestOfIconCNAB
cfgManiDgt := ""
// try to get the digest of the manifest that the config layer is referenced by
for _, reference := range art.References {
......
......@@ -17,6 +17,7 @@ package cnab
import (
"github.com/docker/distribution"
"github.com/goharbor/harbor/src/controller/artifact/processor/base"
"github.com/goharbor/harbor/src/controller/icon"
"github.com/goharbor/harbor/src/pkg/artifact"
"github.com/goharbor/harbor/src/testing/pkg/registry"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
......@@ -98,6 +99,7 @@ func (p *processorTestSuite) TestAbstractMetadata() {
p.Len(art.ExtraAttrs, 7)
p.Equal("0.1.1", art.ExtraAttrs["version"].(string))
p.Equal("helloworld", art.ExtraAttrs["name"].(string))
p.Equal(icon.DigestOfIconCNAB, art.Icon)
}
func (p *processorTestSuite) TestGetArtifactType() {
......
......@@ -20,23 +20,20 @@ import (
"regexp"
"strings"
"github.com/docker/distribution/manifest/schema2"
"github.com/goharbor/harbor/src/controller/icon"
// annotation parsers will be registered
"github.com/goharbor/harbor/src/controller/artifact/annotation"
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/lib/log"
"github.com/goharbor/harbor/src/pkg/artifact"
"github.com/goharbor/harbor/src/pkg/registry"
"github.com/docker/distribution/manifest/schema2"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
)
const (
// ArtifactTypeUnknown defines the type for the unknown artifacts
ArtifactTypeUnknown = "UNKNOWN"
// DefaultIconDigest defines default icon layer digest
DefaultIconDigest = "sha256:da834479c923584f4cbcdecc0dac61f32bef1d51e8aae598cf16bd154efab49f"
)
var (
......@@ -126,11 +123,11 @@ func (d *defaultProcessor) AbstractMetadata(ctx context.Context, artifact *artif
}
if artifact.Icon == "" {
artifact.Icon = DefaultIconDigest
artifact.Icon = icon.DigestOfIconDefault
}
return nil
}
func (d *defaultProcessor) AbstractAddition(ctx context.Context, artifact *artifact.Artifact, addition string) (*Addition, error) {
// Addition not support for user-defined artifact yet.
// It will be support in the future.
......
......@@ -16,6 +16,7 @@ package processor
import (
"context"
"github.com/goharbor/harbor/src/controller/icon"
"github.com/goharbor/harbor/src/pkg/distribution"
"github.com/goharbor/harbor/src/testing/mock"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
......@@ -183,7 +184,7 @@ func (d *defaultProcessorTestSuite) TestAbstractMetadata() {
d.parser.On("Parse", context.TODO(), mock.AnythingOfType("*artifact.Artifact"), mock.AnythingOfType("[]byte")).Return(nil)
err = d.processor.AbstractMetadata(nil, art, content)
d.Require().Nil(err)
d.Equal(DefaultIconDigest, art.Icon)
d.Equal(icon.DigestOfIconDefault, art.Icon)
}
func TestDefaultProcessorTestSuite(t *testing.T) {
......
......@@ -16,9 +16,11 @@ package image
import (
"context"
"github.com/docker/distribution/manifest/manifestlist"
"github.com/goharbor/harbor/src/controller/artifact/processor"
"github.com/goharbor/harbor/src/controller/artifact/processor/base"
"github.com/goharbor/harbor/src/controller/icon"
"github.com/goharbor/harbor/src/lib/log"
"github.com/goharbor/harbor/src/pkg/artifact"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
......@@ -42,6 +44,14 @@ type indexProcessor struct {
*base.IndexProcessor
}
func (i *indexProcessor) AbstractMetadata(ctx context.Context, artifact *artifact.Artifact, manifest []byte) error {
if err := i.IndexProcessor.AbstractMetadata(ctx, artifact, manifest); err != nil {
return err
}
artifact.Icon = icon.DigestOfIconImage
return nil
}
func (i *indexProcessor) GetArtifactType(ctx context.Context, artifact *artifact.Artifact) string {
return ArtifactTypeImage
}
......@@ -15,8 +15,11 @@
package image
import (
"github.com/stretchr/testify/suite"
"testing"
"github.com/goharbor/harbor/src/controller/icon"
"github.com/goharbor/harbor/src/pkg/artifact"
"github.com/stretchr/testify/suite"
)
type indexProcessTestSuite struct {
......@@ -28,6 +31,13 @@ func (i *indexProcessTestSuite) SetupTest() {
i.processor = &indexProcessor{}
}
func (i *indexProcessTestSuite) TestAbstractMetadata() {
artifact := &artifact.Artifact{}
err := i.processor.AbstractMetadata(nil, artifact, nil)
i.Require().Nil(err)
i.Equal(icon.DigestOfIconImage, artifact.Icon)
}
func (i *indexProcessTestSuite) TestGetArtifactType() {
i.Assert().Equal(ArtifactTypeImage, i.processor.GetArtifactType(nil, nil))
}
......
......@@ -19,6 +19,7 @@ import (
"encoding/json"
"github.com/docker/distribution/manifest/schema1"
"github.com/goharbor/harbor/src/controller/artifact/processor"
"github.com/goharbor/harbor/src/controller/icon"
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/lib/log"
"github.com/goharbor/harbor/src/pkg/artifact"
......@@ -45,6 +46,7 @@ func (m *manifestV1Processor) AbstractMetadata(ctx context.Context, artifact *ar
artifact.ExtraAttrs = map[string]interface{}{}
}
artifact.ExtraAttrs["architecture"] = mani.Architecture
artifact.Icon = icon.DigestOfIconImage
return nil
}
......
......@@ -15,10 +15,12 @@
package image
import (
"testing"
"github.com/goharbor/harbor/src/controller/icon"
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/pkg/artifact"
"github.com/stretchr/testify/suite"
"testing"
)
type manifestV1ProcessorTestSuite struct {
......@@ -81,6 +83,7 @@ func (m *manifestV1ProcessorTestSuite) TestAbstractMetadata() {
err := m.processor.AbstractMetadata(nil, artifact, []byte(manifest))
m.Require().Nil(err)
m.Assert().Equal("amd64", artifact.ExtraAttrs["architecture"].(string))
m.Equal(icon.DigestOfIconImage, artifact.Icon)
}
func (m *manifestV1ProcessorTestSuite) TestAbstractAddition() {
......
......@@ -17,9 +17,11 @@ package image
import (
"context"
"encoding/json"
"github.com/docker/distribution/manifest/schema2"
"github.com/goharbor/harbor/src/controller/artifact/processor"
"github.com/goharbor/harbor/src/controller/artifact/processor/base"
"github.com/goharbor/harbor/src/controller/icon"
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/lib/log"
"github.com/goharbor/harbor/src/pkg/artifact"
......@@ -51,6 +53,14 @@ type manifestV2Processor struct {
*base.ManifestProcessor
}
func (m *manifestV2Processor) AbstractMetadata(ctx context.Context, artifact *artifact.Artifact, manifest []byte) error {
if err := m.ManifestProcessor.AbstractMetadata(ctx, artifact, manifest); err != nil {
return err
}
artifact.Icon = icon.DigestOfIconImage
return nil
}
func (m *manifestV2Processor) AbstractAddition(ctx context.Context, artifact *artifact.Artifact, addition string) (*processor.Addition, error) {
if addition != AdditionTypeBuildHistory {
return nil, errors.New(nil).WithCode(errors.BadRequestCode).
......
......@@ -15,16 +15,20 @@
package image
import (
"bytes"
"io/ioutil"
"strings"
"testing"
"github.com/docker/distribution"
"github.com/docker/distribution/manifest/schema2"
"github.com/goharbor/harbor/src/controller/artifact/processor/base"
"github.com/goharbor/harbor/src/controller/icon"
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/pkg/artifact"
"github.com/goharbor/harbor/src/testing/mock"
"github.com/goharbor/harbor/src/testing/pkg/registry"
"github.com/stretchr/testify/suite"
"io/ioutil"
"strings"
"testing"
)
var (
......@@ -135,6 +139,15 @@ func (m *manifestV2ProcessorTestSuite) SetupTest() {
m.processor.ManifestProcessor = &base.ManifestProcessor{RegCli: m.regCli}
}
func (m *manifestV2ProcessorTestSuite) TestAbstractMetadata() {
artifact := &artifact.Artifact{}
m.regCli.On("PullBlob", mock.Anything, mock.Anything).Return(0, ioutil.NopCloser(bytes.NewReader([]byte(config))), nil)
err := m.processor.AbstractMetadata(nil, artifact, []byte(manifest))
m.Require().Nil(err)
m.Equal(icon.DigestOfIconImage, artifact.Icon)
m.regCli.AssertExpectations(m.T())
}
func (m *manifestV2ProcessorTestSuite) TestAbstractAddition() {
// unknown addition
_, err := m.processor.AbstractAddition(nil, nil, "unknown_addition")
......
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package icon
import (
"bytes"
"context"
"encoding/base64"
"image"
// import the gif format
_ "image/gif"
// import the jpeg format
_ "image/jpeg"
"image/png"
"io"
"os"
"sync"
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/lib/q"
"github.com/goharbor/harbor/src/pkg/artifact"
"github.com/goharbor/harbor/src/pkg/registry"
"github.com/nfnt/resize"
)
// const definitions
const (
DigestOfIconImage = "sha256:0048162a053eef4d4ce3fe7518615bef084403614f8bca43b40ae2e762e11e06"
DigestOfIconChart = "sha256:61cf3a178ff0f75bf08a25d96b75cf7355dc197749a9f128ed3ef34b0df05518"
DigestOfIconCNAB = "sha256:089bdda265c14d8686111402c8ad629e8177a1ceb7dcd0f7f39b6480f623b3bd"
DigestOfIconDefault = "sha256:da834479c923584f4cbcdecc0dac61f32bef1d51e8aae598cf16bd154efab49f"
)
var (
builtInIcons = map[string]string{
DigestOfIconImage: "./icons/image.png",
DigestOfIconChart: "./icons/chart.png",
DigestOfIconCNAB: "./icons/cnab.png",
DigestOfIconDefault: "./icons/default.png",
}
// Ctl is a global icon controller instance
Ctl = NewController()
)
// Icon model for artifact icon
type Icon struct {
ContentType string
Content string // base64 encoded
}
// Controller defines the operations related with icon
type Controller interface {
// Get the icon specified by digest
Get(ctx context.Context, digest string) (icon *Icon, err error)
}
// NewController creates a new instance of the icon controller
func NewController() Controller {
return &controller{
artMgr: artifact.Mgr,
regCli: registry.Cli,
cache: sync.Map{},
}
}
type controller struct {
artMgr artifact.Manager
regCli registry.Client
cache sync.Map
}
func (c *controller) Get(ctx context.Context, digest string) (*Icon, error) {
ic, exist := c.cache.Load(digest)
if exist {
return ic.(*Icon), nil
}
var (
iconFile io.ReadCloser
err error
)
// for the fixed icons: image, helm chart, CNAB and unknown
if path, exist := builtInIcons[digest]; exist {
iconFile, err = os.Open(path)
if err != nil {
return nil, err
}
defer iconFile.Close()
} else {
// read icon from blob
artifacts, err := c.artMgr.List(ctx, &q.Query{
Keywords: map[string]interface{}{
"Icon": digest,
},
})
if err != nil {
return nil, err
}
if len(artifacts) == 0 {
return nil, errors.New(nil).WithCode(errors.NotFoundCode).
WithMessage("the icon %s not found", digest)
}
_, iconFile, err = c.regCli.PullBlob(artifacts[0].RepositoryName, digest)
if err != nil {
return nil, err
}
defer iconFile.Close()
}
img, _, err := image.Decode(iconFile)
if err != nil {
return nil, err
}