mirror of
https://github.com/goharbor/harbor.git
synced 2024-12-19 15:17:43 +01:00
Implement the artifact abstractor and resolver
1. Define the interface for artifact abstractor and resolver 2. Implement the artifact abstractor 3. Implement the resolver for image with manifest v2 Signed-off-by: Wenkai Yin <yinw@vmware.com>
This commit is contained in:
parent
e2bab855ac
commit
df551e1310
118
src/api/artifact/abstractor/abstractor.go
Normal file
118
src/api/artifact/abstractor/abstractor.go
Normal file
@ -0,0 +1,118 @@
|
||||
// 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 abstractor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/docker/distribution/manifest/manifestlist"
|
||||
"github.com/docker/distribution/manifest/schema1"
|
||||
"github.com/docker/distribution/manifest/schema2"
|
||||
"github.com/goharbor/harbor/src/api/artifact/abstractor/blob"
|
||||
"github.com/goharbor/harbor/src/api/artifact/abstractor/resolver"
|
||||
"github.com/goharbor/harbor/src/pkg/artifact"
|
||||
"github.com/goharbor/harbor/src/pkg/repository"
|
||||
"github.com/opencontainers/image-spec/specs-go/v1"
|
||||
)
|
||||
|
||||
// Abstractor abstracts the specific information for different types of artifacts
|
||||
type Abstractor interface {
|
||||
// Abstract the specific information for the specific artifact type into the artifact model,
|
||||
// the information can be got from the manifest or other layers referenced by the manifest.
|
||||
Abstract(ctx context.Context, artifact *artifact.Artifact) error
|
||||
}
|
||||
|
||||
// NewAbstractor returns an instance of the default abstractor
|
||||
func NewAbstractor() Abstractor {
|
||||
return &abstractor{
|
||||
repoMgr: repository.Mgr,
|
||||
blobFetcher: blob.Fcher,
|
||||
}
|
||||
}
|
||||
|
||||
type abstractor struct {
|
||||
repoMgr repository.Manager
|
||||
blobFetcher blob.Fetcher
|
||||
}
|
||||
|
||||
// TODO try CNAB, how to forbid CNAB
|
||||
|
||||
// TODO add white list for supported artifact type
|
||||
func (a *abstractor) Abstract(ctx context.Context, artifact *artifact.Artifact) error {
|
||||
repository, err := a.repoMgr.Get(ctx, artifact.RepositoryID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// read manifest content
|
||||
manifestMediaType, content, err := a.blobFetcher.FetchManifest(repository.Name, artifact.Digest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
artifact.ManifestMediaType = manifestMediaType
|
||||
|
||||
switch artifact.ManifestMediaType {
|
||||
// docker manifest v1
|
||||
case "", "application/json", schema1.MediaTypeSignedManifest:
|
||||
// TODO as the manifestmediatype isn't null, so add not null constraint in database
|
||||
// unify the media type of v1 manifest to "schema1.MediaTypeSignedManifest"
|
||||
artifact.ManifestMediaType = schema1.MediaTypeSignedManifest
|
||||
// as no config layer in the docker v1 manifest, use the "schema1.MediaTypeSignedManifest"
|
||||
// as the media type of artifact
|
||||
artifact.MediaType = schema1.MediaTypeSignedManifest
|
||||
// there is no layer size in v1 manifest, doesn't set the artifact size
|
||||
// OCI manifest/docker manifest v2
|
||||
case v1.MediaTypeImageManifest, schema2.MediaTypeManifest:
|
||||
manifest := &v1.Manifest{}
|
||||
if err := json.Unmarshal(content, manifest); err != nil {
|
||||
return err
|
||||
}
|
||||
// use the "manifest.config.mediatype" as the media type of the artifact
|
||||
artifact.MediaType = manifest.Config.MediaType
|
||||
// set size
|
||||
artifact.Size = int64(len(content)) + manifest.Config.Size
|
||||
for _, layer := range manifest.Layers {
|
||||
artifact.Size += layer.Size
|
||||
}
|
||||
// set annotations
|
||||
artifact.Annotations = manifest.Annotations
|
||||
// OCI index/docker manifest list
|
||||
case v1.MediaTypeImageIndex, manifestlist.MediaTypeManifestList:
|
||||
// the identity of index is still in progress, only handle image index for now
|
||||
// and use the manifestMediaType as the media type of artifact
|
||||
// If we want to support CNAB, we should get the media type from annotation
|
||||
artifact.MediaType = artifact.ManifestMediaType
|
||||
|
||||
index := &v1.Index{}
|
||||
if err := json.Unmarshal(content, index); err != nil {
|
||||
return err
|
||||
}
|
||||
// the size for image index is meaningless, doesn't set it for image index
|
||||
// but it is useful for CNAB or other artifacts, set it when needed
|
||||
|
||||
// set annotations
|
||||
artifact.Annotations = index.Annotations
|
||||
// TODO handle references in resolvers
|
||||
default:
|
||||
return fmt.Errorf("unsupported manifest media type: %s", artifact.ManifestMediaType)
|
||||
}
|
||||
|
||||
resolver, err := resolver.Get(artifact.MediaType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
artifact.Type = resolver.ArtifactType()
|
||||
return resolver.Resolve(ctx, content, artifact)
|
||||
}
|
299
src/api/artifact/abstractor/abstractor_test.go
Normal file
299
src/api/artifact/abstractor/abstractor_test.go
Normal file
@ -0,0 +1,299 @@
|
||||
// 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 abstractor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/docker/distribution/manifest/schema1"
|
||||
"github.com/docker/distribution/manifest/schema2"
|
||||
"github.com/goharbor/harbor/src/api/artifact/abstractor/resolver"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/pkg/artifact"
|
||||
htesting "github.com/goharbor/harbor/src/testing"
|
||||
"github.com/goharbor/harbor/src/testing/api/artifact/abstractor/blob"
|
||||
v1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var (
|
||||
fakeArtifactType = "FAKE_ARTIFACT"
|
||||
v1Manifest = `{
|
||||
"name": "hello-world",
|
||||
"tag": "latest",
|
||||
"architecture": "amd64",
|
||||
"fsLayers": [
|
||||
{
|
||||
"blobSum": "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"
|
||||
},
|
||||
{
|
||||
"blobSum": "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"
|
||||
},
|
||||
{
|
||||
"blobSum": "sha256:cc8567d70002e957612902a8e985ea129d831ebe04057d88fb644857caa45d11"
|
||||
},
|
||||
{
|
||||
"blobSum": "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"
|
||||
}
|
||||
],
|
||||
"history": [
|
||||
{
|
||||
"v1Compatibility": "{\"id\":\"e45a5af57b00862e5ef5782a9925979a02ba2b12dff832fd0991335f4a11e5c5\",\"parent\":\"31cbccb51277105ba3ae35ce33c22b69c9e3f1002e76e4c736a2e8ebff9d7b5d\",\"created\":\"2014-12-31T22:57:59.178729048Z\",\"container\":\"27b45f8fb11795b52e9605b686159729b0d9ca92f76d40fb4f05a62e19c46b4f\",\"container_config\":{\"Hostname\":\"8ce6509d66e2\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"],\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) CMD [/hello]\"],\"Image\":\"31cbccb51277105ba3ae35ce33c22b69c9e3f1002e76e4c736a2e8ebff9d7b5d\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"SecurityOpt\":null,\"Labels\":null},\"docker_version\":\"1.4.1\",\"config\":{\"Hostname\":\"8ce6509d66e2\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"],\"Cmd\":[\"/hello\"],\"Image\":\"31cbccb51277105ba3ae35ce33c22b69c9e3f1002e76e4c736a2e8ebff9d7b5d\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"SecurityOpt\":null,\"Labels\":null},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n"
|
||||
},
|
||||
{
|
||||
"v1Compatibility": "{\"id\":\"e45a5af57b00862e5ef5782a9925979a02ba2b12dff832fd0991335f4a11e5c5\",\"parent\":\"31cbccb51277105ba3ae35ce33c22b69c9e3f1002e76e4c736a2e8ebff9d7b5d\",\"created\":\"2014-12-31T22:57:59.178729048Z\",\"container\":\"27b45f8fb11795b52e9605b686159729b0d9ca92f76d40fb4f05a62e19c46b4f\",\"container_config\":{\"Hostname\":\"8ce6509d66e2\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"],\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) CMD [/hello]\"],\"Image\":\"31cbccb51277105ba3ae35ce33c22b69c9e3f1002e76e4c736a2e8ebff9d7b5d\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"SecurityOpt\":null,\"Labels\":null},\"docker_version\":\"1.4.1\",\"config\":{\"Hostname\":\"8ce6509d66e2\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"],\"Cmd\":[\"/hello\"],\"Image\":\"31cbccb51277105ba3ae35ce33c22b69c9e3f1002e76e4c736a2e8ebff9d7b5d\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"SecurityOpt\":null,\"Labels\":null},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n"
|
||||
},
|
||||
],
|
||||
"schemaVersion": 1,
|
||||
"signatures": [
|
||||
{
|
||||
"header": {
|
||||
"jwk": {
|
||||
"crv": "P-256",
|
||||
"kid": "OD6I:6DRK:JXEJ:KBM4:255X:NSAA:MUSF:E4VM:ZI6W:CUN2:L4Z6:LSF4",
|
||||
"kty": "EC",
|
||||
"x": "3gAwX48IQ5oaYQAYSxor6rYYc_6yjuLCjtQ9LUakg4A",
|
||||
"y": "t72ge6kIA1XOjqjVoEOiPPAURltJFBMGDSQvEGVB010"
|
||||
},
|
||||
"alg": "ES256"
|
||||
},
|
||||
"signature": "XREm0L8WNn27Ga_iE_vRnTxVMhhYY0Zst_FfkKopg6gWSoTOZTuW4rK0fg_IqnKkEKlbD83tD46LKEGi5aIVFg",
|
||||
"protected": "eyJmb3JtYXRMZW5ndGgiOjY2MjgsImZvcm1hdFRhaWwiOiJDbjAiLCJ0aW1lIjoiMjAxNS0wNC0wOFQxODo1Mjo1OVoifQ"
|
||||
}
|
||||
]
|
||||
}`
|
||||
v2Manifest = `{
|
||||
"schemaVersion": 2,
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
|
||||
"config": {
|
||||
"mediaType": "application/vnd.docker.container.image.v1+json",
|
||||
"size": 1510,
|
||||
"digest": "sha256:fce289e99eb9bca977dae136fbe2a82b6b7d4c372474c9235adc1741675f587e"
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
|
||||
"size": 977,
|
||||
"digest": "sha256:1b930d010525941c1d56ec53b97bd057a67ae1865eebf042686d2a2d18271ced"
|
||||
}
|
||||
],
|
||||
"annotations": {
|
||||
"com.example.key1": "value1"
|
||||
}
|
||||
}`
|
||||
v2Config = `{
|
||||
"architecture": "amd64",
|
||||
"config": {
|
||||
"Hostname": "",
|
||||
"Domainname": "",
|
||||
"User": "",
|
||||
"AttachStdin": false,
|
||||
"AttachStdout": false,
|
||||
"AttachStderr": false,
|
||||
"Tty": false,
|
||||
"OpenStdin": false,
|
||||
"StdinOnce": false,
|
||||
"Env": [
|
||||
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
],
|
||||
"Cmd": [
|
||||
"/hello"
|
||||
],
|
||||
"ArgsEscaped": true,
|
||||
"Image": "sha256:a6d1aaad8ca65655449a26146699fe9d61240071f6992975be7e720f1cd42440",
|
||||
"Volumes": null,
|
||||
"WorkingDir": "",
|
||||
"Entrypoint": null,
|
||||
"OnBuild": null,
|
||||
"Labels": null
|
||||
},
|
||||
"container": "8e2caa5a514bb6d8b4f2a2553e9067498d261a0fd83a96aeaaf303943dff6ff9",
|
||||
"container_config": {
|
||||
"Hostname": "8e2caa5a514b",
|
||||
"Domainname": "",
|
||||
"User": "",
|
||||
"AttachStdin": false,
|
||||
"AttachStdout": false,
|
||||
"AttachStderr": false,
|
||||
"Tty": false,
|
||||
"OpenStdin": false,
|
||||
"StdinOnce": false,
|
||||
"Env": [
|
||||
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
],
|
||||
"Cmd": [
|
||||
"/bin/sh",
|
||||
"-c",
|
||||
"#(nop) ",
|
||||
"CMD [\"/hello\"]"
|
||||
],
|
||||
"ArgsEscaped": true,
|
||||
"Image": "sha256:a6d1aaad8ca65655449a26146699fe9d61240071f6992975be7e720f1cd42440",
|
||||
"Volumes": null,
|
||||
"WorkingDir": "",
|
||||
"Entrypoint": null,
|
||||
"OnBuild": null,
|
||||
"Labels": {
|
||||
|
||||
}
|
||||
},
|
||||
"created": "2019-01-01T01:29:27.650294696Z",
|
||||
"docker_version": "18.06.1-ce",
|
||||
"history": [
|
||||
{
|
||||
"created": "2019-01-01T01:29:27.416803627Z",
|
||||
"created_by": "/bin/sh -c #(nop) COPY file:f77490f70ce51da25bd21bfc30cb5e1a24b2b65eb37d4af0c327ddc24f0986a6 in / "
|
||||
},
|
||||
{
|
||||
"created": "2019-01-01T01:29:27.650294696Z",
|
||||
"created_by": "/bin/sh -c #(nop) CMD [\"/hello\"]",
|
||||
"empty_layer": true
|
||||
}
|
||||
],
|
||||
"os": "linux",
|
||||
"rootfs": {
|
||||
"type": "layers",
|
||||
"diff_ids": [
|
||||
"sha256:af0b15c8625bb1938f1d7b17081031f649fd14e6b233688eea3c5483994a66a3"
|
||||
]
|
||||
}
|
||||
}`
|
||||
index = `{
|
||||
"schemaVersion": 2,
|
||||
"manifests": [
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"size": 7143,
|
||||
"digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f",
|
||||
"platform": {
|
||||
"architecture": "ppc64le",
|
||||
"os": "linux"
|
||||
}
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"size": 7682,
|
||||
"digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270",
|
||||
"platform": {
|
||||
"architecture": "amd64",
|
||||
"os": "linux"
|
||||
}
|
||||
}
|
||||
],
|
||||
"annotations": {
|
||||
"com.example.key1": "value1"
|
||||
}
|
||||
}`
|
||||
)
|
||||
|
||||
type fakeResolver struct{}
|
||||
|
||||
func (f *fakeResolver) ArtifactType() string {
|
||||
return fakeArtifactType
|
||||
|
||||
}
|
||||
func (f *fakeResolver) Resolve(ctx context.Context, manifest []byte, artifact *artifact.Artifact) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type abstractorTestSuite struct {
|
||||
suite.Suite
|
||||
abstractor Abstractor
|
||||
fetcher *blob.FakeFetcher
|
||||
repoMgr *htesting.FakeRepositoryManager
|
||||
}
|
||||
|
||||
func (a *abstractorTestSuite) SetupSuite() {
|
||||
resolver.Register(&fakeResolver{}, schema1.MediaTypeSignedManifest,
|
||||
schema2.MediaTypeImageConfig, v1.MediaTypeImageIndex)
|
||||
}
|
||||
|
||||
func (a *abstractorTestSuite) SetupTest() {
|
||||
a.fetcher = &blob.FakeFetcher{}
|
||||
a.repoMgr = &htesting.FakeRepositoryManager{}
|
||||
a.abstractor = &abstractor{
|
||||
repoMgr: a.repoMgr,
|
||||
blobFetcher: a.fetcher,
|
||||
}
|
||||
}
|
||||
|
||||
// docker manifest v1
|
||||
func (a *abstractorTestSuite) TestAbstractV1Manifest() {
|
||||
a.repoMgr.On("Get").Return(&models.RepoRecord{}, nil)
|
||||
a.fetcher.On("FetchManifest").Return(schema1.MediaTypeSignedManifest, []byte(v1Manifest), nil)
|
||||
artifact := &artifact.Artifact{
|
||||
ID: 1,
|
||||
}
|
||||
err := a.abstractor.Abstract(nil, artifact)
|
||||
a.Require().Nil(err)
|
||||
a.repoMgr.AssertExpectations(a.T())
|
||||
a.fetcher.AssertExpectations(a.T())
|
||||
a.Assert().Equal(int64(1), artifact.ID)
|
||||
a.Assert().Equal(fakeArtifactType, artifact.Type)
|
||||
a.Assert().Equal(schema1.MediaTypeSignedManifest, artifact.ManifestMediaType)
|
||||
a.Assert().Equal(schema1.MediaTypeSignedManifest, artifact.MediaType)
|
||||
a.Assert().Equal(int64(0), artifact.Size)
|
||||
}
|
||||
|
||||
// docker manifest v2
|
||||
func (a *abstractorTestSuite) TestAbstractV2Manifest() {
|
||||
a.repoMgr.On("Get").Return(&models.RepoRecord{}, nil)
|
||||
a.fetcher.On("FetchManifest").Return(schema2.MediaTypeManifest, []byte(v2Manifest), nil)
|
||||
artifact := &artifact.Artifact{
|
||||
ID: 1,
|
||||
}
|
||||
|
||||
err := a.abstractor.Abstract(nil, artifact)
|
||||
a.Require().Nil(err)
|
||||
a.repoMgr.AssertExpectations(a.T())
|
||||
a.fetcher.AssertExpectations(a.T())
|
||||
a.Assert().Equal(int64(1), artifact.ID)
|
||||
a.Assert().Equal(fakeArtifactType, artifact.Type)
|
||||
a.Assert().Equal(schema2.MediaTypeManifest, artifact.ManifestMediaType)
|
||||
a.Assert().Equal(schema2.MediaTypeImageConfig, artifact.MediaType)
|
||||
a.Assert().Equal(int64(3043), artifact.Size)
|
||||
}
|
||||
|
||||
// OCI index
|
||||
func (a *abstractorTestSuite) TestAbstractIndex() {
|
||||
a.repoMgr.On("Get").Return(&models.RepoRecord{}, nil)
|
||||
a.fetcher.On("FetchManifest").Return(v1.MediaTypeImageIndex, []byte(index), nil)
|
||||
artifact := &artifact.Artifact{
|
||||
ID: 1,
|
||||
}
|
||||
err := a.abstractor.Abstract(nil, artifact)
|
||||
a.Require().Nil(err)
|
||||
a.repoMgr.AssertExpectations(a.T())
|
||||
a.fetcher.AssertExpectations(a.T())
|
||||
a.Assert().Equal(int64(1), artifact.ID)
|
||||
a.Assert().Equal(fakeArtifactType, artifact.Type)
|
||||
a.Assert().Equal(v1.MediaTypeImageIndex, artifact.ManifestMediaType)
|
||||
a.Assert().Equal(v1.MediaTypeImageIndex, artifact.MediaType)
|
||||
a.Assert().Equal(int64(0), artifact.Size)
|
||||
a.Assert().Equal("value1", artifact.Annotations["com.example.key1"])
|
||||
}
|
||||
|
||||
// OCI index
|
||||
func (a *abstractorTestSuite) TestAbstractUnsupported() {
|
||||
a.repoMgr.On("Get").Return(&models.RepoRecord{}, nil)
|
||||
a.fetcher.On("FetchManifest").Return("unsupported-manifest", []byte{}, nil)
|
||||
artifact := &artifact.Artifact{
|
||||
ID: 1,
|
||||
}
|
||||
err := a.abstractor.Abstract(nil, artifact)
|
||||
a.Require().NotNil(err)
|
||||
a.repoMgr.AssertExpectations(a.T())
|
||||
a.fetcher.AssertExpectations(a.T())
|
||||
}
|
||||
|
||||
func TestAbstractorTestSuite(t *testing.T) {
|
||||
suite.Run(t, &abstractorTestSuite{})
|
||||
}
|
18
src/api/artifact/abstractor/blob/cache.go
Normal file
18
src/api/artifact/abstractor/blob/cache.go
Normal file
@ -0,0 +1,18 @@
|
||||
// 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 blob
|
||||
|
||||
// TODO add cache
|
||||
// TODO cache content and mediatype
|
78
src/api/artifact/abstractor/blob/fetcher.go
Normal file
78
src/api/artifact/abstractor/blob/fetcher.go
Normal file
@ -0,0 +1,78 @@
|
||||
// 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 blob
|
||||
|
||||
import (
|
||||
"github.com/docker/distribution/manifest/manifestlist"
|
||||
"github.com/docker/distribution/manifest/schema1"
|
||||
"github.com/docker/distribution/manifest/schema2"
|
||||
coreutils "github.com/goharbor/harbor/src/core/utils"
|
||||
v1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"io/ioutil"
|
||||
)
|
||||
|
||||
var (
|
||||
// Fcher is a global blob fetcher instance
|
||||
Fcher = NewFetcher()
|
||||
|
||||
accept = []string{
|
||||
schema1.MediaTypeSignedManifest,
|
||||
schema2.MediaTypeManifest,
|
||||
v1.MediaTypeImageManifest,
|
||||
manifestlist.MediaTypeManifestList,
|
||||
v1.MediaTypeImageIndex,
|
||||
}
|
||||
)
|
||||
|
||||
// Fetcher fetches the content of blob
|
||||
type Fetcher interface {
|
||||
// FetchManifest the content of manifest under the repository
|
||||
FetchManifest(repository, digest string) (mediaType string, content []byte, err error)
|
||||
// FetchLayer the content of layer under the repository
|
||||
FetchLayer(repository, digest string) (content []byte, err error)
|
||||
}
|
||||
|
||||
// NewFetcher returns an instance of the default blob fetcher
|
||||
func NewFetcher() Fetcher {
|
||||
return &fetcher{}
|
||||
}
|
||||
|
||||
type fetcher struct{}
|
||||
|
||||
// TODO re-implement it based on OCI registry driver
|
||||
func (f *fetcher) FetchManifest(repository, digest string) (string, []byte, error) {
|
||||
// TODO read from cache first
|
||||
client, err := coreutils.NewRepositoryClientForLocal("admin", repository)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
_, mediaType, payload, err := client.PullManifest(digest, accept)
|
||||
return mediaType, payload, err
|
||||
}
|
||||
|
||||
// TODO re-implement it based on OCI registry driver
|
||||
func (f *fetcher) FetchLayer(repository, digest string) ([]byte, error) {
|
||||
// TODO read from cache first
|
||||
client, err := coreutils.NewRepositoryClientForLocal("admin", repository)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, reader, err := client.PullBlob(digest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer reader.Close()
|
||||
return ioutil.ReadAll(reader)
|
||||
}
|
11
src/api/artifact/abstractor/resolver/chart/chart.go
Normal file
11
src/api/artifact/abstractor/resolver/chart/chart.go
Normal file
@ -0,0 +1,11 @@
|
||||
// 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 chart
|
42
src/api/artifact/abstractor/resolver/image/index.go
Normal file
42
src/api/artifact/abstractor/resolver/image/index.go
Normal file
@ -0,0 +1,42 @@
|
||||
// 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 image
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/docker/distribution/manifest/manifestlist"
|
||||
"github.com/goharbor/harbor/src/api/artifact/abstractor/resolver"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/pkg/artifact"
|
||||
v1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rslver := &indexResolver{}
|
||||
if err := resolver.Register(rslver, v1.MediaTypeImageIndex, manifestlist.MediaTypeManifestList); err != nil {
|
||||
log.Errorf("failed to register resolver for artifact %s: %v", rslver.ArtifactType(), err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// indexResolver resolves artifact with OCI index and docker manifest list
|
||||
type indexResolver struct {
|
||||
}
|
||||
|
||||
func (i *indexResolver) ArtifactType() string {
|
||||
return ArtifactTypeImage
|
||||
}
|
||||
|
||||
func (i *indexResolver) Resolve(ctx context.Context, manifest []byte, artifact *artifact.Artifact) error {
|
||||
// TODO implement
|
||||
// how to make sure the artifact referenced by the index has already been saved in database
|
||||
return nil
|
||||
}
|
40
src/api/artifact/abstractor/resolver/image/manifest_v1.go
Normal file
40
src/api/artifact/abstractor/resolver/image/manifest_v1.go
Normal file
@ -0,0 +1,40 @@
|
||||
// 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 image
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/docker/distribution/manifest/schema1"
|
||||
"github.com/goharbor/harbor/src/api/artifact/abstractor/resolver"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/pkg/artifact"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rslver := &manifestV1Resolver{}
|
||||
if err := resolver.Register(rslver, schema1.MediaTypeSignedManifest); err != nil {
|
||||
log.Errorf("failed to register resolver for artifact %s: %v", rslver.ArtifactType(), err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// manifestV1Resolver resolve artifact with docker v1 manifest
|
||||
type manifestV1Resolver struct {
|
||||
}
|
||||
|
||||
func (m *manifestV1Resolver) ArtifactType() string {
|
||||
return ArtifactTypeImage
|
||||
}
|
||||
|
||||
func (m *manifestV1Resolver) Resolve(ctx context.Context, manifest []byte, artifact *artifact.Artifact) error {
|
||||
// TODO implement
|
||||
return nil
|
||||
}
|
76
src/api/artifact/abstractor/resolver/image/manifest_v2.go
Normal file
76
src/api/artifact/abstractor/resolver/image/manifest_v2.go
Normal file
@ -0,0 +1,76 @@
|
||||
// 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 image
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"github.com/docker/distribution/manifest/schema2"
|
||||
"github.com/goharbor/harbor/src/api/artifact/abstractor/blob"
|
||||
"github.com/goharbor/harbor/src/api/artifact/abstractor/resolver"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/pkg/artifact"
|
||||
"github.com/goharbor/harbor/src/pkg/repository"
|
||||
"github.com/opencontainers/image-spec/specs-go/v1"
|
||||
)
|
||||
|
||||
const (
|
||||
// ArtifactTypeImage is the artifact type for image
|
||||
ArtifactTypeImage = "IMAGE"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rslver := &manifestV2Resolver{
|
||||
repoMgr: repository.Mgr,
|
||||
blobFetcher: blob.Fcher,
|
||||
}
|
||||
if err := resolver.Register(rslver, v1.MediaTypeImageConfig, schema2.MediaTypeImageConfig); err != nil {
|
||||
log.Errorf("failed to register resolver for artifact %s: %v", rslver.ArtifactType(), err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// manifestV2Resolver resolve artifact with OCI manifest and docker v2 manifest
|
||||
type manifestV2Resolver struct {
|
||||
repoMgr repository.Manager
|
||||
blobFetcher blob.Fetcher
|
||||
}
|
||||
|
||||
func (m *manifestV2Resolver) ArtifactType() string {
|
||||
return ArtifactTypeImage
|
||||
}
|
||||
|
||||
func (m *manifestV2Resolver) Resolve(ctx context.Context, content []byte, artifact *artifact.Artifact) error {
|
||||
repository, err := m.repoMgr.Get(ctx, artifact.RepositoryID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
manifest := &v1.Manifest{}
|
||||
if err := json.Unmarshal(content, manifest); err != nil {
|
||||
return err
|
||||
}
|
||||
digest := manifest.Config.Digest.String()
|
||||
layer, err := m.blobFetcher.FetchLayer(repository.Name, digest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
image := &v1.Image{}
|
||||
if err := json.Unmarshal(layer, image); err != nil {
|
||||
return err
|
||||
}
|
||||
artifact.ExtraAttrs = map[string]interface{}{
|
||||
"created": image.Created,
|
||||
"author": image.Author,
|
||||
"architecture": image.Architecture,
|
||||
"os": image.OS,
|
||||
}
|
||||
return nil
|
||||
}
|
150
src/api/artifact/abstractor/resolver/image/manifest_v2_test.go
Normal file
150
src/api/artifact/abstractor/resolver/image/manifest_v2_test.go
Normal file
@ -0,0 +1,150 @@
|
||||
// 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 image
|
||||
|
||||
import (
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/pkg/artifact"
|
||||
htesting "github.com/goharbor/harbor/src/testing"
|
||||
"github.com/goharbor/harbor/src/testing/api/artifact/abstractor/blob"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type manifestV2ResolverTestSuite struct {
|
||||
suite.Suite
|
||||
resolver *manifestV2Resolver
|
||||
repoMgr *htesting.FakeRepositoryManager
|
||||
blobFetcher *blob.FakeFetcher
|
||||
}
|
||||
|
||||
func (m *manifestV2ResolverTestSuite) SetupTest() {
|
||||
m.repoMgr = &htesting.FakeRepositoryManager{}
|
||||
m.blobFetcher = &blob.FakeFetcher{}
|
||||
m.resolver = &manifestV2Resolver{
|
||||
repoMgr: m.repoMgr,
|
||||
blobFetcher: m.blobFetcher,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (m *manifestV2ResolverTestSuite) TestArtifactType() {
|
||||
m.Assert().Equal(ArtifactTypeImage, m.resolver.ArtifactType())
|
||||
}
|
||||
|
||||
func (m *manifestV2ResolverTestSuite) TestResolve() {
|
||||
content := `{
|
||||
"schemaVersion": 2,
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
|
||||
"config": {
|
||||
"mediaType": "application/vnd.docker.container.image.v1+json",
|
||||
"size": 1510,
|
||||
"digest": "sha256:fce289e99eb9bca977dae136fbe2a82b6b7d4c372474c9235adc1741675f587e"
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
|
||||
"size": 977,
|
||||
"digest": "sha256:1b930d010525941c1d56ec53b97bd057a67ae1865eebf042686d2a2d18271ced"
|
||||
}
|
||||
]
|
||||
}`
|
||||
config := `{
|
||||
"architecture": "amd64",
|
||||
"config": {
|
||||
"Hostname": "",
|
||||
"Domainname": "",
|
||||
"User": "",
|
||||
"AttachStdin": false,
|
||||
"AttachStdout": false,
|
||||
"AttachStderr": false,
|
||||
"Tty": false,
|
||||
"OpenStdin": false,
|
||||
"StdinOnce": false,
|
||||
"Env": [
|
||||
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
],
|
||||
"Cmd": [
|
||||
"/hello"
|
||||
],
|
||||
"ArgsEscaped": true,
|
||||
"Image": "sha256:a6d1aaad8ca65655449a26146699fe9d61240071f6992975be7e720f1cd42440",
|
||||
"Volumes": null,
|
||||
"WorkingDir": "",
|
||||
"Entrypoint": null,
|
||||
"OnBuild": null,
|
||||
"Labels": null
|
||||
},
|
||||
"container": "8e2caa5a514bb6d8b4f2a2553e9067498d261a0fd83a96aeaaf303943dff6ff9",
|
||||
"container_config": {
|
||||
"Hostname": "8e2caa5a514b",
|
||||
"Domainname": "",
|
||||
"User": "",
|
||||
"AttachStdin": false,
|
||||
"AttachStdout": false,
|
||||
"AttachStderr": false,
|
||||
"Tty": false,
|
||||
"OpenStdin": false,
|
||||
"StdinOnce": false,
|
||||
"Env": [
|
||||
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
],
|
||||
"Cmd": [
|
||||
"/bin/sh",
|
||||
"-c",
|
||||
"#(nop) ",
|
||||
"CMD [\"/hello\"]"
|
||||
],
|
||||
"ArgsEscaped": true,
|
||||
"Image": "sha256:a6d1aaad8ca65655449a26146699fe9d61240071f6992975be7e720f1cd42440",
|
||||
"Volumes": null,
|
||||
"WorkingDir": "",
|
||||
"Entrypoint": null,
|
||||
"OnBuild": null,
|
||||
"Labels": {
|
||||
|
||||
}
|
||||
},
|
||||
"created": "2019-01-01T01:29:27.650294696Z",
|
||||
"docker_version": "18.06.1-ce",
|
||||
"history": [
|
||||
{
|
||||
"created": "2019-01-01T01:29:27.416803627Z",
|
||||
"created_by": "/bin/sh -c #(nop) COPY file:f77490f70ce51da25bd21bfc30cb5e1a24b2b65eb37d4af0c327ddc24f0986a6 in / "
|
||||
},
|
||||
{
|
||||
"created": "2019-01-01T01:29:27.650294696Z",
|
||||
"created_by": "/bin/sh -c #(nop) CMD [\"/hello\"]",
|
||||
"empty_layer": true
|
||||
}
|
||||
],
|
||||
"os": "linux",
|
||||
"rootfs": {
|
||||
"type": "layers",
|
||||
"diff_ids": [
|
||||
"sha256:af0b15c8625bb1938f1d7b17081031f649fd14e6b233688eea3c5483994a66a3"
|
||||
]
|
||||
}
|
||||
}`
|
||||
artifact := &artifact.Artifact{}
|
||||
m.repoMgr.On("Get").Return(&models.RepoRecord{}, nil)
|
||||
m.blobFetcher.On("FetchLayer").Return([]byte(config), nil)
|
||||
err := m.resolver.Resolve(nil, []byte(content), artifact)
|
||||
m.Require().Nil(err)
|
||||
m.repoMgr.AssertExpectations(m.T())
|
||||
m.blobFetcher.AssertExpectations(m.T())
|
||||
m.Assert().Equal("amd64", artifact.ExtraAttrs["architecture"].(string))
|
||||
m.Assert().Equal("linux", artifact.ExtraAttrs["os"].(string))
|
||||
}
|
||||
|
||||
func TestManifestV2ResolverTestSuite(t *testing.T) {
|
||||
suite.Run(t, &manifestV2ResolverTestSuite{})
|
||||
}
|
58
src/api/artifact/abstractor/resolver/resolver.go
Normal file
58
src/api/artifact/abstractor/resolver/resolver.go
Normal file
@ -0,0 +1,58 @@
|
||||
// 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 resolver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/pkg/artifact"
|
||||
)
|
||||
|
||||
var (
|
||||
registry = map[string]Resolver{}
|
||||
)
|
||||
|
||||
// Resolver resolves the detail information for a specific kind of artifact
|
||||
type Resolver interface {
|
||||
// ArtifactType returns the type of artifact that the resolver handles
|
||||
ArtifactType() string
|
||||
// Resolve receives the manifest content, resolves the detail information
|
||||
// from the manifest or the layers referenced by the manifest, and populates
|
||||
// the detail information into the artifact
|
||||
Resolve(ctx context.Context, manifest []byte, artifact *artifact.Artifact) error
|
||||
}
|
||||
|
||||
// Register resolver, one resolver can handle multiple media types for one kind of artifact
|
||||
func Register(resolver Resolver, mediaTypes ...string) error {
|
||||
for _, mediaType := range mediaTypes {
|
||||
_, exist := registry[mediaType]
|
||||
if exist {
|
||||
return fmt.Errorf("resolver to handle media type %s already exists", mediaType)
|
||||
}
|
||||
registry[mediaType] = resolver
|
||||
log.Infof("resolver to handle media type %s registered", mediaType)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get the resolver according to the media type
|
||||
func Get(mediaType string) (Resolver, error) {
|
||||
resolver, exist := registry[mediaType]
|
||||
if !exist {
|
||||
return nil, fmt.Errorf("resolver resolves %s not found", mediaType)
|
||||
}
|
||||
return resolver, nil
|
||||
}
|
54
src/api/artifact/abstractor/resolver/resolver_test.go
Normal file
54
src/api/artifact/abstractor/resolver/resolver_test.go
Normal file
@ -0,0 +1,54 @@
|
||||
// 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 resolver
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/suite"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type resolverTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func (r *resolverTestSuite) SetupTest() {
|
||||
registry = map[string]Resolver{}
|
||||
}
|
||||
|
||||
func (r *resolverTestSuite) TestRegister() {
|
||||
// registry a resolver
|
||||
mediaType := "fake_media_type"
|
||||
err := Register(nil, mediaType)
|
||||
r.Assert().Nil(err)
|
||||
|
||||
// try to register a resolver for the existing media type
|
||||
err = Register(nil, mediaType)
|
||||
r.Assert().NotNil(err)
|
||||
}
|
||||
|
||||
func (r *resolverTestSuite) TestGet() {
|
||||
// registry a resolver
|
||||
mediaType := "fake_media_type"
|
||||
err := Register(nil, mediaType)
|
||||
r.Assert().Nil(err)
|
||||
|
||||
// get the resolver
|
||||
_, err = Get(mediaType)
|
||||
r.Assert().Nil(err)
|
||||
|
||||
// get the not exist resolver
|
||||
_, err = Get("not_existing_media_type")
|
||||
r.Assert().NotNil(err)
|
||||
}
|
||||
|
||||
func TestResolverTestSuite(t *testing.T) {
|
||||
suite.Run(t, &resolverTestSuite{})
|
||||
}
|
@ -17,6 +17,9 @@ package artifact
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/goharbor/harbor/src/api/artifact/abstractor"
|
||||
// registry image resolvers
|
||||
_ "github.com/goharbor/harbor/src/api/artifact/abstractor/resolver/image"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
ierror "github.com/goharbor/harbor/src/internal/error"
|
||||
"github.com/goharbor/harbor/src/pkg/artifact"
|
||||
@ -29,7 +32,7 @@ import (
|
||||
|
||||
var (
|
||||
// Ctl is a global artifact controller instance
|
||||
Ctl = NewController(repository.Mgr, artifact.Mgr, tag.Mgr)
|
||||
Ctl = NewController()
|
||||
)
|
||||
|
||||
// Controller defines the operations related with artifacts and tags
|
||||
@ -63,11 +66,12 @@ type Controller interface {
|
||||
}
|
||||
|
||||
// NewController creates an instance of the default artifact controller
|
||||
func NewController(repoMgr repository.Manager, artMgr artifact.Manager, tagMgr tag.Manager) Controller {
|
||||
func NewController() Controller {
|
||||
return &controller{
|
||||
repoMgr: repoMgr,
|
||||
artMgr: artMgr,
|
||||
tagMgr: tagMgr,
|
||||
repoMgr: repository.Mgr,
|
||||
artMgr: artifact.Mgr,
|
||||
tagMgr: tag.Mgr,
|
||||
abstractor: abstractor.NewAbstractor(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -76,9 +80,10 @@ func NewController(repoMgr repository.Manager, artMgr artifact.Manager, tagMgr t
|
||||
// for artifacts and tags??
|
||||
|
||||
type controller struct {
|
||||
repoMgr repository.Manager
|
||||
artMgr artifact.Manager
|
||||
tagMgr tag.Manager
|
||||
repoMgr repository.Manager
|
||||
artMgr artifact.Manager
|
||||
tagMgr tag.Manager
|
||||
abstractor abstractor.Abstractor
|
||||
}
|
||||
|
||||
func (c *controller) Ensure(ctx context.Context, repositoryID int64, digest string, tags ...string) (bool, int64, error) {
|
||||
@ -123,7 +128,10 @@ func (c *controller) ensureArtifact(ctx context.Context, repositoryID int64, dig
|
||||
PushTime: time.Now(),
|
||||
}
|
||||
// abstract the specific information for the artifact
|
||||
c.abstract(ctx, artifact)
|
||||
if err = c.abstractor.Abstract(ctx, artifact); err != nil {
|
||||
return false, 0, err
|
||||
}
|
||||
|
||||
// create it
|
||||
id, err := c.artMgr.Create(ctx, artifact)
|
||||
if err != nil {
|
||||
@ -301,8 +309,3 @@ func (c *controller) assembleTag(ctx context.Context, tag *tm.Tag, option *TagOp
|
||||
// TODO populate label, signature, immutable status for tag
|
||||
return t
|
||||
}
|
||||
|
||||
func (c *controller) abstract(ctx context.Context, artifact *artifact.Artifact) {
|
||||
// TODO abstract the specific info for the artifact
|
||||
// handler references
|
||||
}
|
||||
|
@ -15,32 +15,46 @@
|
||||
package artifact
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/pkg/artifact"
|
||||
"github.com/goharbor/harbor/src/pkg/q"
|
||||
"github.com/goharbor/harbor/src/pkg/tag/model/tag"
|
||||
htesting "github.com/goharbor/harbor/src/testing"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type fakeAbstractor struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (f *fakeAbstractor) Abstract(ctx context.Context, artifact *artifact.Artifact) error {
|
||||
args := f.Called()
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
type controllerTestSuite struct {
|
||||
suite.Suite
|
||||
ctl *controller
|
||||
repoMgr *htesting.FakeRepositoryManager
|
||||
artMgr *htesting.FakeArtifactManager
|
||||
tagMgr *htesting.FakeTagManager
|
||||
ctl *controller
|
||||
repoMgr *htesting.FakeRepositoryManager
|
||||
artMgr *htesting.FakeArtifactManager
|
||||
tagMgr *htesting.FakeTagManager
|
||||
abstractor *fakeAbstractor
|
||||
}
|
||||
|
||||
func (c *controllerTestSuite) SetupTest() {
|
||||
c.repoMgr = &htesting.FakeRepositoryManager{}
|
||||
c.artMgr = &htesting.FakeArtifactManager{}
|
||||
c.tagMgr = &htesting.FakeTagManager{}
|
||||
c.abstractor = &fakeAbstractor{}
|
||||
c.ctl = &controller{
|
||||
repoMgr: c.repoMgr,
|
||||
artMgr: c.artMgr,
|
||||
tagMgr: c.tagMgr,
|
||||
repoMgr: c.repoMgr,
|
||||
artMgr: c.artMgr,
|
||||
tagMgr: c.tagMgr,
|
||||
abstractor: c.abstractor,
|
||||
}
|
||||
}
|
||||
|
||||
@ -94,10 +108,6 @@ func (c *controllerTestSuite) TestAssembleArtifact() {
|
||||
// TODO check other fields of option
|
||||
}
|
||||
|
||||
func (c *controllerTestSuite) TestAbstract() {
|
||||
// TODO add test case
|
||||
}
|
||||
|
||||
func (c *controllerTestSuite) TestEnsureArtifact() {
|
||||
digest := "sha256:418fb88ec412e340cdbef913b8ca1bbe8f9e8dc705f9617414c1f2c8db980180"
|
||||
|
||||
@ -117,16 +127,18 @@ func (c *controllerTestSuite) TestEnsureArtifact() {
|
||||
// reset the mock
|
||||
c.SetupTest()
|
||||
|
||||
// the artifact doesn't exist
|
||||
c.repoMgr.On("Get").Return(&models.RepoRecord{
|
||||
ProjectID: 1,
|
||||
}, nil)
|
||||
// the artifact doesn't exist
|
||||
c.artMgr.On("List").Return(1, []*artifact.Artifact{}, nil)
|
||||
c.artMgr.On("Create").Return(1, nil)
|
||||
c.abstractor.On("Abstract").Return(nil)
|
||||
created, id, err = c.ctl.ensureArtifact(nil, 1, digest)
|
||||
c.Require().Nil(err)
|
||||
c.repoMgr.AssertExpectations(c.T())
|
||||
c.artMgr.AssertExpectations(c.T())
|
||||
c.abstractor.AssertExpectations(c.T())
|
||||
c.True(created)
|
||||
c.Equal(int64(1), id)
|
||||
}
|
||||
@ -184,11 +196,13 @@ func (c *controllerTestSuite) TestEnsure() {
|
||||
c.artMgr.On("Create").Return(1, nil)
|
||||
c.tagMgr.On("List").Return(1, []*tag.Tag{}, nil)
|
||||
c.tagMgr.On("Create").Return(1, nil)
|
||||
c.abstractor.On("Abstract").Return(nil)
|
||||
_, id, err := c.ctl.Ensure(nil, 1, digest, "latest")
|
||||
c.Require().Nil(err)
|
||||
c.repoMgr.AssertExpectations(c.T())
|
||||
c.artMgr.AssertExpectations(c.T())
|
||||
c.tagMgr.AssertExpectations(c.T())
|
||||
c.abstractor.AssertExpectations(c.T())
|
||||
c.Equal(int64(1), id)
|
||||
}
|
||||
|
||||
|
36
src/testing/api/artifact/abstractor/blob/fetcher.go
Normal file
36
src/testing/api/artifact/abstractor/blob/fetcher.go
Normal file
@ -0,0 +1,36 @@
|
||||
// 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 blob
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// FakeFetcher is a fake blob fetcher that implement the src/api/artifact/blob.Fetcher interface
|
||||
type FakeFetcher struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// FetchManifest ...
|
||||
func (f *FakeFetcher) FetchManifest(repoFullName, digest string) (string, []byte, error) {
|
||||
args := f.Called(mock.Anything)
|
||||
return args.String(0), args.Get(1).([]byte), args.Error(2)
|
||||
}
|
||||
|
||||
// FetchLayer ...
|
||||
func (f *FakeFetcher) FetchLayer(repoFullName, digest string) (content []byte, err error) {
|
||||
args := f.Called(mock.Anything)
|
||||
return args.Get(0).([]byte), args.Error(1)
|
||||
}
|
Loading…
Reference in New Issue
Block a user