From b98b8b9159a61883c5a93e76828c240b31bc94e3 Mon Sep 17 00:00:00 2001 From: Yiyang Huang Date: Wed, 8 Jul 2020 15:57:58 +0800 Subject: [PATCH 1/2] Unify parameters for functions in Processor interface Signed-off-by: Yiyang Huang --- src/controller/artifact/abstractor.go | 10 +++++----- src/controller/artifact/controller.go | 4 ++-- src/controller/artifact/processor/base/index.go | 6 +++--- src/controller/artifact/processor/base/manifest.go | 6 +++--- .../artifact/processor/base/manifest_test.go | 4 ++-- src/controller/artifact/processor/chart/chart.go | 4 ++-- src/controller/artifact/processor/chart/chart_test.go | 4 ++-- src/controller/artifact/processor/cnab/cnab.go | 6 +++--- src/controller/artifact/processor/cnab/cnab_test.go | 4 ++-- src/controller/artifact/processor/default.go | 6 +++--- src/controller/artifact/processor/default_test.go | 10 +++++----- src/controller/artifact/processor/image/index.go | 4 +++- src/controller/artifact/processor/image/index_test.go | 2 +- src/controller/artifact/processor/image/manifest_v1.go | 6 +++--- .../artifact/processor/image/manifest_v1_test.go | 6 +++--- src/controller/artifact/processor/image/manifest_v2.go | 4 ++-- .../artifact/processor/image/manifest_v2_test.go | 4 ++-- src/controller/artifact/processor/processor.go | 6 +++--- src/controller/artifact/processor/processor_test.go | 6 +++--- 19 files changed, 52 insertions(+), 50 deletions(-) diff --git a/src/controller/artifact/abstractor.go b/src/controller/artifact/abstractor.go index 070c24af7..4a1e1cac5 100644 --- a/src/controller/artifact/abstractor.go +++ b/src/controller/artifact/abstractor.go @@ -62,17 +62,17 @@ func (a *abstractor) AbstractMetadata(ctx context.Context, artifact *artifact.Ar case "", "application/json", schema1.MediaTypeSignedManifest: a.abstractManifestV1Metadata(artifact) case v1.MediaTypeImageManifest, schema2.MediaTypeManifest: - if err = a.abstractManifestV2Metadata(content, artifact); err != nil { + if err = a.abstractManifestV2Metadata(artifact, content); err != nil { return err } case v1.MediaTypeImageIndex, manifestlist.MediaTypeManifestList: - if err = a.abstractIndexMetadata(ctx, content, artifact); err != nil { + if err = a.abstractIndexMetadata(ctx, artifact, content); err != nil { return err } default: return fmt.Errorf("unsupported manifest media type: %s", artifact.ManifestMediaType) } - return processor.Get(artifact.MediaType).AbstractMetadata(ctx, content, artifact) + return processor.Get(artifact.MediaType).AbstractMetadata(ctx, artifact, content) } // the artifact is enveloped by docker manifest v1 @@ -86,7 +86,7 @@ func (a *abstractor) abstractManifestV1Metadata(artifact *artifact.Artifact) { } // the artifact is enveloped by OCI manifest or docker manifest v2 -func (a *abstractor) abstractManifestV2Metadata(content []byte, artifact *artifact.Artifact) error { +func (a *abstractor) abstractManifestV2Metadata(artifact *artifact.Artifact, content []byte) error { manifest := &v1.Manifest{} if err := json.Unmarshal(content, manifest); err != nil { return err @@ -104,7 +104,7 @@ func (a *abstractor) abstractManifestV2Metadata(content []byte, artifact *artifa } // the artifact is enveloped by OCI index or docker manifest list -func (a *abstractor) abstractIndexMetadata(ctx context.Context, content []byte, art *artifact.Artifact) error { +func (a *abstractor) abstractIndexMetadata(ctx context.Context, art *artifact.Artifact, content []byte) error { // the identity of index is still in progress, we use the manifest mediaType // as the media type of artifact art.MediaType = art.ManifestMediaType diff --git a/src/controller/artifact/controller.go b/src/controller/artifact/controller.go index 5f9ec828b..8d24419b8 100644 --- a/src/controller/artifact/controller.go +++ b/src/controller/artifact/controller.go @@ -185,7 +185,7 @@ func (c *controller) ensureArtifact(ctx context.Context, repository, digest stri } // populate the artifact type - artifact.Type = processor.Get(artifact.MediaType).GetArtifactType() + artifact.Type = processor.Get(artifact.MediaType).GetArtifactType(ctx, artifact) // create it // use orm.WithTransaction here to avoid the issue: @@ -600,7 +600,7 @@ func (c *controller) populateLabels(ctx context.Context, art *Artifact) { } func (c *controller) populateAdditionLinks(ctx context.Context, artifact *Artifact) { - types := processor.Get(artifact.MediaType).ListAdditionTypes() + types := processor.Get(artifact.MediaType).ListAdditionTypes(ctx, &artifact.Artifact) if len(types) > 0 { version := lib.GetAPIVersion(ctx) for _, t := range types { diff --git a/src/controller/artifact/processor/base/index.go b/src/controller/artifact/processor/base/index.go index 7e23d4e6d..d2c05fce3 100644 --- a/src/controller/artifact/processor/base/index.go +++ b/src/controller/artifact/processor/base/index.go @@ -36,7 +36,7 @@ type IndexProcessor struct { } // AbstractMetadata abstracts metadata of artifact -func (m *IndexProcessor) AbstractMetadata(ctx context.Context, content []byte, artifact *artifact.Artifact) error { +func (m *IndexProcessor) AbstractMetadata(ctx context.Context, artifact *artifact.Artifact, content []byte) error { return nil } @@ -47,11 +47,11 @@ func (m *IndexProcessor) AbstractAddition(ctx context.Context, artifact *artifac } // GetArtifactType returns the artifact type -func (m *IndexProcessor) GetArtifactType() string { +func (m *IndexProcessor) GetArtifactType(ctx context.Context, artifact *artifact.Artifact) string { return "" } // ListAdditionTypes returns the supported addition types -func (m *IndexProcessor) ListAdditionTypes() []string { +func (m *IndexProcessor) ListAdditionTypes(ctx context.Context, artifact *artifact.Artifact) []string { return nil } diff --git a/src/controller/artifact/processor/base/manifest.go b/src/controller/artifact/processor/base/manifest.go index 0e343b1ff..a91299a0c 100644 --- a/src/controller/artifact/processor/base/manifest.go +++ b/src/controller/artifact/processor/base/manifest.go @@ -40,7 +40,7 @@ type ManifestProcessor struct { } // AbstractMetadata abstracts metadata of artifact -func (m *ManifestProcessor) AbstractMetadata(ctx context.Context, content []byte, artifact *artifact.Artifact) error { +func (m *ManifestProcessor) AbstractMetadata(ctx context.Context, artifact *artifact.Artifact, content []byte) error { // get manifest manifest := &v1.Manifest{} if err := json.Unmarshal(content, manifest); err != nil { @@ -79,11 +79,11 @@ func (m *ManifestProcessor) AbstractAddition(ctx context.Context, artifact *arti } // GetArtifactType returns the artifact type -func (m *ManifestProcessor) GetArtifactType() string { +func (m *ManifestProcessor) GetArtifactType(ctx context.Context, artifact *artifact.Artifact) string { return "" } // ListAdditionTypes returns the supported addition types -func (m *ManifestProcessor) ListAdditionTypes() []string { +func (m *ManifestProcessor) ListAdditionTypes(ctx context.Context, artifact *artifact.Artifact) []string { return nil } diff --git a/src/controller/artifact/processor/base/manifest_test.go b/src/controller/artifact/processor/base/manifest_test.go index 47797be7a..d29e23a3f 100644 --- a/src/controller/artifact/processor/base/manifest_test.go +++ b/src/controller/artifact/processor/base/manifest_test.go @@ -138,7 +138,7 @@ func (m *manifestTestSuite) TestAbstractMetadata() { art := &artifact.Artifact{} m.regCli.On("PullBlob").Return(0, ioutil.NopCloser(strings.NewReader(config)), nil) - m.processor.AbstractMetadata(nil, []byte(manifest), art) + m.processor.AbstractMetadata(nil, art, []byte(manifest)) m.Len(art.ExtraAttrs, 9) // reset the mock @@ -148,7 +148,7 @@ func (m *manifestTestSuite) TestAbstractMetadata() { m.processor.properties = []string{"os"} art = &artifact.Artifact{} m.regCli.On("PullBlob").Return(0, ioutil.NopCloser(strings.NewReader(config)), nil) - m.processor.AbstractMetadata(nil, []byte(manifest), art) + m.processor.AbstractMetadata(nil, art, []byte(manifest)) m.Require().Len(art.ExtraAttrs, 1) m.Equal("linux", art.ExtraAttrs["os"]) } diff --git a/src/controller/artifact/processor/chart/chart.go b/src/controller/artifact/processor/chart/chart.go index 6417829dd..4a5206a62 100644 --- a/src/controller/artifact/processor/chart/chart.go +++ b/src/controller/artifact/processor/chart/chart.go @@ -121,10 +121,10 @@ func (p *processor) AbstractAddition(ctx context.Context, artifact *artifact.Art return nil, nil } -func (p *processor) GetArtifactType() string { +func (p *processor) GetArtifactType(ctx context.Context, artifact *artifact.Artifact) string { return ArtifactTypeChart } -func (p *processor) ListAdditionTypes() []string { +func (p *processor) ListAdditionTypes(ctx context.Context, artifact *artifact.Artifact) []string { return []string{AdditionTypeValues, AdditionTypeReadme, AdditionTypeDependencies} } diff --git a/src/controller/artifact/processor/chart/chart_test.go b/src/controller/artifact/processor/chart/chart_test.go index d468e2811..682303521 100644 --- a/src/controller/artifact/processor/chart/chart_test.go +++ b/src/controller/artifact/processor/chart/chart_test.go @@ -128,11 +128,11 @@ func (p *processorTestSuite) TestAbstractAddition() { } func (p *processorTestSuite) TestGetArtifactType() { - p.Assert().Equal(ArtifactTypeChart, p.processor.GetArtifactType()) + p.Assert().Equal(ArtifactTypeChart, p.processor.GetArtifactType(nil, nil)) } func (p *processorTestSuite) TestListAdditionTypes() { - additions := p.processor.ListAdditionTypes() + additions := p.processor.ListAdditionTypes(nil, nil) p.EqualValues([]string{AdditionTypeValues, AdditionTypeReadme, AdditionTypeDependencies}, additions) } diff --git a/src/controller/artifact/processor/cnab/cnab.go b/src/controller/artifact/processor/cnab/cnab.go index b335cbcb4..2d0ec28e9 100644 --- a/src/controller/artifact/processor/cnab/cnab.go +++ b/src/controller/artifact/processor/cnab/cnab.go @@ -45,7 +45,7 @@ type processor struct { manifestProcessor *base.ManifestProcessor } -func (p *processor) AbstractMetadata(ctx context.Context, manifest []byte, art *artifact.Artifact) error { +func (p *processor) AbstractMetadata(ctx context.Context, art *artifact.Artifact, manifest []byte, ) error { cfgManiDgt := "" // try to get the digest of the manifest that the config layer is referenced by for _, reference := range art.References { @@ -69,9 +69,9 @@ func (p *processor) AbstractMetadata(ctx context.Context, manifest []byte, art * } // abstract the metadata from config layer - return p.manifestProcessor.AbstractMetadata(ctx, payload, art) + return p.manifestProcessor.AbstractMetadata(ctx, art, payload) } -func (p *processor) GetArtifactType() string { +func (p *processor) GetArtifactType(ctx context.Context, artifact *artifact.Artifact) string { return ArtifactTypeCNAB } diff --git a/src/controller/artifact/processor/cnab/cnab_test.go b/src/controller/artifact/processor/cnab/cnab_test.go index dd5e5cedf..6545717e1 100644 --- a/src/controller/artifact/processor/cnab/cnab_test.go +++ b/src/controller/artifact/processor/cnab/cnab_test.go @@ -93,7 +93,7 @@ func (p *processorTestSuite) TestAbstractMetadata() { p.Require().Nil(err) p.regCli.On("PullManifest").Return(mani, "", nil) p.regCli.On("PullBlob").Return(0, ioutil.NopCloser(strings.NewReader(config)), nil) - err = p.processor.AbstractMetadata(nil, nil, art) + err = p.processor.AbstractMetadata(nil, art, nil) p.Require().Nil(err) p.Len(art.ExtraAttrs, 7) p.Equal("0.1.1", art.ExtraAttrs["version"].(string)) @@ -101,7 +101,7 @@ func (p *processorTestSuite) TestAbstractMetadata() { } func (p *processorTestSuite) TestGetArtifactType() { - p.Assert().Equal(ArtifactTypeCNAB, p.processor.GetArtifactType()) + p.Assert().Equal(ArtifactTypeCNAB, p.processor.GetArtifactType(nil, nil)) } func TestProcessorTestSuite(t *testing.T) { diff --git a/src/controller/artifact/processor/default.go b/src/controller/artifact/processor/default.go index f1c7a8505..3fd2a362d 100644 --- a/src/controller/artifact/processor/default.go +++ b/src/controller/artifact/processor/default.go @@ -35,7 +35,7 @@ type defaultProcessor struct { mediaType string } -func (d *defaultProcessor) GetArtifactType() string { +func (d *defaultProcessor) GetArtifactType(ctx context.Context, artifact *artifact.Artifact) string { // try to parse the type from the media type strs := artifactTypeRegExp.FindStringSubmatch(d.mediaType) if len(strs) == 2 { @@ -44,10 +44,10 @@ func (d *defaultProcessor) GetArtifactType() string { // can not get the artifact type from the media type, return unknown return ArtifactTypeUnknown } -func (d *defaultProcessor) ListAdditionTypes() []string { +func (d *defaultProcessor) ListAdditionTypes(ctx context.Context, artifact *artifact.Artifact) []string { return nil } -func (d *defaultProcessor) AbstractMetadata(ctx context.Context, manifest []byte, artifact *artifact.Artifact) error { +func (d *defaultProcessor) AbstractMetadata(ctx context.Context, artifact *artifact.Artifact, manifest []byte) error { // do nothing currently // we can extend this function to abstract the metadata in the future if needed return nil diff --git a/src/controller/artifact/processor/default_test.go b/src/controller/artifact/processor/default_test.go index f865e232c..61ca127dd 100644 --- a/src/controller/artifact/processor/default_test.go +++ b/src/controller/artifact/processor/default_test.go @@ -26,27 +26,27 @@ type defaultProcessorTestSuite struct { func (d *defaultProcessorTestSuite) TestGetArtifactType() { mediaType := "" processor := &defaultProcessor{mediaType: mediaType} - typee := processor.GetArtifactType() + typee := processor.GetArtifactType(nil, nil) d.Equal(ArtifactTypeUnknown, typee) mediaType = "unknown" processor = &defaultProcessor{mediaType: mediaType} - typee = processor.GetArtifactType() + typee = processor.GetArtifactType(nil, nil) d.Equal(ArtifactTypeUnknown, typee) mediaType = "application/vnd.oci.image.config.v1+json" processor = &defaultProcessor{mediaType: mediaType} - typee = processor.GetArtifactType() + typee = processor.GetArtifactType(nil, nil) d.Equal("IMAGE", typee) mediaType = "application/vnd.cncf.helm.chart.config.v1+json" processor = &defaultProcessor{mediaType: mediaType} - typee = processor.GetArtifactType() + typee = processor.GetArtifactType(nil, nil) d.Equal("HELM.CHART", typee) mediaType = "application/vnd.sylabs.sif.config.v1+json" processor = &defaultProcessor{mediaType: mediaType} - typee = processor.GetArtifactType() + typee = processor.GetArtifactType(nil, nil) d.Equal("SIF", typee) } diff --git a/src/controller/artifact/processor/image/index.go b/src/controller/artifact/processor/image/index.go index 7680f8750..dbfe82662 100644 --- a/src/controller/artifact/processor/image/index.go +++ b/src/controller/artifact/processor/image/index.go @@ -15,10 +15,12 @@ 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/lib/log" + "github.com/goharbor/harbor/src/pkg/artifact" v1 "github.com/opencontainers/image-spec/specs-go/v1" ) @@ -40,6 +42,6 @@ type indexProcessor struct { *base.IndexProcessor } -func (i *indexProcessor) GetArtifactType() string { +func (i *indexProcessor) GetArtifactType(ctx context.Context, artifact *artifact.Artifact) string { return ArtifactTypeImage } diff --git a/src/controller/artifact/processor/image/index_test.go b/src/controller/artifact/processor/image/index_test.go index a6b7e084f..f8393c98a 100644 --- a/src/controller/artifact/processor/image/index_test.go +++ b/src/controller/artifact/processor/image/index_test.go @@ -29,7 +29,7 @@ func (i *indexProcessTestSuite) SetupTest() { } func (i *indexProcessTestSuite) TestGetArtifactType() { - i.Assert().Equal(ArtifactTypeImage, i.processor.GetArtifactType()) + i.Assert().Equal(ArtifactTypeImage, i.processor.GetArtifactType(nil, nil)) } func TestIndexProcessTestSuite(t *testing.T) { diff --git a/src/controller/artifact/processor/image/manifest_v1.go b/src/controller/artifact/processor/image/manifest_v1.go index 2936e9ff5..01a1b4db8 100644 --- a/src/controller/artifact/processor/image/manifest_v1.go +++ b/src/controller/artifact/processor/image/manifest_v1.go @@ -36,7 +36,7 @@ func init() { type manifestV1Processor struct { } -func (m *manifestV1Processor) AbstractMetadata(ctx context.Context, manifest []byte, artifact *artifact.Artifact) error { +func (m *manifestV1Processor) AbstractMetadata(ctx context.Context, artifact *artifact.Artifact, manifest []byte) error { mani := &schema1.Manifest{} if err := json.Unmarshal(manifest, mani); err != nil { return err @@ -53,10 +53,10 @@ func (m *manifestV1Processor) AbstractAddition(ctx context.Context, artifact *ar WithMessage("addition %s isn't supported for %s(manifest version 1)", addition, ArtifactTypeImage) } -func (m *manifestV1Processor) GetArtifactType() string { +func (m *manifestV1Processor) GetArtifactType(ctx context.Context, artifact *artifact.Artifact) string { return ArtifactTypeImage } -func (m *manifestV1Processor) ListAdditionTypes() []string { +func (m *manifestV1Processor) ListAdditionTypes(ctx context.Context, artifact *artifact.Artifact) []string { return nil } diff --git a/src/controller/artifact/processor/image/manifest_v1_test.go b/src/controller/artifact/processor/image/manifest_v1_test.go index e32f24dbc..3aaf9162c 100644 --- a/src/controller/artifact/processor/image/manifest_v1_test.go +++ b/src/controller/artifact/processor/image/manifest_v1_test.go @@ -78,7 +78,7 @@ func (m *manifestV1ProcessorTestSuite) TestAbstractMetadata() { } ` artifact := &artifact.Artifact{} - err := m.processor.AbstractMetadata(nil, []byte(manifest), artifact) + err := m.processor.AbstractMetadata(nil, artifact, []byte(manifest)) m.Require().Nil(err) m.Assert().Equal("amd64", artifact.ExtraAttrs["architecture"].(string)) } @@ -89,11 +89,11 @@ func (m *manifestV1ProcessorTestSuite) TestAbstractAddition() { } func (m *manifestV1ProcessorTestSuite) TestGetArtifactType() { - m.Assert().Equal(ArtifactTypeImage, m.processor.GetArtifactType()) + m.Assert().Equal(ArtifactTypeImage, m.processor.GetArtifactType(nil, nil)) } func (m *manifestV1ProcessorTestSuite) TestListAdditionTypes() { - additions := m.processor.ListAdditionTypes() + additions := m.processor.ListAdditionTypes(nil, nil) m.Len(additions, 0) } diff --git a/src/controller/artifact/processor/image/manifest_v2.go b/src/controller/artifact/processor/image/manifest_v2.go index 66f9bbd21..8fa3a3bdb 100644 --- a/src/controller/artifact/processor/image/manifest_v2.go +++ b/src/controller/artifact/processor/image/manifest_v2.go @@ -86,10 +86,10 @@ func (m *manifestV2Processor) AbstractAddition(ctx context.Context, artifact *ar }, nil } -func (m *manifestV2Processor) GetArtifactType() string { +func (m *manifestV2Processor) GetArtifactType(ctx context.Context, artifact *artifact.Artifact) string { return ArtifactTypeImage } -func (m *manifestV2Processor) ListAdditionTypes() []string { +func (m *manifestV2Processor) ListAdditionTypes(ctx context.Context, artifact *artifact.Artifact) []string { return []string{AdditionTypeBuildHistory} } diff --git a/src/controller/artifact/processor/image/manifest_v2_test.go b/src/controller/artifact/processor/image/manifest_v2_test.go index a510a2c7c..1cb941e69 100644 --- a/src/controller/artifact/processor/image/manifest_v2_test.go +++ b/src/controller/artifact/processor/image/manifest_v2_test.go @@ -153,11 +153,11 @@ func (m *manifestV2ProcessorTestSuite) TestAbstractAddition() { } func (m *manifestV2ProcessorTestSuite) TestGetArtifactType() { - m.Assert().Equal(ArtifactTypeImage, m.processor.GetArtifactType()) + m.Assert().Equal(ArtifactTypeImage, m.processor.GetArtifactType(nil, nil)) } func (m *manifestV2ProcessorTestSuite) TestListAdditionTypes() { - additions := m.processor.ListAdditionTypes() + additions := m.processor.ListAdditionTypes(nil, nil) m.EqualValues([]string{AdditionTypeBuildHistory}, additions) } diff --git a/src/controller/artifact/processor/processor.go b/src/controller/artifact/processor/processor.go index 6d504d8d5..a59734ceb 100644 --- a/src/controller/artifact/processor/processor.go +++ b/src/controller/artifact/processor/processor.go @@ -35,12 +35,12 @@ type Addition struct { // Processor processes specified artifact type Processor interface { // GetArtifactType returns the type of one kind of artifact specified by media type - GetArtifactType() string + GetArtifactType(ctx context.Context, artifact *artifact.Artifact) string // ListAdditionTypes returns the supported addition types of one kind of artifact specified by media type - ListAdditionTypes() []string + ListAdditionTypes(ctx context.Context, artifact *artifact.Artifact) []string // AbstractMetadata abstracts the metadata for the specific artifact type into the artifact model, // the metadata can be got from the manifest or other layers referenced by the manifest. - AbstractMetadata(ctx context.Context, manifest []byte, artifact *artifact.Artifact) error + AbstractMetadata(ctx context.Context, artifact *artifact.Artifact, manifest []byte) error // AbstractAddition abstracts the addition of the artifact. // The additions are different for different artifacts: // build history for image; values.yaml, readme and dependencies for chart, etc diff --git a/src/controller/artifact/processor/processor_test.go b/src/controller/artifact/processor/processor_test.go index a3fb3c9f8..8a415a1a5 100644 --- a/src/controller/artifact/processor/processor_test.go +++ b/src/controller/artifact/processor/processor_test.go @@ -23,13 +23,13 @@ import ( type fakeProcessor struct{} -func (f *fakeProcessor) GetArtifactType() string { +func (f *fakeProcessor) GetArtifactType(ctx context.Context, artifact *artifact.Artifact) string { return "" } -func (f *fakeProcessor) ListAdditionTypes() []string { +func (f *fakeProcessor) ListAdditionTypes(ctx context.Context, artifact *artifact.Artifact) []string { return nil } -func (f *fakeProcessor) AbstractMetadata(ctx context.Context, manifest []byte, artifact *artifact.Artifact) error { +func (f *fakeProcessor) AbstractMetadata(ctx context.Context, artifact *artifact.Artifact, manifest []byte) error { return nil } func (f *fakeProcessor) AbstractAddition(ctx context.Context, artifact *artifact.Artifact, additionType string) (*Addition, error) { From b98dc97fbdc2ba9ab5003605a9cd0201ba080577 Mon Sep 17 00:00:00 2001 From: Yiyang Huang Date: Mon, 20 Jul 2020 09:56:56 +0800 Subject: [PATCH 2/2] feat: enhanced default processor Signed-off-by: Yiyang Huang --- .../postgresql/0040_2.1.0_schema.up.sql | 5 +- src/controller/artifact/abstractor_test.go | 18 +- src/controller/artifact/annotation/parser.go | 77 ++++++ .../artifact/annotation/parser_test.go | 41 +++ .../artifact/annotation/v1alpha1.go | 99 +++++++ .../artifact/annotation/v1alpha1_test.go | 255 ++++++++++++++++++ .../artifact/processor/cnab/cnab.go | 2 +- src/controller/artifact/processor/default.go | 99 ++++++- .../artifact/processor/default_test.go | 158 ++++++++++- .../artifact/processor/processor.go | 3 +- src/pkg/artifact/dao/model.go | 1 + src/pkg/artifact/model.go | 1 + src/testing/pkg/parser/parser.go | 30 +++ src/testing/pkg/processor/processor.go | 85 ++++++ 14 files changed, 845 insertions(+), 29 deletions(-) create mode 100644 src/controller/artifact/annotation/parser.go create mode 100644 src/controller/artifact/annotation/parser_test.go create mode 100644 src/controller/artifact/annotation/v1alpha1.go create mode 100644 src/controller/artifact/annotation/v1alpha1_test.go create mode 100644 src/testing/pkg/parser/parser.go create mode 100644 src/testing/pkg/processor/processor.go diff --git a/make/migrations/postgresql/0040_2.1.0_schema.up.sql b/make/migrations/postgresql/0040_2.1.0_schema.up.sql index 4fae82bcd..1a8a8f281 100644 --- a/make/migrations/postgresql/0040_2.1.0_schema.up.sql +++ b/make/migrations/postgresql/0040_2.1.0_schema.up.sql @@ -113,5 +113,6 @@ END $$; ALTER TABLE schedule DROP COLUMN IF EXISTS job_id; ALTER TABLE schedule DROP COLUMN IF EXISTS status; -/*replication quay.io update vendor type*/ -UPDATE registry SET type = 'quay' WHERE type = 'quay-io'; \ No newline at end of file +UPDATE registry SET type = 'quay' WHERE type = 'quay-io'; + +ALTER TABLE artifact ADD COLUMN icon varchar(255); diff --git a/src/controller/artifact/abstractor_test.go b/src/controller/artifact/abstractor_test.go index ee7ffe050..3c164547b 100644 --- a/src/controller/artifact/abstractor_test.go +++ b/src/controller/artifact/abstractor_test.go @@ -15,16 +15,20 @@ package artifact import ( + "testing" + + "github.com/goharbor/harbor/src/controller/artifact/processor" + "github.com/goharbor/harbor/src/pkg/artifact" + "github.com/goharbor/harbor/src/testing/mock" + tart "github.com/goharbor/harbor/src/testing/pkg/artifact" + tpro "github.com/goharbor/harbor/src/testing/pkg/processor" + "github.com/goharbor/harbor/src/testing/pkg/registry" + "github.com/docker/distribution" "github.com/docker/distribution/manifest/schema1" "github.com/docker/distribution/manifest/schema2" - "github.com/goharbor/harbor/src/controller/artifact/processor" - "github.com/goharbor/harbor/src/pkg/artifact" - tart "github.com/goharbor/harbor/src/testing/pkg/artifact" - "github.com/goharbor/harbor/src/testing/pkg/registry" v1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/stretchr/testify/suite" - "testing" ) var ( @@ -203,6 +207,7 @@ type abstractorTestSuite struct { argMgr *tart.FakeManager regCli *registry.FakeClient abstractor *abstractor + processor *tpro.Processor } func (a *abstractorTestSuite) SetupTest() { @@ -212,8 +217,10 @@ func (a *abstractorTestSuite) SetupTest() { artMgr: a.argMgr, regCli: a.regCli, } + a.processor = &tpro.Processor{} // clear all registered processors processor.Registry = map[string]processor.Processor{} + processor.Registry[schema2.MediaTypeImageConfig] = a.processor } // docker manifest v1 @@ -240,6 +247,7 @@ func (a *abstractorTestSuite) TestAbstractMetadataOfV2Manifest() { artifact := &artifact.Artifact{ ID: 1, } + a.processor.On("AbstractMetadata", mock.Anything, mock.Anything, mock.Anything).Return(nil) err = a.abstractor.AbstractMetadata(nil, artifact) a.Require().Nil(err) a.Assert().Equal(int64(1), artifact.ID) diff --git a/src/controller/artifact/annotation/parser.go b/src/controller/artifact/annotation/parser.go new file mode 100644 index 000000000..d4173921b --- /dev/null +++ b/src/controller/artifact/annotation/parser.go @@ -0,0 +1,77 @@ +package annotation + +import ( + "context" + + "github.com/goharbor/harbor/src/lib/log" + "github.com/goharbor/harbor/src/pkg/artifact" + reg "github.com/goharbor/harbor/src/pkg/registry" +) + +const ( + // GIF is icon content type image/gif + GIF = "image/gif" + // PNG is icon content type image/png + PNG = "image/png" + // JPEG is icon content type image/jpeg + JPEG = "image/jpeg" + + // AnnotationPrefix is the prefix of annotation + AnnotationPrefix = "io.goharbor.artifact" + + // SkipList is the key word of skip-list annotation + SkipList = "skip-list" + // Icon is the key word of icon annotation + Icon = "icon" +) + +var ( + // registry for registered annotation parsers + registry = map[string]Parser{} + + // sortedAnnotationVersionList define the order of AnnotationParser from low to high version. + // Low version annotation parser will parser annotation first. + sortedAnnotationVersionList = make([]string, 0) +) + +func init() { + v1alpha1Parser := &v1alpha1Parser{ + regCli: reg.Cli, + } + RegisterAnnotationParser(v1alpha1Parser, V1alpha1) +} + +// NewParser creates a new annotation parser +func NewParser() Parser { + return &parser{} +} + +// Parser parses annotations in artifact manifest +type Parser interface { + // Parse parses annotations in artifact manifest, abstracts data from artifact config layer into the artifact model + Parse(ctx context.Context, artifact *artifact.Artifact, manifest []byte) (err error) +} + +type parser struct{} + +func (p *parser) Parse(ctx context.Context, artifact *artifact.Artifact, manifest []byte) (err error) { + for _, annotationVersion := range sortedAnnotationVersionList { + err = GetAnnotationParser(annotationVersion).Parse(ctx, artifact, manifest) + if err != nil { + return err + } + } + return nil +} + +// RegisterAnnotationParser register annotation parser +func RegisterAnnotationParser(parser Parser, version string) { + registry[version] = parser + sortedAnnotationVersionList = append(sortedAnnotationVersionList, version) + log.Infof("the annotation parser to parser artifact annotation version %s registered", version) +} + +// GetAnnotationParser register annotation parser +func GetAnnotationParser(version string) Parser { + return registry[version] +} diff --git a/src/controller/artifact/annotation/parser_test.go b/src/controller/artifact/annotation/parser_test.go new file mode 100644 index 000000000..31e873405 --- /dev/null +++ b/src/controller/artifact/annotation/parser_test.go @@ -0,0 +1,41 @@ +package annotation + +import ( + "testing" + + fp "github.com/goharbor/harbor/src/testing/pkg/parser" + + "github.com/stretchr/testify/suite" +) + +type parserTestSuite struct { + suite.Suite +} + +func (p *parserTestSuite) SetupTest() { + registry = map[string]Parser{} +} + +func (p *parserTestSuite) TestRegisterAnnotationParser() { + // success + version := "v1alpha1" + parser := &fp.Parser{} + RegisterAnnotationParser(parser, version) + p.Equal(map[string]Parser{version: parser}, registry) +} + +func (p *parserTestSuite) TestGetAnnotationParser() { + // register the parser + version := "v1alpha1" + RegisterAnnotationParser(&fp.Parser{}, "v1alpha1") + + // get the parser + parser := GetAnnotationParser(version) + p.Require().NotNil(parser) + _, ok := parser.(*fp.Parser) + p.True(ok) +} + +func TestProcessorTestSuite(t *testing.T) { + suite.Run(t, &parserTestSuite{}) +} diff --git a/src/controller/artifact/annotation/v1alpha1.go b/src/controller/artifact/annotation/v1alpha1.go new file mode 100644 index 000000000..8e7552991 --- /dev/null +++ b/src/controller/artifact/annotation/v1alpha1.go @@ -0,0 +1,99 @@ +package annotation + +import ( + "context" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "strings" + + "github.com/goharbor/harbor/src/lib/errors" + "github.com/goharbor/harbor/src/pkg/artifact" + reg "github.com/goharbor/harbor/src/pkg/registry" + + "github.com/docker/distribution/manifest/schema2" + v1 "github.com/opencontainers/image-spec/specs-go/v1" +) + +const ( + // V1alpha1 is the version of annotation parser + V1alpha1 = "v1alpha1" +) + +type v1alpha1Parser struct { + regCli reg.Client +} + +func (p *v1alpha1Parser) Parse(ctx context.Context, artifact *artifact.Artifact, manifest []byte) error { + if artifact.ManifestMediaType != v1.MediaTypeImageManifest && artifact.ManifestMediaType != schema2.MediaTypeManifest { + return nil + } + // get manifest + mani := &v1.Manifest{} + if err := json.Unmarshal(manifest, mani); err != nil { + return err + } + + // parse skip-list annotation io.goharor.artifact.v1alpha1.skip-list + parseV1alpha1SkipList(artifact, mani) + + // parse icon annotation io.goharbor.artifact.v1alpha1.icon + err := parseV1alpha1Icon(artifact, mani, p.regCli) + if err != nil { + return err + } + + return nil +} + +func parseV1alpha1SkipList(artifact *artifact.Artifact, manifest *v1.Manifest) { + metadata := artifact.ExtraAttrs + skipListAnnotationKey := fmt.Sprintf("%s.%s.%s", AnnotationPrefix, V1alpha1, SkipList) + skipList, ok := manifest.Config.Annotations[skipListAnnotationKey] + if ok { + skipKeyList := strings.Split(skipList, ",") + for _, skipKey := range skipKeyList { + delete(metadata, skipKey) + } + artifact.ExtraAttrs = metadata + } +} + +func parseV1alpha1Icon(artifact *artifact.Artifact, manifest *v1.Manifest, reg reg.Client) error { + iconAnnotationKey := fmt.Sprintf("%s.%s.%s", AnnotationPrefix, V1alpha1, Icon) + var iconDigest string + for _, layer := range manifest.Layers { + _, ok := layer.Annotations[iconAnnotationKey] + if ok { + iconDigest = layer.Digest.String() + break + } + } + if iconDigest == "" { + return nil + } + // pull icon layer + _, icon, err := reg.PullBlob(artifact.RepositoryName, iconDigest) + if err != nil { + return err + } + // check the size of the size <= 1MB + data, err := ioutil.ReadAll(io.LimitReader(icon, 1<<20)) + if err != nil { + if err == io.EOF { + return errors.New(nil).WithCode(errors.BadRequestCode).WithMessage("the maximum size of the icon is 1MB") + } + return err + } + // check the content type + contentType := http.DetectContentType(data) + switch contentType { + case GIF, PNG, JPEG: + default: + return errors.New(nil).WithCode(errors.BadRequestCode).WithMessage("unsupported content type: %s", contentType) + } + artifact.Icon = iconDigest + return nil +} diff --git a/src/controller/artifact/annotation/v1alpha1_test.go b/src/controller/artifact/annotation/v1alpha1_test.go new file mode 100644 index 000000000..6ed7f54e8 --- /dev/null +++ b/src/controller/artifact/annotation/v1alpha1_test.go @@ -0,0 +1,255 @@ +// 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 annotation + +import ( + "encoding/base64" + "encoding/json" + "io/ioutil" + "strings" + "testing" + + "github.com/goharbor/harbor/src/pkg/artifact" + "github.com/goharbor/harbor/src/pkg/distribution" + reg "github.com/goharbor/harbor/src/testing/pkg/registry" + + v1 "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/stretchr/testify/suite" +) + +var ( + ormbConfig = `{ + "created": "2015-10-31T22:22:56.015925234Z", + "author": "Ce Gao ", + "description": "CNN Model", + "tags": [ + "cv" + ], + "labels": { + "tensorflow.version": "2.0.0" + }, + "framework": "TensorFlow", + "format": "SavedModel", + "size": 9223372036854775807, + "metrics": [ + { + "name": "acc", + "value": "0.9" + } + ], + "hyperparameters": [ + { + "name": "batch_size", + "value": "32" + } + ], + "signature": { + "inputs": [ + { + "name": "input_1", + "size": [ + 224, + 224, + 3 + ], + "dtype": "float64" + } + ], + "outputs": [ + { + "name": "output_1", + "size": [ + 1, + 1000 + ], + "dtype": "float64" + } + ], + "layers": [ + { + "name": "conv" + } + ] + }, + "training": { + "git": { + "repository": "git@github.com:caicloud/ormb.git", + "revision": "22f1d8406d464b0c0874075539c1f2e96c253775" + } + }, + "dataset": { + "git": { + "repository": "git@github.com:caicloud/ormb.git", + "revision": "22f1d8406d464b0c0874075539c1f2e96c253775" + } + } +}` + ormbManifest = `{ + "schemaVersion":2, + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "config":{ + "mediaType":"application/vnd.caicloud.model.config.v1alpha1+json", + "digest":"sha256:be948daf0e22f264ea70b713ea0db35050ae659c185706aa2fad74834455fe8c", + "size":187, + "annotations": { + "io.goharbor.artifact.v1alpha1.skip-list": "metrics,git" + } + }, + "layers":[ + { + "mediaType": "image/png", + "digest": "sha256:d923b93eadde0af5c639a972710a4d919066aba5d0dfbf4b9385099f70272da0", + "size": 166015, + "annotations": { + "io.goharbor.artifact.v1alpha1.icon": "" + } + }, + { + "mediaType":"application/tar+gzip", + "digest":"sha256:eb6063fecbb50a9d98268cb61746a0fd62a27a4af9e850ffa543a1a62d3948b2", + "size":166022 + } + ] +}` + ormbManifestWithoutSkipList = `{ + "schemaVersion":2, + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "config":{ + "mediaType":"application/vnd.caicloud.model.config.v1alpha1+json", + "digest":"sha256:be948daf0e22f264ea70b713ea0db35050ae659c185706aa2fad74834455fe8c", + "size":187 + }, + "layers":[ + { + "mediaType": "image/png", + "digest": "sha256:d923b93eadde0af5c639a972710a4d919066aba5d0dfbf4b9385099f70272da0", + "size": 166015, + "annotations": { + "io.goharbor.artifact.v1alpha1.icon": "" + } + }, + { + "mediaType":"application/tar+gzip", + "digest":"sha256:eb6063fecbb50a9d98268cb61746a0fd62a27a4af9e850ffa543a1a62d3948b2", + "size":166022 + } + ] +}` + ormbManifestWithoutIcon = `{ + "schemaVersion":2, + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "config":{ + "mediaType":"application/vnd.caicloud.model.config.v1alpha1+json", + "digest":"sha256:be948daf0e22f264ea70b713ea0db35050ae659c185706aa2fad74834455fe8c", + "size":187, + "annotations": { + "io.goharbor.artifact.v1alpha1.skip-list": "metrics,git" + } + }, + "layers":[ + { + "mediaType":"application/tar+gzip", + "digest":"sha256:eb6063fecbb50a9d98268cb61746a0fd62a27a4af9e850ffa543a1a62d3948b2", + "size":166022 + } + ] +}` + ormbIcon = "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LQAAgGBCCwAAIJjQAgAACCa0AAAAggktAACAYEILAAAgmNACAAAIJrQAAACCCS0AAIBgQgsAACCY0AIAAAgmtAAAAIIJLQAAgGBCCwAAIJjQAgAACCa0AAAAggktAACAYEILAAAgmNACAAAIJrQAAACCCS0AAIBgQgsAACCY0AIAAAgmtAAAAIIJLQAAgGBCCwAAIJjQAgAACCa0AAAAggktAACAYEILAAAgmNACAAAIJrQAAACCCS0AAIBgQgsAACCY0AIAAAgmtAAAAIIJLQAAgGBCCwAAIJjQAgAACCa0AAAAggktAACAYEILAAAgmNACAAAIJrQAAACCCS0AAIBgQgsAACCY0AIAAAgmtAAAAIIJLQAAgGBCCwAAIJjQAgAACCa0AAAAggktAACAYEILAAAgmNACAAAIJrQAAACCCS0AAIBgQgsAACCY0AIAAAgmtAAAAIIJLQAAgGBCCwAAIJjQAgAACCa0AAAAggktAACAYEILAAAgmNACAAAIJrQAAACCCS0AAIBgQgsAACCY0AIAAAgmtAAAAIIJLQAAgGBCCwAAIJjQAgAACCa0AAAAggktAACAYEILAAAgmNACAAAIJrQAAACCCS0AAIBgQgsAACCY0AIAAAgmtAAAAIIJLQAAgGBCCwAAIJjQAgAACCa0AAAAggktAACAYEILAAAgmNACAAAIJrQAAACCCS0AAIBgQgsAACCY0AIAAAgmtAAAAIIJLQAAgGBCCwAAIJjQAgAACCa0AAAAggktAACAYEILAAAgmNACAAAIJrQAAACCCS0AAIBgQgsAACBS0zT/H29PQcL+62hzAAAAAElFTkSuQmCC" +) + +// v1alpha1TestSuite is a test suite of testing v1alpha1 parser +type v1alpha1TestSuite struct { + suite.Suite + regCli *reg.FakeClient + v1alpha1Parser *v1alpha1Parser +} + +func (p *v1alpha1TestSuite) SetupTest() { + p.regCli = ®.FakeClient{} + p.v1alpha1Parser = &v1alpha1Parser{ + regCli: p.regCli, + } +} + +func (p *v1alpha1TestSuite) TestParse() { + manifest, _, err := distribution.UnmarshalManifest(v1.MediaTypeImageManifest, []byte(ormbManifest)) + p.Require().Nil(err) + manifestMediaType, content, err := manifest.Payload() + p.Require().Nil(err) + + metadata := map[string]interface{}{} + configBlob := ioutil.NopCloser(strings.NewReader(ormbConfig)) + err = json.NewDecoder(configBlob).Decode(&metadata) + p.Require().Nil(err) + art := &artifact.Artifact{ManifestMediaType: manifestMediaType, ExtraAttrs: metadata} + + blob := ioutil.NopCloser(base64.NewDecoder(base64.StdEncoding, strings.NewReader(ormbIcon))) + p.regCli.On("PullBlob").Return(0, blob, nil) + err = p.v1alpha1Parser.Parse(nil, art, content) + p.Require().Nil(err) + p.Len(art.ExtraAttrs, 12) + p.Equal("CNN Model", art.ExtraAttrs["description"]) + p.Equal("TensorFlow", art.ExtraAttrs["framework"]) + p.Equal([]interface{}{map[string]interface{}{"name": "batch_size", "value": "32"}}, art.ExtraAttrs["hyperparameters"]) + p.Equal("sha256:d923b93eadde0af5c639a972710a4d919066aba5d0dfbf4b9385099f70272da0", art.Icon) + + // reset the mock + p.SetupTest() + manifest, _, err = distribution.UnmarshalManifest(v1.MediaTypeImageManifest, []byte(ormbManifestWithoutSkipList)) + p.Require().Nil(err) + manifestMediaType, content, err = manifest.Payload() + p.Require().Nil(err) + + metadata = map[string]interface{}{} + configBlob = ioutil.NopCloser(strings.NewReader(ormbConfig)) + err = json.NewDecoder(configBlob).Decode(&metadata) + p.Require().Nil(err) + art = &artifact.Artifact{ManifestMediaType: manifestMediaType, ExtraAttrs: metadata} + + blob = ioutil.NopCloser(base64.NewDecoder(base64.StdEncoding, strings.NewReader(ormbIcon))) + p.regCli.On("PullBlob").Return(0, blob, nil) + err = p.v1alpha1Parser.Parse(nil, art, content) + p.Require().Nil(err) + p.Len(art.ExtraAttrs, 13) + p.Equal("CNN Model", art.ExtraAttrs["description"]) + p.Equal("TensorFlow", art.ExtraAttrs["framework"]) + p.Equal([]interface{}{map[string]interface{}{"name": "batch_size", "value": "32"}}, art.ExtraAttrs["hyperparameters"]) + p.Equal("sha256:d923b93eadde0af5c639a972710a4d919066aba5d0dfbf4b9385099f70272da0", art.Icon) + + // reset the mock + p.SetupTest() + manifest, _, err = distribution.UnmarshalManifest(v1.MediaTypeImageManifest, []byte(ormbManifestWithoutIcon)) + p.Require().Nil(err) + manifestMediaType, content, err = manifest.Payload() + p.Require().Nil(err) + + metadata = map[string]interface{}{} + configBlob = ioutil.NopCloser(strings.NewReader(ormbConfig)) + err = json.NewDecoder(configBlob).Decode(&metadata) + p.Require().Nil(err) + art = &artifact.Artifact{ManifestMediaType: manifestMediaType, ExtraAttrs: metadata} + + err = p.v1alpha1Parser.Parse(nil, art, content) + p.Require().Nil(err) + p.Len(art.ExtraAttrs, 12) + p.Equal("CNN Model", art.ExtraAttrs["description"]) + p.Equal("TensorFlow", art.ExtraAttrs["framework"]) + p.Equal([]interface{}{map[string]interface{}{"name": "batch_size", "value": "32"}}, art.ExtraAttrs["hyperparameters"]) + p.Equal("", art.Icon) +} + +func TestDefaultProcessorTestSuite(t *testing.T) { + suite.Run(t, &v1alpha1TestSuite{}) +} diff --git a/src/controller/artifact/processor/cnab/cnab.go b/src/controller/artifact/processor/cnab/cnab.go index 2d0ec28e9..7b2605118 100644 --- a/src/controller/artifact/processor/cnab/cnab.go +++ b/src/controller/artifact/processor/cnab/cnab.go @@ -45,7 +45,7 @@ type processor struct { manifestProcessor *base.ManifestProcessor } -func (p *processor) AbstractMetadata(ctx context.Context, art *artifact.Artifact, manifest []byte, ) error { +func (p *processor) AbstractMetadata(ctx context.Context, art *artifact.Artifact, manifest []byte) error { cfgManiDgt := "" // try to get the digest of the manifest that the config layer is referenced by for _, reference := range art.References { diff --git a/src/controller/artifact/processor/default.go b/src/controller/artifact/processor/default.go index 3fd2a362d..4c51ec68c 100644 --- a/src/controller/artifact/processor/default.go +++ b/src/controller/artifact/processor/default.go @@ -16,28 +16,44 @@ package processor import ( "context" - "github.com/goharbor/harbor/src/lib/errors" - "github.com/goharbor/harbor/src/pkg/artifact" + "encoding/json" "regexp" "strings" + + // 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" ) -// ArtifactTypeUnknown defines the type for the unknown artifacts -const ArtifactTypeUnknown = "UNKNOWN" +const ( + // ArtifactTypeUnknown defines the type for the unknown artifacts + ArtifactTypeUnknown = "UNKNOWN" + // DefaultIconDigest defines default icon layer digest + DefaultIconDigest = "sha256:da834479c923584f4cbcdecc0dac61f32bef1d51e8aae598cf16bd154efab49f" +) var ( + // DefaultProcessor is to process artifact which has no specific processor + DefaultProcessor = &defaultProcessor{regCli: registry.Cli} + artifactTypeRegExp = regexp.MustCompile(`^application/vnd\.[^.]*\.(.*)\.config\.[^.]*\+json$`) ) // the default processor to process artifact -// currently, it only tries to parse the artifact type from media type type defaultProcessor struct { - mediaType string + regCli registry.Client } func (d *defaultProcessor) GetArtifactType(ctx context.Context, artifact *artifact.Artifact) string { // try to parse the type from the media type - strs := artifactTypeRegExp.FindStringSubmatch(d.mediaType) + strs := artifactTypeRegExp.FindStringSubmatch(artifact.MediaType) if len(strs) == 2 { return strings.ToUpper(strs[1]) } @@ -47,12 +63,77 @@ func (d *defaultProcessor) GetArtifactType(ctx context.Context, artifact *artifa func (d *defaultProcessor) ListAdditionTypes(ctx context.Context, artifact *artifact.Artifact) []string { return nil } + +// The default processor will process user-defined artifact. +// AbstractMetadata will abstract data in a specific way. +// Annotation keys in artifact annotation will decide which content will be processed in artifact. +// Here is a manifest example: +// { +// "schemaVersion": 2, +// "config": { +// "mediaType": "application/vnd.caicloud.model.config.v1alpha1+json", +// "digest": "sha256:be948daf0e22f264ea70b713ea0db35050ae659c185706aa2fad74834455fe8c", +// "size": 187, +// "annotations": { +// "io.goharbor.artifact.v1alpha1.skip-list": "metrics,git" +// } +// }, +// "layers": [ +// { +// "mediaType": "image/png", +// "digest": "sha256:d923b93eadde0af5c639a972710a4d919066aba5d0dfbf4b9385099f70272da0", +// "size": 166015, +// "annotations": { +// "io.goharbor.artifact.v1alpha1.icon": "" +// } +// }, +// { +// "mediaType": "application/tar+gzip", +// "digest": "sha256:d923b93eadde0af5c639a972710a4d919066aba5d0dfbf4b9385099f70272da0", +// "size": 166015 +// } +// ] +// } func (d *defaultProcessor) AbstractMetadata(ctx context.Context, artifact *artifact.Artifact, manifest []byte) error { - // do nothing currently - // we can extend this function to abstract the metadata in the future if needed + if artifact.ManifestMediaType != v1.MediaTypeImageManifest && artifact.ManifestMediaType != schema2.MediaTypeManifest { + return nil + } + // get manifest + mani := &v1.Manifest{} + if err := json.Unmarshal(manifest, mani); err != nil { + return err + } + // get config layer + _, blob, err := d.regCli.PullBlob(artifact.RepositoryName, mani.Config.Digest.String()) + if err != nil { + return err + } + defer blob.Close() + // parse metadata from config layer + metadata := map[string]interface{}{} + // Some artifact may not have empty config layer. + if mani.Config.Size != 0 { + if err := json.NewDecoder(blob).Decode(&metadata); err != nil { + return err + } + } + // Populate all metadata into the ExtraAttrs first. + artifact.ExtraAttrs = metadata + annotationParser := annotation.NewParser() + err = annotationParser.Parse(ctx, artifact, manifest) + if err != nil { + log.Errorf("the annotation parser parse annotation for artifact error: %v", err) + } + + if artifact.Icon == "" { + artifact.Icon = DefaultIconDigest + } + 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. // return error directly return nil, errors.New(nil).WithCode(errors.BadRequestCode). WithMessage("the processor for artifact %s not found, cannot get the addition", artifact.Type) diff --git a/src/controller/artifact/processor/default_test.go b/src/controller/artifact/processor/default_test.go index 61ca127dd..2bf48525e 100644 --- a/src/controller/artifact/processor/default_test.go +++ b/src/controller/artifact/processor/default_test.go @@ -15,39 +15,175 @@ package processor import ( - "github.com/stretchr/testify/suite" + "context" + "github.com/goharbor/harbor/src/pkg/distribution" + "github.com/goharbor/harbor/src/testing/mock" + v1 "github.com/opencontainers/image-spec/specs-go/v1" + "io/ioutil" + "strings" "testing" + + "github.com/goharbor/harbor/src/pkg/artifact" + "github.com/goharbor/harbor/src/testing/pkg/parser" + "github.com/goharbor/harbor/src/testing/pkg/registry" + + "github.com/stretchr/testify/suite" +) + +var ( + ormbConfig = `{ + "created": "2015-10-31T22:22:56.015925234Z", + "author": "Ce Gao ", + "description": "CNN Model", + "tags": [ + "cv" + ], + "labels": { + "tensorflow.version": "2.0.0" + }, + "framework": "TensorFlow", + "format": "SavedModel", + "size": 9223372036854775807, + "metrics": [ + { + "name": "acc", + "value": "0.9" + } + ], + "hyperparameters": [ + { + "name": "batch_size", + "value": "32" + } + ], + "signature": { + "inputs": [ + { + "name": "input_1", + "size": [ + 224, + 224, + 3 + ], + "dtype": "float64" + } + ], + "outputs": [ + { + "name": "output_1", + "size": [ + 1, + 1000 + ], + "dtype": "float64" + } + ], + "layers": [ + { + "name": "conv" + } + ] + }, + "training": { + "git": { + "repository": "git@github.com:caicloud/ormb.git", + "revision": "22f1d8406d464b0c0874075539c1f2e96c253775" + } + }, + "dataset": { + "git": { + "repository": "git@github.com:caicloud/ormb.git", + "revision": "22f1d8406d464b0c0874075539c1f2e96c253775" + } + } +}` + ormbManifestWithoutIcon = `{ + "schemaVersion":2, + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "config":{ + "mediaType":"application/vnd.caicloud.model.config.v1alpha1+json", + "digest":"sha256:be948daf0e22f264ea70b713ea0db35050ae659c185706aa2fad74834455fe8c", + "size":187, + "annotations": { + "io.goharbor.artifact.v1alpha1.skip-list": "metrics,git" + } + }, + "layers":[ + { + "mediaType":"application/tar+gzip", + "digest":"sha256:eb6063fecbb50a9d98268cb61746a0fd62a27a4af9e850ffa543a1a62d3948b2", + "size":166022 + } + ] +}` ) type defaultProcessorTestSuite struct { suite.Suite + processor *defaultProcessor + parser *parser.Parser + regCli *registry.FakeClient +} + +func (d *defaultProcessorTestSuite) SetupTest() { + d.regCli = ®istry.FakeClient{} + d.processor = &defaultProcessor{ + regCli: d.regCli, + } + d.parser = &parser.Parser{} } func (d *defaultProcessorTestSuite) TestGetArtifactType() { mediaType := "" - processor := &defaultProcessor{mediaType: mediaType} - typee := processor.GetArtifactType(nil, nil) + art := &artifact.Artifact{MediaType: mediaType} + processor := &defaultProcessor{} + typee := processor.GetArtifactType(nil, art) d.Equal(ArtifactTypeUnknown, typee) mediaType = "unknown" - processor = &defaultProcessor{mediaType: mediaType} - typee = processor.GetArtifactType(nil, nil) + art = &artifact.Artifact{MediaType: mediaType} + processor = &defaultProcessor{} + typee = processor.GetArtifactType(nil, art) d.Equal(ArtifactTypeUnknown, typee) mediaType = "application/vnd.oci.image.config.v1+json" - processor = &defaultProcessor{mediaType: mediaType} - typee = processor.GetArtifactType(nil, nil) + art = &artifact.Artifact{MediaType: mediaType} + processor = &defaultProcessor{} + typee = processor.GetArtifactType(nil, art) d.Equal("IMAGE", typee) mediaType = "application/vnd.cncf.helm.chart.config.v1+json" - processor = &defaultProcessor{mediaType: mediaType} - typee = processor.GetArtifactType(nil, nil) + art = &artifact.Artifact{MediaType: mediaType} + processor = &defaultProcessor{} + typee = processor.GetArtifactType(nil, art) d.Equal("HELM.CHART", typee) mediaType = "application/vnd.sylabs.sif.config.v1+json" - processor = &defaultProcessor{mediaType: mediaType} - typee = processor.GetArtifactType(nil, nil) + art = &artifact.Artifact{MediaType: mediaType} + processor = &defaultProcessor{} + typee = processor.GetArtifactType(nil, art) d.Equal("SIF", typee) + + mediaType = "application/vnd.caicloud.model.config.v1alpha1+json" + art = &artifact.Artifact{MediaType: mediaType} + processor = &defaultProcessor{} + typee = processor.GetArtifactType(nil, art) + d.Equal("MODEL", typee) +} + +func (d *defaultProcessorTestSuite) TestAbstractMetadata() { + manifest, _, err := distribution.UnmarshalManifest(v1.MediaTypeImageManifest, []byte(ormbManifestWithoutIcon)) + d.Require().Nil(err) + manifestMediaType, content, err := manifest.Payload() + d.Require().Nil(err) + + configBlob := ioutil.NopCloser(strings.NewReader(ormbConfig)) + art := &artifact.Artifact{ManifestMediaType: manifestMediaType} + d.regCli.On("PullBlob").Return(0, configBlob, nil) + 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) } func TestDefaultProcessorTestSuite(t *testing.T) { diff --git a/src/controller/artifact/processor/processor.go b/src/controller/artifact/processor/processor.go index a59734ceb..7cd41592a 100644 --- a/src/controller/artifact/processor/processor.go +++ b/src/controller/artifact/processor/processor.go @@ -17,6 +17,7 @@ package processor import ( "context" "fmt" + "github.com/goharbor/harbor/src/lib/log" "github.com/goharbor/harbor/src/pkg/artifact" ) @@ -66,7 +67,7 @@ func Get(mediaType string) Processor { // no registered processor found, use the default one if processor == nil { log.Debugf("the processor for media type %s not found, use the default one", mediaType) - processor = &defaultProcessor{mediaType: mediaType} + processor = DefaultProcessor } return processor } diff --git a/src/pkg/artifact/dao/model.go b/src/pkg/artifact/dao/model.go index 2ce24dc7e..e2bb55b4a 100644 --- a/src/pkg/artifact/dao/model.go +++ b/src/pkg/artifact/dao/model.go @@ -40,6 +40,7 @@ type Artifact struct { PullTime time.Time `orm:"column(pull_time)"` ExtraAttrs string `orm:"column(extra_attrs)"` // json string Annotations string `orm:"column(annotations);type(jsonb)"` // json string + Icon string `orm:"column(icon)"` // icon layer digest } // TableName for artifact diff --git a/src/pkg/artifact/model.go b/src/pkg/artifact/model.go index d922a0434..757ea68cf 100644 --- a/src/pkg/artifact/model.go +++ b/src/pkg/artifact/model.go @@ -43,6 +43,7 @@ type Artifact struct { ExtraAttrs map[string]interface{} `json:"extra_attrs"` // only contains the simple attributes specific for the different artifact type, most of them should come from the config layer Annotations map[string]string `json:"annotations"` References []*Reference `json:"references"` // child artifacts referenced by the parent artifact if the artifact is an index + Icon string `json:"icon"` // icon layer digest } // IsImageIndex returns true when artifact is image index diff --git a/src/testing/pkg/parser/parser.go b/src/testing/pkg/parser/parser.go new file mode 100644 index 000000000..2afbbf1ad --- /dev/null +++ b/src/testing/pkg/parser/parser.go @@ -0,0 +1,30 @@ +// Code generated by mockery v2.1.0. DO NOT EDIT. + +package parser + +import ( + context "context" + + artifact "github.com/goharbor/harbor/src/pkg/artifact" + + mock "github.com/stretchr/testify/mock" +) + +// Parser is an autogenerated mock type for the Parser type +type Parser struct { + mock.Mock +} + +// Parse provides a mock function with given fields: ctx, _a1, manifest +func (_m *Parser) Parse(ctx context.Context, _a1 *artifact.Artifact, manifest []byte) error { + ret := _m.Called(ctx, _a1, manifest) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *artifact.Artifact, []byte) error); ok { + r0 = rf(ctx, _a1, manifest) + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/src/testing/pkg/processor/processor.go b/src/testing/pkg/processor/processor.go new file mode 100644 index 000000000..f300de73e --- /dev/null +++ b/src/testing/pkg/processor/processor.go @@ -0,0 +1,85 @@ +// Code generated by mockery v2.1.0. DO NOT EDIT. + +package processor + +import ( + context "context" + + artifact "github.com/goharbor/harbor/src/pkg/artifact" + + mock "github.com/stretchr/testify/mock" + + processor "github.com/goharbor/harbor/src/controller/artifact/processor" +) + +// Processor is an autogenerated mock type for the Processor type +type Processor struct { + mock.Mock +} + +// AbstractAddition provides a mock function with given fields: ctx, _a1, additionType +func (_m *Processor) AbstractAddition(ctx context.Context, _a1 *artifact.Artifact, additionType string) (*processor.Addition, error) { + ret := _m.Called(ctx, _a1, additionType) + + var r0 *processor.Addition + if rf, ok := ret.Get(0).(func(context.Context, *artifact.Artifact, string) *processor.Addition); ok { + r0 = rf(ctx, _a1, additionType) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*processor.Addition) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *artifact.Artifact, string) error); ok { + r1 = rf(ctx, _a1, additionType) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// AbstractMetadata provides a mock function with given fields: ctx, _a1, manifest +func (_m *Processor) AbstractMetadata(ctx context.Context, _a1 *artifact.Artifact, manifest []byte) error { + ret := _m.Called(ctx, _a1, manifest) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *artifact.Artifact, []byte) error); ok { + r0 = rf(ctx, _a1, manifest) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// GetArtifactType provides a mock function with given fields: ctx, _a1 +func (_m *Processor) GetArtifactType(ctx context.Context, _a1 *artifact.Artifact) string { + ret := _m.Called(ctx, _a1) + + var r0 string + if rf, ok := ret.Get(0).(func(context.Context, *artifact.Artifact) string); ok { + r0 = rf(ctx, _a1) + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// ListAdditionTypes provides a mock function with given fields: ctx, _a1 +func (_m *Processor) ListAdditionTypes(ctx context.Context, _a1 *artifact.Artifact) []string { + ret := _m.Called(ctx, _a1) + + var r0 []string + if rf, ok := ret.Get(0).(func(context.Context, *artifact.Artifact) []string); ok { + r0 = rf(ctx, _a1) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + + return r0 +}