mirror of
https://github.com/goharbor/harbor.git
synced 2024-09-27 13:02:59 +02:00
Merge pull request #11033 from ywk253100/200311_artifact
Restructure the packages of artifact
This commit is contained in:
commit
d6b32e19df
145
src/api/artifact/abstractor.go
Normal file
145
src/api/artifact/abstractor.go
Normal file
@ -0,0 +1,145 @@
|
||||
// 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 artifact
|
||||
|
||||
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/processor"
|
||||
"github.com/goharbor/harbor/src/api/artifact/processor/blob"
|
||||
"github.com/goharbor/harbor/src/pkg/artifact"
|
||||
v1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
)
|
||||
|
||||
// Abstractor abstracts the metadata of artifact
|
||||
type Abstractor interface {
|
||||
// AbstractMetadata abstracts the metadata for the specific artifact type into the artifact model,
|
||||
AbstractMetadata(ctx context.Context, artifact *artifact.Artifact) error
|
||||
}
|
||||
|
||||
// NewAbstractor creates a new abstractor
|
||||
func NewAbstractor() Abstractor {
|
||||
return &abstractor{
|
||||
artMgr: artifact.Mgr,
|
||||
blobFetcher: blob.Fcher,
|
||||
}
|
||||
}
|
||||
|
||||
type abstractor struct {
|
||||
artMgr artifact.Manager
|
||||
blobFetcher blob.Fetcher
|
||||
}
|
||||
|
||||
func (a *abstractor) AbstractMetadata(ctx context.Context, artifact *artifact.Artifact) error {
|
||||
// read manifest content
|
||||
manifestMediaType, content, err := a.blobFetcher.FetchManifest(artifact.RepositoryName, artifact.Digest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
artifact.ManifestMediaType = manifestMediaType
|
||||
|
||||
switch artifact.ManifestMediaType {
|
||||
case "", "application/json", schema1.MediaTypeSignedManifest:
|
||||
a.abstractManifestV1Metadata(artifact)
|
||||
case v1.MediaTypeImageManifest, schema2.MediaTypeManifest:
|
||||
if err = a.abstractManifestV2Metadata(content, artifact); err != nil {
|
||||
return err
|
||||
}
|
||||
case v1.MediaTypeImageIndex, manifestlist.MediaTypeManifestList:
|
||||
if err = a.abstractIndexMetadata(ctx, content, artifact); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unsupported manifest media type: %s", artifact.ManifestMediaType)
|
||||
}
|
||||
return processor.Get(artifact.MediaType).AbstractMetadata(ctx, content, artifact)
|
||||
}
|
||||
|
||||
// the artifact is enveloped by docker manifest v1
|
||||
func (a *abstractor) abstractManifestV1Metadata(artifact *artifact.Artifact) {
|
||||
// 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
|
||||
}
|
||||
|
||||
// the artifact is enveloped by OCI manifest or docker manifest v2
|
||||
func (a *abstractor) abstractManifestV2Metadata(content []byte, artifact *artifact.Artifact) error {
|
||||
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
|
||||
return nil
|
||||
}
|
||||
|
||||
// the artifact is enveloped by OCI index or docker manifest list
|
||||
func (a *abstractor) abstractIndexMetadata(ctx context.Context, content []byte, art *artifact.Artifact) error {
|
||||
// the identity of index is still in progress, we use the manifest mediaType
|
||||
// as the media type of artifact
|
||||
art.MediaType = art.ManifestMediaType
|
||||
|
||||
index := &v1.Index{}
|
||||
if err := json.Unmarshal(content, index); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// set annotations
|
||||
art.Annotations = index.Annotations
|
||||
|
||||
art.Size += int64(len(content))
|
||||
// populate the referenced artifacts
|
||||
for _, mani := range index.Manifests {
|
||||
digest := mani.Digest.String()
|
||||
// make sure the child artifact exist
|
||||
ar, err := a.artMgr.GetByDigest(ctx, art.RepositoryName, digest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
art.Size += ar.Size
|
||||
art.References = append(art.References, &artifact.Reference{
|
||||
ChildID: ar.ID,
|
||||
ChildDigest: digest,
|
||||
Platform: mani.Platform,
|
||||
URLs: mani.URLs,
|
||||
Annotations: mani.Annotations,
|
||||
})
|
||||
}
|
||||
|
||||
// Currently, CNAB put its media type inside the annotations
|
||||
// try to parse the artifact media type from the annotations
|
||||
if art.Annotations != nil {
|
||||
mediaType := art.Annotations["org.opencontainers.artifactType"]
|
||||
if len(mediaType) > 0 {
|
||||
art.MediaType = mediaType
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -1,129 +0,0 @@
|
||||
// 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"
|
||||
ierror "github.com/goharbor/harbor/src/internal/error"
|
||||
"github.com/goharbor/harbor/src/pkg/artifact"
|
||||
"github.com/opencontainers/image-spec/specs-go/v1"
|
||||
)
|
||||
|
||||
// Abstractor abstracts the specific information for different types of artifacts
|
||||
type Abstractor interface {
|
||||
// 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, artifact *artifact.Artifact) 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
|
||||
AbstractAddition(ctx context.Context, artifact *artifact.Artifact, additionType string) (addition *resolver.Addition, err error)
|
||||
}
|
||||
|
||||
// NewAbstractor returns an instance of the default abstractor
|
||||
func NewAbstractor() Abstractor {
|
||||
return &abstractor{
|
||||
blobFetcher: blob.Fcher,
|
||||
}
|
||||
}
|
||||
|
||||
type abstractor struct {
|
||||
blobFetcher blob.Fetcher
|
||||
}
|
||||
|
||||
// TODO add white list for supported artifact type
|
||||
func (a *abstractor) AbstractMetadata(ctx context.Context, artifact *artifact.Artifact) error {
|
||||
// read manifest content
|
||||
manifestMediaType, content, err := a.blobFetcher.FetchManifest(artifact.RepositoryName, artifact.Digest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
artifact.ManifestMediaType = manifestMediaType
|
||||
|
||||
switch artifact.ManifestMediaType {
|
||||
// docker manifest v1
|
||||
case "", "application/json", schema1.MediaTypeSignedManifest:
|
||||
// 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, we use the manifest mediaType
|
||||
// as the media type of artifact
|
||||
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
|
||||
|
||||
// Currently, CNAB put its media type inside the annotations
|
||||
// try to parse the artifact media type from the annotations
|
||||
if artifact.Annotations != nil {
|
||||
mediaType := artifact.Annotations["org.opencontainers.artifactType"]
|
||||
if len(mediaType) > 0 {
|
||||
artifact.MediaType = mediaType
|
||||
}
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unsupported manifest media type: %s", artifact.ManifestMediaType)
|
||||
}
|
||||
|
||||
resolver := resolver.Get(artifact.MediaType)
|
||||
if resolver != nil {
|
||||
return resolver.ResolveMetadata(ctx, content, artifact)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *abstractor) AbstractAddition(ctx context.Context, artifact *artifact.Artifact, addition string) (*resolver.Addition, error) {
|
||||
resolver := resolver.Get(artifact.MediaType)
|
||||
if resolver == nil {
|
||||
return nil, ierror.New(nil).WithCode(ierror.BadRequestCode).
|
||||
WithMessage("the resolver for artifact %s not found, cannot get the addition", artifact.Type)
|
||||
}
|
||||
return resolver.ResolveAddition(ctx, artifact, addition)
|
||||
}
|
@ -1,298 +0,0 @@
|
||||
// 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 (
|
||||
"github.com/docker/distribution/manifest/schema1"
|
||||
"github.com/docker/distribution/manifest/schema2"
|
||||
"github.com/goharbor/harbor/src/api/artifact/abstractor/resolver"
|
||||
ierror "github.com/goharbor/harbor/src/internal/error"
|
||||
"github.com/goharbor/harbor/src/pkg/artifact"
|
||||
"github.com/goharbor/harbor/src/testing/api/artifact/abstractor/blob"
|
||||
tresolver "github.com/goharbor/harbor/src/testing/api/artifact/abstractor/resolver"
|
||||
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 abstractorTestSuite struct {
|
||||
suite.Suite
|
||||
abstractor Abstractor
|
||||
fetcher *blob.FakeFetcher
|
||||
resolver *tresolver.FakeResolver
|
||||
}
|
||||
|
||||
func (a *abstractorTestSuite) SetupTest() {
|
||||
a.fetcher = &blob.FakeFetcher{}
|
||||
a.resolver = &tresolver.FakeResolver{}
|
||||
a.abstractor = &abstractor{
|
||||
blobFetcher: a.fetcher,
|
||||
}
|
||||
}
|
||||
|
||||
// docker manifest v1
|
||||
func (a *abstractorTestSuite) TestAbstractMetadataOfV1Manifest() {
|
||||
resolver.Register(a.resolver, schema1.MediaTypeSignedManifest)
|
||||
a.fetcher.On("FetchManifest").Return(schema1.MediaTypeSignedManifest, []byte(v1Manifest), nil)
|
||||
a.resolver.On("ArtifactType").Return(fakeArtifactType)
|
||||
a.resolver.On("ResolveMetadata").Return(nil)
|
||||
artifact := &artifact.Artifact{
|
||||
ID: 1,
|
||||
}
|
||||
err := a.abstractor.AbstractMetadata(nil, artifact)
|
||||
a.Require().Nil(err)
|
||||
a.Assert().Equal(int64(1), artifact.ID)
|
||||
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) TestAbstractMetadataOfV2Manifest() {
|
||||
resolver.Register(a.resolver, schema2.MediaTypeImageConfig)
|
||||
a.fetcher.On("FetchManifest").Return(schema2.MediaTypeManifest, []byte(v2Manifest), nil)
|
||||
a.resolver.On("ArtifactType").Return(fakeArtifactType)
|
||||
a.resolver.On("ResolveMetadata").Return(nil)
|
||||
artifact := &artifact.Artifact{
|
||||
ID: 1,
|
||||
}
|
||||
|
||||
err := a.abstractor.AbstractMetadata(nil, artifact)
|
||||
a.Require().Nil(err)
|
||||
a.Assert().Equal(int64(1), artifact.ID)
|
||||
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) TestAbstractMetadataOfIndex() {
|
||||
resolver.Register(a.resolver, v1.MediaTypeImageIndex)
|
||||
a.fetcher.On("FetchManifest").Return(v1.MediaTypeImageIndex, []byte(index), nil)
|
||||
a.resolver.On("ArtifactType").Return(fakeArtifactType)
|
||||
a.resolver.On("ResolveMetadata").Return(nil)
|
||||
artifact := &artifact.Artifact{
|
||||
ID: 1,
|
||||
}
|
||||
err := a.abstractor.AbstractMetadata(nil, artifact)
|
||||
a.Require().Nil(err)
|
||||
a.Assert().Equal(int64(1), artifact.ID)
|
||||
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) TestAbstractMetadataOfUnsupported() {
|
||||
a.fetcher.On("FetchManifest").Return("unsupported-manifest", []byte{}, nil)
|
||||
artifact := &artifact.Artifact{
|
||||
ID: 1,
|
||||
}
|
||||
err := a.abstractor.AbstractMetadata(nil, artifact)
|
||||
a.Require().NotNil(err)
|
||||
}
|
||||
|
||||
func (a *abstractorTestSuite) TestAbstractAddition() {
|
||||
resolver.Register(a.resolver, v1.MediaTypeImageConfig)
|
||||
// cannot get the resolver
|
||||
art := &artifact.Artifact{
|
||||
MediaType: "unknown",
|
||||
}
|
||||
_, err := a.abstractor.AbstractAddition(nil, art, "addition")
|
||||
a.True(ierror.IsErr(err, ierror.BadRequestCode))
|
||||
|
||||
// get the resolver
|
||||
art = &artifact.Artifact{
|
||||
MediaType: v1.MediaTypeImageConfig,
|
||||
}
|
||||
a.resolver.On("ResolveAddition").Return(nil, nil)
|
||||
_, err = a.abstractor.AbstractAddition(nil, art, "addition")
|
||||
a.Require().Nil(err)
|
||||
}
|
||||
|
||||
func TestAbstractorTestSuite(t *testing.T) {
|
||||
suite.Run(t, &abstractorTestSuite{})
|
||||
}
|
@ -1,126 +0,0 @@
|
||||
// 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 cnab
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"github.com/goharbor/harbor/src/api/artifact/abstractor/blob"
|
||||
resolv "github.com/goharbor/harbor/src/api/artifact/abstractor/resolver"
|
||||
"github.com/goharbor/harbor/src/api/artifact/descriptor"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
ierror "github.com/goharbor/harbor/src/internal/error"
|
||||
"github.com/goharbor/harbor/src/pkg/artifact"
|
||||
v1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
)
|
||||
|
||||
// const definitions
|
||||
const (
|
||||
ArtifactTypeCNAB = "CNAB"
|
||||
mediaType = "application/vnd.cnab.manifest.v1"
|
||||
)
|
||||
|
||||
func init() {
|
||||
resolver := &resolver{
|
||||
argMgr: artifact.Mgr,
|
||||
blobFetcher: blob.Fcher,
|
||||
}
|
||||
if err := resolv.Register(resolver, mediaType); err != nil {
|
||||
log.Errorf("failed to register resolver for media type %s: %v", mediaType, err)
|
||||
return
|
||||
}
|
||||
if err := descriptor.Register(resolver, mediaType); err != nil {
|
||||
log.Errorf("failed to register descriptor for media type %s: %v", mediaType, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
type resolver struct {
|
||||
argMgr artifact.Manager
|
||||
blobFetcher blob.Fetcher
|
||||
}
|
||||
|
||||
func (r *resolver) ResolveMetadata(ctx context.Context, manifest []byte, art *artifact.Artifact) error {
|
||||
index := &v1.Index{}
|
||||
if err := json.Unmarshal(manifest, index); err != nil {
|
||||
return err
|
||||
}
|
||||
cfgManiDgt := ""
|
||||
// populate the referenced artifacts
|
||||
for _, mani := range index.Manifests {
|
||||
digest := mani.Digest.String()
|
||||
// make sure the child artifact exist
|
||||
ar, err := r.argMgr.GetByDigest(ctx, art.RepositoryName, digest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
art.References = append(art.References, &artifact.Reference{
|
||||
ChildID: ar.ID,
|
||||
ChildDigest: digest,
|
||||
Platform: mani.Platform,
|
||||
URLs: mani.URLs,
|
||||
Annotations: mani.Annotations,
|
||||
})
|
||||
// try to get the digest of the manifest that the config layer is referenced by
|
||||
if mani.Annotations != nil &&
|
||||
mani.Annotations["io.cnab.manifest.type"] == "config" {
|
||||
cfgManiDgt = mani.Digest.String()
|
||||
}
|
||||
}
|
||||
if len(cfgManiDgt) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// resolve the config of CNAB
|
||||
// get the manifest that the config layer is referenced by
|
||||
_, cfgMani, err := r.blobFetcher.FetchManifest(art.RepositoryName, cfgManiDgt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m := &v1.Manifest{}
|
||||
if err := json.Unmarshal(cfgMani, m); err != nil {
|
||||
return err
|
||||
}
|
||||
cfgDgt := m.Config.Digest.String()
|
||||
// get the config layer
|
||||
cfg, err := r.blobFetcher.FetchLayer(art.RepositoryName, cfgDgt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
metadata := map[string]interface{}{}
|
||||
if err := json.Unmarshal(cfg, &metadata); err != nil {
|
||||
return err
|
||||
}
|
||||
if art.ExtraAttrs == nil {
|
||||
art.ExtraAttrs = map[string]interface{}{}
|
||||
}
|
||||
for k, v := range metadata {
|
||||
art.ExtraAttrs[k] = v
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *resolver) ResolveAddition(ctx context.Context, artifact *artifact.Artifact, addition string) (*resolv.Addition, error) {
|
||||
return nil, ierror.New(nil).WithCode(ierror.BadRequestCode).
|
||||
WithMessage("addition %s isn't supported for %s", addition, ArtifactTypeCNAB)
|
||||
}
|
||||
|
||||
func (r *resolver) GetArtifactType() string {
|
||||
return ArtifactTypeCNAB
|
||||
}
|
||||
|
||||
func (r *resolver) ListAdditionTypes() []string {
|
||||
return nil
|
||||
}
|
@ -1,138 +0,0 @@
|
||||
// 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 cnab
|
||||
|
||||
import (
|
||||
ierror "github.com/goharbor/harbor/src/internal/error"
|
||||
"github.com/goharbor/harbor/src/pkg/artifact"
|
||||
"github.com/goharbor/harbor/src/testing/api/artifact/abstractor/blob"
|
||||
testingartifact "github.com/goharbor/harbor/src/testing/pkg/artifact"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type resolverTestSuite struct {
|
||||
suite.Suite
|
||||
resolver *resolver
|
||||
artMgr *testingartifact.FakeManager
|
||||
blobFetcher *blob.FakeFetcher
|
||||
}
|
||||
|
||||
func (r *resolverTestSuite) SetupTest() {
|
||||
r.artMgr = &testingartifact.FakeManager{}
|
||||
r.blobFetcher = &blob.FakeFetcher{}
|
||||
r.resolver = &resolver{
|
||||
argMgr: r.artMgr,
|
||||
blobFetcher: r.blobFetcher,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (r *resolverTestSuite) TestResolveMetadata() {
|
||||
index := `{
|
||||
"schemaVersion": 2,
|
||||
"manifests": [
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"digest": "sha256:b9616da7500f8c7c9a5e8d915714cd02d11bcc71ff5b4fd190bb77b1355c8549",
|
||||
"size": 193,
|
||||
"annotations": {
|
||||
"io.cnab.manifest.type": "config"
|
||||
}
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
|
||||
"digest": "sha256:a59a4e74d9cc89e4e75dfb2cc7ea5c108e4236ba6231b53081a9e2506d1197b6",
|
||||
"size": 942,
|
||||
"annotations": {
|
||||
"io.cnab.manifest.type": "invocation"
|
||||
}
|
||||
}
|
||||
],
|
||||
"annotations": {
|
||||
"io.cnab.keywords": "[\"helloworld\",\"cnab\",\"tutorial\"]",
|
||||
"io.cnab.runtime_version": "v1.0.0",
|
||||
"org.opencontainers.artifactType": "application/vnd.cnab.manifest.v1",
|
||||
"org.opencontainers.image.authors": "[{\"name\":\"Jane Doe\",\"email\":\"jane.doe@example.com\",\"url\":\"https://example.com\"}]",
|
||||
"org.opencontainers.image.description": "A short description of your bundle",
|
||||
"org.opencontainers.image.title": "helloworld",
|
||||
"org.opencontainers.image.version": "0.1.1"
|
||||
}
|
||||
}`
|
||||
|
||||
manifest := `{
|
||||
"schemaVersion": 2,
|
||||
"config": {
|
||||
"mediaType": "application/vnd.oci.image.config.v1+json",
|
||||
"digest": "sha256:e91b9dfcbbb3b88bac94726f276b89de46e4460b55f6e6d6f876e666b150ec5b",
|
||||
"size": 498
|
||||
},
|
||||
"layers": null
|
||||
}`
|
||||
config := `{
|
||||
"description": "A short description of your bundle",
|
||||
"invocationImages": [
|
||||
{
|
||||
"contentDigest": "sha256:a59a4e74d9cc89e4e75dfb2cc7ea5c108e4236ba6231b53081a9e2506d1197b6",
|
||||
"image": "cnab/helloworld:0.1.1",
|
||||
"imageType": "docker",
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
|
||||
"size": 942
|
||||
}
|
||||
],
|
||||
"keywords": [
|
||||
"helloworld",
|
||||
"cnab",
|
||||
"tutorial"
|
||||
],
|
||||
"maintainers": [
|
||||
{
|
||||
"email": "jane.doe@example.com",
|
||||
"name": "Jane Doe",
|
||||
"url": "https://example.com"
|
||||
}
|
||||
],
|
||||
"name": "helloworld",
|
||||
"schemaVersion": "v1.0.0",
|
||||
"version": "0.1.1"
|
||||
}`
|
||||
art := &artifact.Artifact{}
|
||||
r.artMgr.On("GetByDigest").Return(&artifact.Artifact{ID: 1}, nil)
|
||||
r.blobFetcher.On("FetchManifest").Return("", []byte(manifest), nil)
|
||||
r.blobFetcher.On("FetchLayer").Return([]byte(config), nil)
|
||||
err := r.resolver.ResolveMetadata(nil, []byte(index), art)
|
||||
r.Require().Nil(err)
|
||||
r.Len(art.References, 2)
|
||||
r.Equal("0.1.1", art.ExtraAttrs["version"].(string))
|
||||
r.Equal("helloworld", art.ExtraAttrs["name"].(string))
|
||||
}
|
||||
|
||||
func (r *resolverTestSuite) TestResolveAddition() {
|
||||
_, err := r.resolver.ResolveAddition(nil, nil, "")
|
||||
r.Require().NotNil(err)
|
||||
r.True(ierror.IsErr(err, ierror.BadRequestCode))
|
||||
}
|
||||
|
||||
func (r *resolverTestSuite) TestGetArtifactType() {
|
||||
r.Assert().Equal(ArtifactTypeCNAB, r.resolver.GetArtifactType())
|
||||
}
|
||||
|
||||
func (r *resolverTestSuite) TestListAdditionTypes() {
|
||||
r.Nil(r.resolver.ListAdditionTypes())
|
||||
}
|
||||
|
||||
func TestResolverTestSuite(t *testing.T) {
|
||||
suite.Run(t, &resolverTestSuite{})
|
||||
}
|
@ -1,87 +0,0 @@
|
||||
// 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 image
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"github.com/docker/distribution/manifest/manifestlist"
|
||||
"github.com/goharbor/harbor/src/api/artifact/abstractor/resolver"
|
||||
"github.com/goharbor/harbor/src/api/artifact/descriptor"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
ierror "github.com/goharbor/harbor/src/internal/error"
|
||||
"github.com/goharbor/harbor/src/pkg/artifact"
|
||||
v1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rslver := &indexResolver{
|
||||
artMgr: artifact.Mgr,
|
||||
}
|
||||
mediaTypes := []string{
|
||||
v1.MediaTypeImageIndex,
|
||||
manifestlist.MediaTypeManifestList,
|
||||
}
|
||||
if err := resolver.Register(rslver, mediaTypes...); err != nil {
|
||||
log.Errorf("failed to register resolver for media type %v: %v", mediaTypes, err)
|
||||
return
|
||||
}
|
||||
if err := descriptor.Register(rslver, mediaTypes...); err != nil {
|
||||
log.Errorf("failed to register descriptor for media type %v: %v", mediaTypes, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// indexResolver resolves artifact with OCI index and docker manifest list
|
||||
type indexResolver struct {
|
||||
artMgr artifact.Manager
|
||||
}
|
||||
|
||||
func (i *indexResolver) ResolveMetadata(ctx context.Context, manifest []byte, art *artifact.Artifact) error {
|
||||
index := &v1.Index{}
|
||||
if err := json.Unmarshal(manifest, index); err != nil {
|
||||
return err
|
||||
}
|
||||
// populate the referenced artifacts
|
||||
for _, mani := range index.Manifests {
|
||||
digest := mani.Digest.String()
|
||||
// make sure the child artifact exist
|
||||
ar, err := i.artMgr.GetByDigest(ctx, art.RepositoryName, digest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
art.References = append(art.References, &artifact.Reference{
|
||||
ChildID: ar.ID,
|
||||
ChildDigest: digest,
|
||||
Platform: mani.Platform,
|
||||
URLs: mani.URLs,
|
||||
Annotations: mani.Annotations,
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *indexResolver) ResolveAddition(ctx context.Context, artifact *artifact.Artifact, addition string) (*resolver.Addition, error) {
|
||||
return nil, ierror.New(nil).WithCode(ierror.BadRequestCode).
|
||||
WithMessage("addition %s isn't supported for %s(index)", addition, ArtifactTypeImage)
|
||||
}
|
||||
|
||||
func (i *indexResolver) GetArtifactType() string {
|
||||
return ArtifactTypeImage
|
||||
}
|
||||
|
||||
func (i *indexResolver) ListAdditionTypes() []string {
|
||||
return nil
|
||||
}
|
@ -1,150 +0,0 @@
|
||||
// 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 image
|
||||
|
||||
import (
|
||||
ierror "github.com/goharbor/harbor/src/internal/error"
|
||||
"github.com/goharbor/harbor/src/pkg/artifact"
|
||||
arttesting "github.com/goharbor/harbor/src/testing/pkg/artifact"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type indexResolverTestSuite struct {
|
||||
suite.Suite
|
||||
resolver *indexResolver
|
||||
artMgr *arttesting.FakeManager
|
||||
}
|
||||
|
||||
func (i *indexResolverTestSuite) SetupTest() {
|
||||
i.artMgr = &arttesting.FakeManager{}
|
||||
i.resolver = &indexResolver{
|
||||
artMgr: i.artMgr,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (i *indexResolverTestSuite) TestResolveMetadata() {
|
||||
manifest := `{
|
||||
"manifests": [
|
||||
{
|
||||
"digest": "sha256:92c7f9c92844bbbb5d0a101b22f7c2a7949e40f8ea90c8b3bc396879d95e899a",
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
|
||||
"platform": {
|
||||
"architecture": "amd64",
|
||||
"os": "linux"
|
||||
},
|
||||
"size": 524
|
||||
},
|
||||
{
|
||||
"digest": "sha256:e5785cb0c62cebbed4965129bae371f0589cadd6d84798fb58c2c5f9e237efd9",
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
|
||||
"platform": {
|
||||
"architecture": "arm",
|
||||
"os": "linux",
|
||||
"variant": "v5"
|
||||
},
|
||||
"size": 525
|
||||
},
|
||||
{
|
||||
"digest": "sha256:50b8560ad574c779908da71f7ce370c0a2471c098d44d1c8f6b513c5a55eeeb1",
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
|
||||
"platform": {
|
||||
"architecture": "arm",
|
||||
"os": "linux",
|
||||
"variant": "v7"
|
||||
},
|
||||
"size": 525
|
||||
},
|
||||
{
|
||||
"digest": "sha256:963612c5503f3f1674f315c67089dee577d8cc6afc18565e0b4183ae355fb343",
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
|
||||
"platform": {
|
||||
"architecture": "arm64",
|
||||
"os": "linux",
|
||||
"variant": "v8"
|
||||
},
|
||||
"size": 525
|
||||
},
|
||||
{
|
||||
"digest": "sha256:85dc5fbe16214366748ebe9d7cc73bc42d61d19d61fe05f01e317d278c2287ed",
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
|
||||
"platform": {
|
||||
"architecture": "386",
|
||||
"os": "linux"
|
||||
},
|
||||
"size": 527
|
||||
},
|
||||
{
|
||||
"digest": "sha256:8aaea2a718a29334caeaf225716284ce29dc17418edba98dbe6dafea5afcda16",
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
|
||||
"platform": {
|
||||
"architecture": "ppc64le",
|
||||
"os": "linux"
|
||||
},
|
||||
"size": 525
|
||||
},
|
||||
{
|
||||
"digest": "sha256:577ad4331d4fac91807308da99ecc107dcc6b2254bc4c7166325fd01113bea2a",
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
|
||||
"platform": {
|
||||
"architecture": "s390x",
|
||||
"os": "linux"
|
||||
},
|
||||
"size": 525
|
||||
},
|
||||
{
|
||||
"digest": "sha256:351e40a9ab7ca6818dfbf9c967d1dd15599438edc41189e3d4d87eeffba5b8bf",
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
|
||||
"platform": {
|
||||
"architecture": "amd64",
|
||||
"os": "windows",
|
||||
"os.version": "10.0.17763.914"
|
||||
},
|
||||
"size": 1124
|
||||
}
|
||||
],
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.list.v2+json",
|
||||
"schemaVersion": 2
|
||||
}`
|
||||
art := &artifact.Artifact{}
|
||||
i.artMgr.On("GetByDigest").Return(&artifact.Artifact{
|
||||
ID: 1,
|
||||
}, nil)
|
||||
err := i.resolver.ResolveMetadata(nil, []byte(manifest), art)
|
||||
i.Require().Nil(err)
|
||||
i.artMgr.AssertExpectations(i.T())
|
||||
i.Require().Len(art.References, 8)
|
||||
i.Assert().Equal(int64(1), art.References[0].ChildID)
|
||||
i.Assert().Equal("amd64", art.References[0].Platform.Architecture)
|
||||
}
|
||||
|
||||
func (i *indexResolverTestSuite) TestResolveAddition() {
|
||||
_, err := i.resolver.ResolveAddition(nil, nil, AdditionTypeBuildHistory)
|
||||
i.True(ierror.IsErr(err, ierror.BadRequestCode))
|
||||
}
|
||||
|
||||
func (i *indexResolverTestSuite) TestGetArtifactType() {
|
||||
i.Assert().Equal(ArtifactTypeImage, i.resolver.GetArtifactType())
|
||||
}
|
||||
|
||||
func (i *indexResolverTestSuite) TestListAdditionTypes() {
|
||||
additions := i.resolver.ListAdditionTypes()
|
||||
i.Len(additions, 0)
|
||||
}
|
||||
|
||||
func TestIndexResolverTestSuite(t *testing.T) {
|
||||
suite.Run(t, &indexResolverTestSuite{})
|
||||
}
|
@ -1,62 +0,0 @@
|
||||
// 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 {
|
||||
// ResolveMetadata receives the manifest content, resolves the metadata
|
||||
// from the manifest or the layers referenced by the manifest, and populates
|
||||
// the metadata into the artifact
|
||||
ResolveMetadata(ctx context.Context, manifest []byte, artifact *artifact.Artifact) error
|
||||
// ResolveAddition returns the addition of the artifact.
|
||||
// The additions are different for different artifacts:
|
||||
// build history for image; values.yaml, readme and dependencies for chart, etc
|
||||
ResolveAddition(ctx context.Context, artifact *artifact.Artifact, additionType string) (addition *Addition, err 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 {
|
||||
return registry[mediaType]
|
||||
}
|
||||
|
||||
// Addition defines the specific addition of different artifacts: build history for image, values.yaml for chart, etc
|
||||
type Addition struct {
|
||||
Content []byte // the content of the addition
|
||||
ContentType string // the content type of the addition, returned as "Content-Type" header in API
|
||||
}
|
@ -1,69 +0,0 @@
|
||||
// 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"
|
||||
"github.com/goharbor/harbor/src/pkg/artifact"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type fakeResolver struct{}
|
||||
|
||||
func (f *fakeResolver) ResolveMetadata(ctx context.Context, manifest []byte, artifact *artifact.Artifact) error {
|
||||
return nil
|
||||
}
|
||||
func (f *fakeResolver) ResolveAddition(ctx context.Context, artifact *artifact.Artifact, additionType string) (*Addition, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
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(&fakeResolver{}, mediaType)
|
||||
r.Assert().Nil(err)
|
||||
|
||||
// get the resolver
|
||||
resolver := Get(mediaType)
|
||||
r.Assert().NotNil(resolver)
|
||||
|
||||
// get the not exist resolver
|
||||
resolver = Get("not_existing_media_type")
|
||||
r.Assert().Nil(resolver)
|
||||
}
|
||||
|
||||
func TestResolverTestSuite(t *testing.T) {
|
||||
suite.Run(t, &resolverTestSuite{})
|
||||
}
|
203
src/api/artifact/abstractor_test.go
Normal file
203
src/api/artifact/abstractor_test.go
Normal file
@ -0,0 +1,203 @@
|
||||
// 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 artifact
|
||||
|
||||
import (
|
||||
"github.com/docker/distribution/manifest/schema1"
|
||||
"github.com/docker/distribution/manifest/schema2"
|
||||
"github.com/goharbor/harbor/src/api/artifact/processor"
|
||||
"github.com/goharbor/harbor/src/pkg/artifact"
|
||||
"github.com/goharbor/harbor/src/testing/api/artifact/processor/blob"
|
||||
tart "github.com/goharbor/harbor/src/testing/pkg/artifact"
|
||||
v1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var (
|
||||
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"
|
||||
}
|
||||
}`
|
||||
|
||||
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 abstractorTestSuite struct {
|
||||
suite.Suite
|
||||
argMgr *tart.FakeManager
|
||||
fetcher *blob.FakeFetcher
|
||||
abstractor *abstractor
|
||||
}
|
||||
|
||||
func (a *abstractorTestSuite) SetupTest() {
|
||||
a.fetcher = &blob.FakeFetcher{}
|
||||
a.argMgr = &tart.FakeManager{}
|
||||
a.abstractor = &abstractor{
|
||||
artMgr: a.argMgr,
|
||||
blobFetcher: a.fetcher,
|
||||
}
|
||||
// clear all registered processors
|
||||
processor.Registry = map[string]processor.Processor{}
|
||||
}
|
||||
|
||||
// docker manifest v1
|
||||
func (a *abstractorTestSuite) TestAbstractMetadataOfV1Manifest() {
|
||||
a.fetcher.On("FetchManifest").Return(schema1.MediaTypeSignedManifest, []byte(v1Manifest), nil)
|
||||
artifact := &artifact.Artifact{
|
||||
ID: 1,
|
||||
}
|
||||
err := a.abstractor.AbstractMetadata(nil, artifact)
|
||||
a.Require().Nil(err)
|
||||
a.Assert().Equal(int64(1), artifact.ID)
|
||||
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) TestAbstractMetadataOfV2Manifest() {
|
||||
a.fetcher.On("FetchManifest").Return(schema2.MediaTypeManifest, []byte(v2Manifest), nil)
|
||||
artifact := &artifact.Artifact{
|
||||
ID: 1,
|
||||
}
|
||||
err := a.abstractor.AbstractMetadata(nil, artifact)
|
||||
a.Require().Nil(err)
|
||||
a.Assert().Equal(int64(1), artifact.ID)
|
||||
a.Assert().Equal(schema2.MediaTypeManifest, artifact.ManifestMediaType)
|
||||
a.Assert().Equal(schema2.MediaTypeImageConfig, artifact.MediaType)
|
||||
a.Assert().Equal(int64(3043), artifact.Size)
|
||||
a.Require().Len(artifact.Annotations, 1)
|
||||
a.Equal("value1", artifact.Annotations["com.example.key1"])
|
||||
}
|
||||
|
||||
// OCI index
|
||||
func (a *abstractorTestSuite) TestAbstractMetadataOfIndex() {
|
||||
a.fetcher.On("FetchManifest").Return(v1.MediaTypeImageIndex, []byte(index), nil)
|
||||
a.argMgr.On("GetByDigest").Return(&artifact.Artifact{
|
||||
ID: 2,
|
||||
Size: 10,
|
||||
}, nil)
|
||||
artifact := &artifact.Artifact{
|
||||
ID: 1,
|
||||
}
|
||||
err := a.abstractor.AbstractMetadata(nil, artifact)
|
||||
a.Require().Nil(err)
|
||||
a.Assert().Equal(int64(1), artifact.ID)
|
||||
a.Assert().Equal(v1.MediaTypeImageIndex, artifact.ManifestMediaType)
|
||||
a.Assert().Equal(v1.MediaTypeImageIndex, artifact.MediaType)
|
||||
a.Assert().Equal(int64(668), artifact.Size)
|
||||
a.Require().Len(artifact.Annotations, 1)
|
||||
a.Assert().Equal("value1", artifact.Annotations["com.example.key1"])
|
||||
a.Len(artifact.References, 2)
|
||||
}
|
||||
|
||||
// OCI index
|
||||
func (a *abstractorTestSuite) TestAbstractMetadataOfUnsupported() {
|
||||
a.fetcher.On("FetchManifest").Return("unsupported-manifest", []byte{}, nil)
|
||||
artifact := &artifact.Artifact{
|
||||
ID: 1,
|
||||
}
|
||||
err := a.abstractor.AbstractMetadata(nil, artifact)
|
||||
a.Require().NotNil(err)
|
||||
}
|
||||
|
||||
func TestAbstractorTestSuite(t *testing.T) {
|
||||
suite.Run(t, &abstractorTestSuite{})
|
||||
}
|
@ -19,14 +19,8 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/goharbor/harbor/src/api/artifact/processor"
|
||||
"github.com/goharbor/harbor/src/api/event"
|
||||
evt "github.com/goharbor/harbor/src/pkg/notifier/event"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/goharbor/harbor/src/api/artifact/abstractor"
|
||||
"github.com/goharbor/harbor/src/api/artifact/abstractor/resolver"
|
||||
"github.com/goharbor/harbor/src/api/artifact/descriptor"
|
||||
"github.com/goharbor/harbor/src/api/tag"
|
||||
"github.com/goharbor/harbor/src/internal"
|
||||
"github.com/goharbor/harbor/src/internal/orm"
|
||||
@ -36,16 +30,19 @@ import (
|
||||
"github.com/goharbor/harbor/src/pkg/immutabletag/match"
|
||||
"github.com/goharbor/harbor/src/pkg/immutabletag/match/rule"
|
||||
"github.com/goharbor/harbor/src/pkg/label"
|
||||
evt "github.com/goharbor/harbor/src/pkg/notifier/event"
|
||||
"github.com/goharbor/harbor/src/pkg/registry"
|
||||
"github.com/goharbor/harbor/src/pkg/signature"
|
||||
"github.com/opencontainers/go-digest"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
// registry image resolvers
|
||||
_ "github.com/goharbor/harbor/src/api/artifact/abstractor/resolver/image"
|
||||
_ "github.com/goharbor/harbor/src/api/artifact/processor/image"
|
||||
// register chart resolver
|
||||
_ "github.com/goharbor/harbor/src/api/artifact/abstractor/resolver/chart"
|
||||
_ "github.com/goharbor/harbor/src/api/artifact/processor/chart"
|
||||
// register CNAB resolver
|
||||
_ "github.com/goharbor/harbor/src/api/artifact/abstractor/resolver/cnab"
|
||||
_ "github.com/goharbor/harbor/src/api/artifact/processor/cnab"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
ierror "github.com/goharbor/harbor/src/internal/error"
|
||||
"github.com/goharbor/harbor/src/pkg/artifact"
|
||||
@ -94,7 +91,7 @@ type Controller interface {
|
||||
// GetAddition returns the addition of the artifact.
|
||||
// The addition is different according to the artifact type:
|
||||
// build history for image; values.yaml, readme and dependencies for chart, etc
|
||||
GetAddition(ctx context.Context, artifactID int64, additionType string) (addition *resolver.Addition, err error)
|
||||
GetAddition(ctx context.Context, artifactID int64, additionType string) (addition *processor.Addition, err error)
|
||||
// AddLabel to the specified artifact
|
||||
AddLabel(ctx context.Context, artifactID int64, labelID int64) (err error)
|
||||
// RemoveLabel from the specified artifact
|
||||
@ -113,9 +110,9 @@ func NewController() Controller {
|
||||
blobMgr: blob.Mgr,
|
||||
sigMgr: signature.GetManager(),
|
||||
labelMgr: label.Mgr,
|
||||
abstractor: abstractor.NewAbstractor(),
|
||||
immutableMtr: rule.NewRuleMatcher(),
|
||||
regCli: registry.Cli,
|
||||
abstractor: NewAbstractor(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -129,9 +126,9 @@ type controller struct {
|
||||
blobMgr blob.Manager
|
||||
sigMgr signature.Manager
|
||||
labelMgr label.Manager
|
||||
abstractor abstractor.Abstractor
|
||||
immutableMtr match.ImmutableTagMatcher
|
||||
regCli registry.Client
|
||||
abstractor Abstractor
|
||||
}
|
||||
|
||||
func (c *controller) Ensure(ctx context.Context, repository, digest string, tags ...string) (bool, int64, error) {
|
||||
@ -187,7 +184,7 @@ func (c *controller) ensureArtifact(ctx context.Context, repository, digest stri
|
||||
}
|
||||
|
||||
// populate the artifact type
|
||||
artifact.Type = descriptor.GetArtifactType(artifact.MediaType)
|
||||
artifact.Type = processor.Get(artifact.MediaType).GetArtifactType()
|
||||
|
||||
// create it
|
||||
// use orm.WithTransaction here to avoid the issue:
|
||||
@ -473,12 +470,12 @@ func (c *controller) UpdatePullTime(ctx context.Context, artifactID int64, tagID
|
||||
return c.tagCtl.Update(ctx, tag, "PullTime")
|
||||
}
|
||||
|
||||
func (c *controller) GetAddition(ctx context.Context, artifactID int64, addition string) (*resolver.Addition, error) {
|
||||
func (c *controller) GetAddition(ctx context.Context, artifactID int64, addition string) (*processor.Addition, error) {
|
||||
artifact, err := c.artMgr.Get(ctx, artifactID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.abstractor.AbstractAddition(ctx, artifact, addition)
|
||||
return processor.Get(artifact.MediaType).AbstractAddition(ctx, artifact, addition)
|
||||
}
|
||||
|
||||
func (c *controller) AddLabel(ctx context.Context, artifactID int64, labelID int64) error {
|
||||
@ -578,7 +575,7 @@ func (c *controller) populateLabels(ctx context.Context, art *Artifact) {
|
||||
}
|
||||
|
||||
func (c *controller) populateAdditionLinks(ctx context.Context, artifact *Artifact) {
|
||||
types := descriptor.ListAdditionTypes(artifact.MediaType)
|
||||
types := processor.Get(artifact.MediaType).ListAdditionTypes()
|
||||
if len(types) > 0 {
|
||||
version := internal.GetAPIVersion(ctx)
|
||||
for _, t := range types {
|
||||
|
@ -19,8 +19,6 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/goharbor/harbor/src/api/artifact/abstractor/resolver"
|
||||
"github.com/goharbor/harbor/src/api/artifact/descriptor"
|
||||
"github.com/goharbor/harbor/src/api/tag"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/internal"
|
||||
@ -53,26 +51,6 @@ func (f *fakeAbstractor) AbstractMetadata(ctx context.Context, artifact *artifac
|
||||
args := f.Called()
|
||||
return args.Error(0)
|
||||
}
|
||||
func (f *fakeAbstractor) AbstractAddition(ctx context.Context, artifact *artifact.Artifact, additionType string) (*resolver.Addition, error) {
|
||||
args := f.Called()
|
||||
var addition *resolver.Addition
|
||||
if args.Get(0) != nil {
|
||||
addition = args.Get(0).(*resolver.Addition)
|
||||
}
|
||||
return addition, args.Error(1)
|
||||
}
|
||||
|
||||
type fakeDescriptor struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (f *fakeDescriptor) GetArtifactType() string {
|
||||
return "IMAGE"
|
||||
}
|
||||
|
||||
func (f *fakeDescriptor) ListAdditionTypes() []string {
|
||||
return []string{"BUILD_HISTORY"}
|
||||
}
|
||||
|
||||
type controllerTestSuite struct {
|
||||
suite.Suite
|
||||
@ -109,7 +87,6 @@ func (c *controllerTestSuite) SetupTest() {
|
||||
immutableMtr: c.immutableMtr,
|
||||
regCli: c.regCli,
|
||||
}
|
||||
descriptor.Register(&fakeDescriptor{}, "")
|
||||
}
|
||||
|
||||
func (c *controllerTestSuite) TestAssembleArtifact() {
|
||||
@ -148,11 +125,6 @@ func (c *controllerTestSuite) TestAssembleArtifact() {
|
||||
c.Require().NotNil(artifact)
|
||||
c.Equal(art.ID, artifact.ID)
|
||||
c.Contains(artifact.Tags, tg)
|
||||
c.Require().NotNil(artifact.AdditionLinks)
|
||||
c.Require().NotNil(artifact.AdditionLinks["build_history"])
|
||||
c.False(artifact.AdditionLinks["build_history"].Absolute)
|
||||
c.Equal("/api/2.0/projects/library/repositories/hello-world/artifacts/sha256:123/additions/build_history",
|
||||
artifact.AdditionLinks["build_history"].HREF)
|
||||
c.Contains(artifact.Labels, lb)
|
||||
// TODO check other fields of option
|
||||
}
|
||||
@ -496,10 +468,9 @@ func (c *controllerTestSuite) TestUpdatePullTime() {
|
||||
}
|
||||
|
||||
func (c *controllerTestSuite) TestGetAddition() {
|
||||
c.artMgr.On("Get").Return(nil, nil)
|
||||
c.abstractor.On("AbstractAddition").Return(nil, nil)
|
||||
c.artMgr.On("Get").Return(&artifact.Artifact{}, nil)
|
||||
_, err := c.ctl.GetAddition(nil, 1, "addition")
|
||||
c.Require().Nil(err)
|
||||
c.Require().NotNil(err)
|
||||
}
|
||||
|
||||
func (c *controllerTestSuite) TestAddTo() {
|
||||
|
@ -1,84 +0,0 @@
|
||||
// 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 descriptor
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ArtifactTypeUnknown defines the type for the unknown artifacts
|
||||
const ArtifactTypeUnknown = "UNKNOWN"
|
||||
|
||||
var (
|
||||
registry = map[string]Descriptor{}
|
||||
artifactTypeRegExp = regexp.MustCompile(`^application/vnd\.[^.]*\.(.*)\.config\.[^.]*\+json$`)
|
||||
)
|
||||
|
||||
// Descriptor describes the static information for one kind of media type
|
||||
type Descriptor interface {
|
||||
// GetArtifactType returns the type of one kind of artifact specified by media type
|
||||
GetArtifactType() string
|
||||
// ListAdditionTypes returns the supported addition types of one kind of artifact specified by media type
|
||||
ListAdditionTypes() []string
|
||||
}
|
||||
|
||||
// Register descriptor, one descriptor can handle multiple media types for one kind of artifact
|
||||
func Register(descriptor Descriptor, mediaTypes ...string) error {
|
||||
for _, mediaType := range mediaTypes {
|
||||
_, exist := registry[mediaType]
|
||||
if exist {
|
||||
return fmt.Errorf("descriptor to handle media type %s already exists", mediaType)
|
||||
}
|
||||
registry[mediaType] = descriptor
|
||||
log.Infof("descriptor to handle media type %s registered", mediaType)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get the descriptor according to the media type
|
||||
func Get(mediaType string) Descriptor {
|
||||
return registry[mediaType]
|
||||
}
|
||||
|
||||
// GetArtifactType gets the artifact type according to the media type
|
||||
func GetArtifactType(mediaType string) string {
|
||||
descriptor := Get(mediaType)
|
||||
if descriptor != nil {
|
||||
return descriptor.GetArtifactType()
|
||||
}
|
||||
// if got no descriptor, try to parse the artifact type based on the media type
|
||||
return parseArtifactType(mediaType)
|
||||
}
|
||||
|
||||
// ListAdditionTypes lists the supported addition types according to the media type
|
||||
func ListAdditionTypes(mediaType string) []string {
|
||||
descriptor := Get(mediaType)
|
||||
if descriptor != nil {
|
||||
return descriptor.ListAdditionTypes()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseArtifactType(mediaType string) string {
|
||||
strs := artifactTypeRegExp.FindStringSubmatch(mediaType)
|
||||
if len(strs) == 2 {
|
||||
return strings.ToUpper(strs[1])
|
||||
}
|
||||
// can not get the artifact type from the media type, return unknown
|
||||
return ArtifactTypeUnknown
|
||||
}
|
48
src/api/artifact/processor/base/index.go
Normal file
48
src/api/artifact/processor/base/index.go
Normal file
@ -0,0 +1,48 @@
|
||||
// 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 base
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/goharbor/harbor/src/api/artifact/processor"
|
||||
ierror "github.com/goharbor/harbor/src/internal/error"
|
||||
"github.com/goharbor/harbor/src/pkg/artifact"
|
||||
)
|
||||
|
||||
// IndexProcessor is a base processor to process artifact enveloped by OCI index or docker manifest list
|
||||
// Currently, it is just a null implementation
|
||||
type IndexProcessor struct {
|
||||
}
|
||||
|
||||
// AbstractMetadata abstracts metadata of artifact
|
||||
func (m *IndexProcessor) AbstractMetadata(ctx context.Context, content []byte, artifact *artifact.Artifact) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// AbstractAddition abstracts the addition of artifact
|
||||
func (m *IndexProcessor) AbstractAddition(ctx context.Context, artifact *artifact.Artifact, addition string) (*processor.Addition, error) {
|
||||
return nil, ierror.New(nil).WithCode(ierror.BadRequestCode).
|
||||
WithMessage("addition %s isn't supported", addition)
|
||||
}
|
||||
|
||||
// GetArtifactType returns the artifact type
|
||||
func (m *IndexProcessor) GetArtifactType() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// ListAdditionTypes returns the supported addition types
|
||||
func (m *IndexProcessor) ListAdditionTypes() []string {
|
||||
return nil
|
||||
}
|
88
src/api/artifact/processor/base/manifest.go
Normal file
88
src/api/artifact/processor/base/manifest.go
Normal file
@ -0,0 +1,88 @@
|
||||
// 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 base
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"github.com/goharbor/harbor/src/api/artifact/processor"
|
||||
"github.com/goharbor/harbor/src/api/artifact/processor/blob"
|
||||
ierror "github.com/goharbor/harbor/src/internal/error"
|
||||
"github.com/goharbor/harbor/src/pkg/artifact"
|
||||
"github.com/opencontainers/image-spec/specs-go/v1"
|
||||
)
|
||||
|
||||
// NewManifestProcessor creates a new base manifest processor.
|
||||
// All metadata read from config layer will be populated if specifying no "properties"
|
||||
func NewManifestProcessor(properties ...string) *ManifestProcessor {
|
||||
return &ManifestProcessor{
|
||||
properties: properties,
|
||||
BlobFetcher: blob.Fcher,
|
||||
}
|
||||
}
|
||||
|
||||
// ManifestProcessor is a base processor to process artifact enveloped by OCI manifest or docker v2 manifest
|
||||
type ManifestProcessor struct {
|
||||
properties []string
|
||||
BlobFetcher blob.Fetcher
|
||||
}
|
||||
|
||||
// AbstractMetadata abstracts metadata of artifact
|
||||
func (m *ManifestProcessor) AbstractMetadata(ctx context.Context, content []byte, artifact *artifact.Artifact) error {
|
||||
// get manifest
|
||||
manifest := &v1.Manifest{}
|
||||
if err := json.Unmarshal(content, manifest); err != nil {
|
||||
return err
|
||||
}
|
||||
// get config layer
|
||||
layer, err := m.BlobFetcher.FetchLayer(artifact.RepositoryName, manifest.Config.Digest.String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// parse metadata from config layer
|
||||
metadata := map[string]interface{}{}
|
||||
if err := json.Unmarshal(layer, &metadata); err != nil {
|
||||
return err
|
||||
}
|
||||
// if no properties specified, populate all metadata into the ExtraAttrs
|
||||
if len(m.properties) == 0 {
|
||||
artifact.ExtraAttrs = metadata
|
||||
return nil
|
||||
}
|
||||
|
||||
if artifact.ExtraAttrs == nil {
|
||||
artifact.ExtraAttrs = map[string]interface{}{}
|
||||
}
|
||||
for _, property := range m.properties {
|
||||
artifact.ExtraAttrs[property] = metadata[property]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AbstractAddition abstracts the addition of artifact
|
||||
func (m *ManifestProcessor) AbstractAddition(ctx context.Context, artifact *artifact.Artifact, addition string) (*processor.Addition, error) {
|
||||
return nil, ierror.New(nil).WithCode(ierror.BadRequestCode).
|
||||
WithMessage("addition %s isn't supported", addition)
|
||||
}
|
||||
|
||||
// GetArtifactType returns the artifact type
|
||||
func (m *ManifestProcessor) GetArtifactType() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// ListAdditionTypes returns the supported addition types
|
||||
func (m *ManifestProcessor) ListAdditionTypes() []string {
|
||||
return nil
|
||||
}
|
154
src/api/artifact/processor/base/manifest_test.go
Normal file
154
src/api/artifact/processor/base/manifest_test.go
Normal file
@ -0,0 +1,154 @@
|
||||
// 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 base
|
||||
|
||||
import (
|
||||
"github.com/goharbor/harbor/src/pkg/artifact"
|
||||
"github.com/goharbor/harbor/src/testing/api/artifact/processor/blob"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const (
|
||||
manifest = `{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
}`
|
||||
)
|
||||
|
||||
type manifestTestSuite struct {
|
||||
suite.Suite
|
||||
processor *ManifestProcessor
|
||||
blobFetcher *blob.FakeFetcher
|
||||
}
|
||||
|
||||
func (m *manifestTestSuite) SetupTest() {
|
||||
m.blobFetcher = &blob.FakeFetcher{}
|
||||
m.processor = &ManifestProcessor{
|
||||
BlobFetcher: m.blobFetcher,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *manifestTestSuite) TestAbstractMetadata() {
|
||||
// abstract all properties
|
||||
art := &artifact.Artifact{}
|
||||
m.blobFetcher.On("FetchLayer").Return([]byte(config), nil)
|
||||
m.processor.AbstractMetadata(nil, []byte(manifest), art)
|
||||
m.Len(art.ExtraAttrs, 9)
|
||||
|
||||
// reset the mock
|
||||
m.SetupTest()
|
||||
|
||||
// abstract the specified properties
|
||||
m.processor.properties = []string{"os"}
|
||||
art = &artifact.Artifact{}
|
||||
m.blobFetcher.On("FetchLayer").Return([]byte(config), nil)
|
||||
m.processor.AbstractMetadata(nil, []byte(manifest), art)
|
||||
m.Require().Len(art.ExtraAttrs, 1)
|
||||
m.Equal("linux", art.ExtraAttrs["os"])
|
||||
}
|
||||
|
||||
func TestManifestSuite(t *testing.T) {
|
||||
suite.Run(t, &manifestTestSuite{})
|
||||
}
|
@ -17,9 +17,9 @@ package chart
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"github.com/goharbor/harbor/src/api/artifact/abstractor/blob"
|
||||
resolv "github.com/goharbor/harbor/src/api/artifact/abstractor/resolver"
|
||||
"github.com/goharbor/harbor/src/api/artifact/descriptor"
|
||||
ps "github.com/goharbor/harbor/src/api/artifact/processor"
|
||||
"github.com/goharbor/harbor/src/api/artifact/processor/base"
|
||||
"github.com/goharbor/harbor/src/api/artifact/processor/blob"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
ierror "github.com/goharbor/harbor/src/internal/error"
|
||||
"github.com/goharbor/harbor/src/pkg/artifact"
|
||||
@ -34,61 +34,36 @@ const (
|
||||
AdditionTypeValues = "VALUES.YAML"
|
||||
AdditionTypeReadme = "README.MD"
|
||||
AdditionTypeDependencies = "DEPENDENCIES"
|
||||
|
||||
// TODO import it from helm chart repository
|
||||
mediaType = "application/vnd.cncf.helm.config.v1+json"
|
||||
)
|
||||
|
||||
func init() {
|
||||
resolver := &resolver{
|
||||
pc := &processor{
|
||||
blobFetcher: blob.Fcher,
|
||||
chartOperator: chart.Optr,
|
||||
}
|
||||
if err := resolv.Register(resolver, mediaType); err != nil {
|
||||
log.Errorf("failed to register resolver for media type %s: %v", mediaType, err)
|
||||
return
|
||||
}
|
||||
if err := descriptor.Register(resolver, mediaType); err != nil {
|
||||
log.Errorf("failed to register descriptor for media type %s: %v", mediaType, err)
|
||||
pc.ManifestProcessor = base.NewManifestProcessor()
|
||||
if err := ps.Register(pc, mediaType); err != nil {
|
||||
log.Errorf("failed to register processor for media type %s: %v", mediaType, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
type resolver struct {
|
||||
type processor struct {
|
||||
*base.ManifestProcessor
|
||||
blobFetcher blob.Fetcher
|
||||
chartOperator chart.Operator
|
||||
}
|
||||
|
||||
func (r *resolver) ResolveMetadata(ctx context.Context, manifest []byte, artifact *artifact.Artifact) error {
|
||||
m := &v1.Manifest{}
|
||||
if err := json.Unmarshal(manifest, m); err != nil {
|
||||
return err
|
||||
}
|
||||
digest := m.Config.Digest.String()
|
||||
layer, err := r.blobFetcher.FetchLayer(artifact.RepositoryName, digest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
metadata := map[string]interface{}{}
|
||||
if err := json.Unmarshal(layer, &metadata); err != nil {
|
||||
return err
|
||||
}
|
||||
if artifact.ExtraAttrs == nil {
|
||||
artifact.ExtraAttrs = map[string]interface{}{}
|
||||
}
|
||||
for k, v := range metadata {
|
||||
artifact.ExtraAttrs[k] = v
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *resolver) ResolveAddition(ctx context.Context, artifact *artifact.Artifact, addition string) (*resolv.Addition, error) {
|
||||
func (p *processor) AbstractAddition(ctx context.Context, artifact *artifact.Artifact, addition string) (*ps.Addition, error) {
|
||||
if addition != AdditionTypeValues && addition != AdditionTypeReadme && addition != AdditionTypeDependencies {
|
||||
return nil, ierror.New(nil).WithCode(ierror.BadRequestCode).
|
||||
WithMessage("addition %s isn't supported for %s", addition, ArtifactTypeChart)
|
||||
}
|
||||
|
||||
_, content, err := r.blobFetcher.FetchManifest(artifact.RepositoryName, artifact.Digest)
|
||||
_, content, err := p.blobFetcher.FetchManifest(artifact.RepositoryName, artifact.Digest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -101,11 +76,11 @@ func (r *resolver) ResolveAddition(ctx context.Context, artifact *artifact.Artif
|
||||
// chart do have two layers, one is config, we should resolve the other one.
|
||||
layerDgst := layer.Digest.String()
|
||||
if layerDgst != manifest.Config.Digest.String() {
|
||||
content, err = r.blobFetcher.FetchLayer(artifact.RepositoryName, layerDgst)
|
||||
content, err = p.blobFetcher.FetchLayer(artifact.RepositoryName, layerDgst)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
chartDetails, err := r.chartOperator.GetDetails(content)
|
||||
chartDetails, err := p.chartOperator.GetDetails(content)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -128,7 +103,7 @@ func (r *resolver) ResolveAddition(ctx context.Context, artifact *artifact.Artif
|
||||
additionContentType = "application/json; charset=utf-8"
|
||||
}
|
||||
|
||||
return &resolv.Addition{
|
||||
return &ps.Addition{
|
||||
Content: additionContent,
|
||||
ContentType: additionContentType,
|
||||
}, nil
|
||||
@ -137,10 +112,10 @@ func (r *resolver) ResolveAddition(ctx context.Context, artifact *artifact.Artif
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *resolver) GetArtifactType() string {
|
||||
func (p *processor) GetArtifactType() string {
|
||||
return ArtifactTypeChart
|
||||
}
|
||||
|
||||
func (r *resolver) ListAdditionTypes() []string {
|
||||
func (p *processor) ListAdditionTypes() []string {
|
||||
return []string{AdditionTypeValues, AdditionTypeReadme, AdditionTypeDependencies}
|
||||
}
|
@ -18,87 +18,34 @@ import (
|
||||
ierror "github.com/goharbor/harbor/src/internal/error"
|
||||
"github.com/goharbor/harbor/src/pkg/artifact"
|
||||
chartserver "github.com/goharbor/harbor/src/pkg/chart"
|
||||
"github.com/goharbor/harbor/src/testing/api/artifact/abstractor/blob"
|
||||
"github.com/goharbor/harbor/src/testing/api/artifact/processor/blob"
|
||||
"github.com/goharbor/harbor/src/testing/pkg/chart"
|
||||
"github.com/stretchr/testify/suite"
|
||||
helm_chart "helm.sh/helm/v3/pkg/chart"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type resolverTestSuite struct {
|
||||
type processorTestSuite struct {
|
||||
suite.Suite
|
||||
resolver *resolver
|
||||
processor *processor
|
||||
blobFetcher *blob.FakeFetcher
|
||||
chartOptr *chart.FakeOpertaor
|
||||
}
|
||||
|
||||
func (r *resolverTestSuite) SetupTest() {
|
||||
r.blobFetcher = &blob.FakeFetcher{}
|
||||
r.chartOptr = &chart.FakeOpertaor{}
|
||||
r.resolver = &resolver{
|
||||
blobFetcher: r.blobFetcher,
|
||||
chartOperator: r.chartOptr,
|
||||
func (p *processorTestSuite) SetupTest() {
|
||||
p.blobFetcher = &blob.FakeFetcher{}
|
||||
p.chartOptr = &chart.FakeOpertaor{}
|
||||
p.processor = &processor{
|
||||
blobFetcher: p.blobFetcher,
|
||||
chartOperator: p.chartOptr,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (r *resolverTestSuite) TestResolveMetadata() {
|
||||
content := `{
|
||||
"schemaVersion": 2,
|
||||
"config": {
|
||||
"mediaType": "application/vnd.cncf.helm.config.v1+json",
|
||||
"digest": "sha256:c87983b066bd08616c6135832363ed42784d66386814694b237f5608213be325",
|
||||
"size": 542
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"mediaType": "application/vnd.cncf.helm.chart.content.layer.v1+tar",
|
||||
"digest": "sha256:0f8c0650d55f5e00d11d7462381c340454a3b9e517e15a0187011dc305690541",
|
||||
"size": 28776
|
||||
}
|
||||
]
|
||||
}`
|
||||
config := `{
|
||||
"name": "harbor",
|
||||
"home": "https://goharbor.io",
|
||||
"sources": [
|
||||
"https://github.com/goharbor/harbor",
|
||||
"https://github.com/goharbor/harbor-helm"
|
||||
],
|
||||
"version": "1.1.2",
|
||||
"description": "An open source trusted cloud native registry that stores, signs, and scans content",
|
||||
"keywords": [
|
||||
"docker",
|
||||
"registry",
|
||||
"harbor"
|
||||
],
|
||||
"maintainers": [
|
||||
{
|
||||
"name": "Jesse Hu",
|
||||
"email": "huh@vmware.com"
|
||||
},
|
||||
{
|
||||
"name": "paulczar",
|
||||
"email": "username.taken@gmail.com"
|
||||
}
|
||||
],
|
||||
"icon": "https://raw.githubusercontent.com/goharbor/harbor/master/docs/img/harbor_logo.png",
|
||||
"apiVersion": "v1",
|
||||
"appVersion": "1.8.2"
|
||||
}`
|
||||
artifact := &artifact.Artifact{}
|
||||
r.blobFetcher.On("FetchLayer").Return([]byte(config), nil)
|
||||
err := r.resolver.ResolveMetadata(nil, []byte(content), artifact)
|
||||
r.Require().Nil(err)
|
||||
r.blobFetcher.AssertExpectations(r.T())
|
||||
r.Assert().Equal("1.1.2", artifact.ExtraAttrs["version"].(string))
|
||||
r.Assert().Equal("1.8.2", artifact.ExtraAttrs["appVersion"].(string))
|
||||
}
|
||||
|
||||
func (r *resolverTestSuite) TestResolveAddition() {
|
||||
func (p *processorTestSuite) TestAbstractAddition() {
|
||||
// unknown addition
|
||||
_, err := r.resolver.ResolveAddition(nil, nil, "unknown_addition")
|
||||
r.True(ierror.IsErr(err, ierror.BadRequestCode))
|
||||
_, err := p.processor.AbstractAddition(nil, nil, "unknown_addition")
|
||||
p.True(ierror.IsErr(err, ierror.BadRequestCode))
|
||||
|
||||
chartManifest := `{"schemaVersion":2,"config":{"mediaType":"application/vnd.cncf.helm.config.v1+json","digest":"sha256:76a59ebef39013bf7b57e411629b569a5175590024f31eeaaa577a0f8da9e523","size":528},"layers":[{"mediaType":"application/tar+gzip","digest":"sha256:0bd64cfb958b68c71b46597e22185a41e784dc96e04090bc7d2a480b704c3b65","size":12607}]}`
|
||||
|
||||
@ -151,38 +98,38 @@ func (r *resolverTestSuite) TestResolveAddition() {
|
||||
}
|
||||
|
||||
artifact := &artifact.Artifact{}
|
||||
r.blobFetcher.On("FetchManifest").Return("", []byte(chartManifest), nil)
|
||||
r.blobFetcher.On("FetchLayer").Return([]byte(chartYaml), nil)
|
||||
r.chartOptr.On("GetDetails").Return(chartDetails, nil)
|
||||
p.blobFetcher.On("FetchManifest").Return("", []byte(chartManifest), nil)
|
||||
p.blobFetcher.On("FetchLayer").Return([]byte(chartYaml), nil)
|
||||
p.chartOptr.On("GetDetails").Return(chartDetails, nil)
|
||||
|
||||
// values.yaml
|
||||
addition, err := r.resolver.ResolveAddition(nil, artifact, AdditionTypeValues)
|
||||
r.Require().Nil(err)
|
||||
r.Equal("text/plain; charset=utf-8", addition.ContentType)
|
||||
r.Equal(`image:\n ## Bitnami MongoDB registry\n ##\n registry: docker.io\n ## Bitnami MongoDB image name\n ##\n repository: bitnami/mongodb\n ## Bitnami MongoDB image tag\n ## ref: https://hub.docker.com/r/bitnami/mongodb/tags/\n`, string(addition.Content))
|
||||
addition, err := p.processor.AbstractAddition(nil, artifact, AdditionTypeValues)
|
||||
p.Require().Nil(err)
|
||||
p.Equal("text/plain; charset=utf-8", addition.ContentType)
|
||||
p.Equal(`image:\n ## Bitnami MongoDB registry\n ##\n registry: docker.io\n ## Bitnami MongoDB image name\n ##\n repository: bitnami/mongodb\n ## Bitnami MongoDB image tag\n ## ref: https://hub.docker.com/r/bitnami/mongodb/tags/\n`, string(addition.Content))
|
||||
|
||||
// README.md
|
||||
addition, err = r.resolver.ResolveAddition(nil, artifact, AdditionTypeReadme)
|
||||
r.Require().Nil(err)
|
||||
r.Equal("text/markdown; charset=utf-8", addition.ContentType)
|
||||
r.Equal(`This chart bootstraps a [Redis](https://github.com/bitnami/bitnami-docker-redis) deployment on a [Kubernetes](http://kubernetes.io) cluster using the [Helm](https://helm.sh) package manager.`, string(addition.Content))
|
||||
addition, err = p.processor.AbstractAddition(nil, artifact, AdditionTypeReadme)
|
||||
p.Require().Nil(err)
|
||||
p.Equal("text/markdown; charset=utf-8", addition.ContentType)
|
||||
p.Equal(`This chart bootstraps a [Redis](https://github.com/bitnami/bitnami-docker-redis) deployment on a [Kubernetes](http://kubernetes.io) cluster using the [Helm](https://helm.sh) package manager.`, string(addition.Content))
|
||||
|
||||
// README.md
|
||||
addition, err = r.resolver.ResolveAddition(nil, artifact, AdditionTypeDependencies)
|
||||
r.Require().Nil(err)
|
||||
r.Equal("application/json; charset=utf-8", addition.ContentType)
|
||||
r.Equal(`[{"name":"harbor","version":"v1.10","repository":"github.com/goharbor"}]`, string(addition.Content))
|
||||
addition, err = p.processor.AbstractAddition(nil, artifact, AdditionTypeDependencies)
|
||||
p.Require().Nil(err)
|
||||
p.Equal("application/json; charset=utf-8", addition.ContentType)
|
||||
p.Equal(`[{"name":"harbor","version":"v1.10","repository":"github.com/goharbor"}]`, string(addition.Content))
|
||||
}
|
||||
|
||||
func (r *resolverTestSuite) TestGetArtifactType() {
|
||||
r.Assert().Equal(ArtifactTypeChart, r.resolver.GetArtifactType())
|
||||
func (p *processorTestSuite) TestGetArtifactType() {
|
||||
p.Assert().Equal(ArtifactTypeChart, p.processor.GetArtifactType())
|
||||
}
|
||||
|
||||
func (r *resolverTestSuite) TestListAdditionTypes() {
|
||||
additions := r.resolver.ListAdditionTypes()
|
||||
r.EqualValues([]string{AdditionTypeValues, AdditionTypeReadme, AdditionTypeDependencies}, additions)
|
||||
func (p *processorTestSuite) TestListAdditionTypes() {
|
||||
additions := p.processor.ListAdditionTypes()
|
||||
p.EqualValues([]string{AdditionTypeValues, AdditionTypeReadme, AdditionTypeDependencies}, additions)
|
||||
}
|
||||
|
||||
func TestResolverTestSuite(t *testing.T) {
|
||||
suite.Run(t, &resolverTestSuite{})
|
||||
func TestProcessorTestSuite(t *testing.T) {
|
||||
suite.Run(t, &processorTestSuite{})
|
||||
}
|
75
src/api/artifact/processor/cnab/cnab.go
Normal file
75
src/api/artifact/processor/cnab/cnab.go
Normal file
@ -0,0 +1,75 @@
|
||||
// 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 cnab
|
||||
|
||||
import (
|
||||
"context"
|
||||
ps "github.com/goharbor/harbor/src/api/artifact/processor"
|
||||
"github.com/goharbor/harbor/src/api/artifact/processor/base"
|
||||
"github.com/goharbor/harbor/src/api/artifact/processor/blob"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/pkg/artifact"
|
||||
)
|
||||
|
||||
// const definitions
|
||||
const (
|
||||
ArtifactTypeCNAB = "CNAB"
|
||||
mediaType = "application/vnd.cnab.manifest.v1"
|
||||
)
|
||||
|
||||
func init() {
|
||||
pc := &processor{
|
||||
blobFetcher: blob.Fcher,
|
||||
manifestProcessor: base.NewManifestProcessor(),
|
||||
}
|
||||
pc.IndexProcessor = &base.IndexProcessor{}
|
||||
if err := ps.Register(pc, mediaType); err != nil {
|
||||
log.Errorf("failed to register processor for media type %s: %v", mediaType, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
type processor struct {
|
||||
*base.IndexProcessor
|
||||
manifestProcessor *base.ManifestProcessor
|
||||
blobFetcher blob.Fetcher
|
||||
}
|
||||
|
||||
func (p *processor) AbstractMetadata(ctx context.Context, manifest []byte, art *artifact.Artifact) error {
|
||||
cfgManiDgt := ""
|
||||
// try to get the digest of the manifest that the config layer is referenced by
|
||||
for _, reference := range art.References {
|
||||
if reference.Annotations != nil &&
|
||||
reference.Annotations["io.cnab.manifest.type"] == "config" {
|
||||
cfgManiDgt = reference.ChildDigest
|
||||
}
|
||||
}
|
||||
if len(cfgManiDgt) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// get the manifest that the config layer is referenced by
|
||||
_, cfgMani, err := p.blobFetcher.FetchManifest(art.RepositoryName, cfgManiDgt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// abstract the metadata from config layer
|
||||
return p.manifestProcessor.AbstractMetadata(ctx, cfgMani, art)
|
||||
}
|
||||
|
||||
func (p *processor) GetArtifactType() string {
|
||||
return ArtifactTypeCNAB
|
||||
}
|
104
src/api/artifact/processor/cnab/cnab_test.go
Normal file
104
src/api/artifact/processor/cnab/cnab_test.go
Normal file
@ -0,0 +1,104 @@
|
||||
// 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 cnab
|
||||
|
||||
import (
|
||||
"github.com/goharbor/harbor/src/api/artifact/processor/base"
|
||||
"github.com/goharbor/harbor/src/pkg/artifact"
|
||||
"github.com/goharbor/harbor/src/testing/api/artifact/processor/blob"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type processorTestSuite struct {
|
||||
suite.Suite
|
||||
processor *processor
|
||||
blobFetcher *blob.FakeFetcher
|
||||
}
|
||||
|
||||
func (p *processorTestSuite) SetupTest() {
|
||||
p.blobFetcher = &blob.FakeFetcher{}
|
||||
p.processor = &processor{
|
||||
blobFetcher: p.blobFetcher,
|
||||
manifestProcessor: &base.ManifestProcessor{
|
||||
BlobFetcher: p.blobFetcher,
|
||||
},
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (p *processorTestSuite) TestAbstractMetadata() {
|
||||
manifest := `{
|
||||
"schemaVersion": 2,
|
||||
"config": {
|
||||
"mediaType": "application/vnd.oci.image.config.v1+json",
|
||||
"digest": "sha256:e91b9dfcbbb3b88bac94726f276b89de46e4460b55f6e6d6f876e666b150ec5b",
|
||||
"size": 498
|
||||
},
|
||||
"layers": null
|
||||
}`
|
||||
config := `{
|
||||
"description": "A short description of your bundle",
|
||||
"invocationImages": [
|
||||
{
|
||||
"contentDigest": "sha256:a59a4e74d9cc89e4e75dfb2cc7ea5c108e4236ba6231b53081a9e2506d1197b6",
|
||||
"image": "cnab/helloworld:0.1.1",
|
||||
"imageType": "docker",
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
|
||||
"size": 942
|
||||
}
|
||||
],
|
||||
"keywords": [
|
||||
"helloworld",
|
||||
"cnab",
|
||||
"tutorial"
|
||||
],
|
||||
"maintainers": [
|
||||
{
|
||||
"email": "jane.doe@example.com",
|
||||
"name": "Jane Doe",
|
||||
"url": "https://example.com"
|
||||
}
|
||||
],
|
||||
"name": "helloworld",
|
||||
"schemaVersion": "v1.0.0",
|
||||
"version": "0.1.1"
|
||||
}`
|
||||
art := &artifact.Artifact{
|
||||
References: []*artifact.Reference{
|
||||
{
|
||||
ChildDigest: "sha256:b9616da7500f8c7c9a5e8d915714cd02d11bcc71ff5b4fd190bb77b1355c8549",
|
||||
Annotations: map[string]string{
|
||||
"io.cnab.manifest.type": "config",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
p.blobFetcher.On("FetchManifest").Return("", []byte(manifest), nil)
|
||||
p.blobFetcher.On("FetchLayer").Return([]byte(config), nil)
|
||||
err := p.processor.AbstractMetadata(nil, nil, art)
|
||||
p.Require().Nil(err)
|
||||
p.Len(art.ExtraAttrs, 7)
|
||||
p.Equal("0.1.1", art.ExtraAttrs["version"].(string))
|
||||
p.Equal("helloworld", art.ExtraAttrs["name"].(string))
|
||||
}
|
||||
|
||||
func (p *processorTestSuite) TestGetArtifactType() {
|
||||
p.Assert().Equal(ArtifactTypeCNAB, p.processor.GetArtifactType())
|
||||
}
|
||||
|
||||
func TestProcessorTestSuite(t *testing.T) {
|
||||
suite.Run(t, &processorTestSuite{})
|
||||
}
|
59
src/api/artifact/processor/default.go
Normal file
59
src/api/artifact/processor/default.go
Normal file
@ -0,0 +1,59 @@
|
||||
// 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 processor
|
||||
|
||||
import (
|
||||
"context"
|
||||
ierror "github.com/goharbor/harbor/src/internal/error"
|
||||
"github.com/goharbor/harbor/src/pkg/artifact"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ArtifactTypeUnknown defines the type for the unknown artifacts
|
||||
const ArtifactTypeUnknown = "UNKNOWN"
|
||||
|
||||
var (
|
||||
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
|
||||
}
|
||||
|
||||
func (d *defaultProcessor) GetArtifactType() string {
|
||||
// try to parse the type from the media type
|
||||
strs := artifactTypeRegExp.FindStringSubmatch(d.mediaType)
|
||||
if len(strs) == 2 {
|
||||
return strings.ToUpper(strs[1])
|
||||
}
|
||||
// can not get the artifact type from the media type, return unknown
|
||||
return ArtifactTypeUnknown
|
||||
}
|
||||
func (d *defaultProcessor) ListAdditionTypes() []string {
|
||||
return nil
|
||||
}
|
||||
func (d *defaultProcessor) AbstractMetadata(ctx context.Context, manifest []byte, artifact *artifact.Artifact) error {
|
||||
// do nothing currently
|
||||
// we can extend this function to abstract the metadata in the future if needed
|
||||
return nil
|
||||
}
|
||||
func (d *defaultProcessor) AbstractAddition(ctx context.Context, artifact *artifact.Artifact, addition string) (*Addition, error) {
|
||||
// return error directly
|
||||
return nil, ierror.New(nil).WithCode(ierror.BadRequestCode).
|
||||
WithMessage("the processor for artifact %s not found, cannot get the addition", artifact.Type)
|
||||
}
|
@ -12,39 +12,44 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package descriptor
|
||||
package processor
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/suite"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type descriptorTestSuite struct {
|
||||
type defaultProcessorTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func (d *descriptorTestSuite) TestParseArtifactType() {
|
||||
func (d *defaultProcessorTestSuite) TestGetArtifactType() {
|
||||
mediaType := ""
|
||||
typee := parseArtifactType(mediaType)
|
||||
processor := &defaultProcessor{mediaType: mediaType}
|
||||
typee := processor.GetArtifactType()
|
||||
d.Equal(ArtifactTypeUnknown, typee)
|
||||
|
||||
mediaType = "unknown"
|
||||
typee = parseArtifactType(mediaType)
|
||||
processor = &defaultProcessor{mediaType: mediaType}
|
||||
typee = processor.GetArtifactType()
|
||||
d.Equal(ArtifactTypeUnknown, typee)
|
||||
|
||||
mediaType = "application/vnd.oci.image.config.v1+json"
|
||||
typee = parseArtifactType(mediaType)
|
||||
processor = &defaultProcessor{mediaType: mediaType}
|
||||
typee = processor.GetArtifactType()
|
||||
d.Equal("IMAGE", typee)
|
||||
|
||||
mediaType = "application/vnd.cncf.helm.chart.config.v1+json"
|
||||
typee = parseArtifactType(mediaType)
|
||||
processor = &defaultProcessor{mediaType: mediaType}
|
||||
typee = processor.GetArtifactType()
|
||||
d.Equal("HELM.CHART", typee)
|
||||
|
||||
mediaType = "application/vnd.sylabs.sif.config.v1+json"
|
||||
typee = parseArtifactType(mediaType)
|
||||
processor = &defaultProcessor{mediaType: mediaType}
|
||||
typee = processor.GetArtifactType()
|
||||
d.Equal("SIF", typee)
|
||||
}
|
||||
|
||||
func TestDescriptorTestSuite(t *testing.T) {
|
||||
suite.Run(t, &descriptorTestSuite{})
|
||||
func TestDefaultProcessorTestSuite(t *testing.T) {
|
||||
suite.Run(t, &defaultProcessorTestSuite{})
|
||||
}
|
45
src/api/artifact/processor/image/index.go
Normal file
45
src/api/artifact/processor/image/index.go
Normal file
@ -0,0 +1,45 @@
|
||||
// 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 image
|
||||
|
||||
import (
|
||||
"github.com/docker/distribution/manifest/manifestlist"
|
||||
"github.com/goharbor/harbor/src/api/artifact/processor"
|
||||
"github.com/goharbor/harbor/src/api/artifact/processor/base"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
v1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
)
|
||||
|
||||
func init() {
|
||||
mediaTypes := []string{
|
||||
v1.MediaTypeImageIndex,
|
||||
manifestlist.MediaTypeManifestList,
|
||||
}
|
||||
pc := &indexProcessor{}
|
||||
pc.IndexProcessor = &base.IndexProcessor{}
|
||||
if err := processor.Register(pc, mediaTypes...); err != nil {
|
||||
log.Errorf("failed to register processor for media type %v: %v", mediaTypes, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// indexProcessor processes image with OCI index and docker manifest list
|
||||
type indexProcessor struct {
|
||||
*base.IndexProcessor
|
||||
}
|
||||
|
||||
func (i *indexProcessor) GetArtifactType() string {
|
||||
return ArtifactTypeImage
|
||||
}
|
37
src/api/artifact/processor/image/index_test.go
Normal file
37
src/api/artifact/processor/image/index_test.go
Normal file
@ -0,0 +1,37 @@
|
||||
// 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 image
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/suite"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type indexProcessTestSuite struct {
|
||||
suite.Suite
|
||||
processor *indexProcessor
|
||||
}
|
||||
|
||||
func (i *indexProcessTestSuite) SetupTest() {
|
||||
i.processor = &indexProcessor{}
|
||||
}
|
||||
|
||||
func (i *indexProcessTestSuite) TestGetArtifactType() {
|
||||
i.Assert().Equal(ArtifactTypeImage, i.processor.GetArtifactType())
|
||||
}
|
||||
|
||||
func TestIndexProcessTestSuite(t *testing.T) {
|
||||
suite.Run(t, &indexProcessTestSuite{})
|
||||
}
|
@ -18,32 +18,27 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"github.com/docker/distribution/manifest/schema1"
|
||||
"github.com/goharbor/harbor/src/api/artifact/abstractor/resolver"
|
||||
"github.com/goharbor/harbor/src/api/artifact/descriptor"
|
||||
"github.com/goharbor/harbor/src/api/artifact/processor"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
ierror "github.com/goharbor/harbor/src/internal/error"
|
||||
"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 media type %s: %v", schema1.MediaTypeSignedManifest, err)
|
||||
return
|
||||
}
|
||||
if err := descriptor.Register(rslver, schema1.MediaTypeSignedManifest); err != nil {
|
||||
log.Errorf("failed to register descriptor for media type %s: %v", schema1.MediaTypeSignedManifest, err)
|
||||
pc := &manifestV1Processor{}
|
||||
if err := processor.Register(pc, schema1.MediaTypeSignedManifest); err != nil {
|
||||
log.Errorf("failed to register processor for media type %s: %v", schema1.MediaTypeSignedManifest, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// manifestV1Resolver resolve artifact with docker v1 manifest
|
||||
type manifestV1Resolver struct {
|
||||
// manifestV1Processor processes image with docker v1 manifest
|
||||
type manifestV1Processor struct {
|
||||
}
|
||||
|
||||
func (m *manifestV1Resolver) ResolveMetadata(ctx context.Context, manifest []byte, artifact *artifact.Artifact) error {
|
||||
func (m *manifestV1Processor) AbstractMetadata(ctx context.Context, manifest []byte, artifact *artifact.Artifact) error {
|
||||
mani := &schema1.Manifest{}
|
||||
if err := json.Unmarshal([]byte(manifest), mani); err != nil {
|
||||
if err := json.Unmarshal(manifest, mani); err != nil {
|
||||
return err
|
||||
}
|
||||
if artifact.ExtraAttrs == nil {
|
||||
@ -53,15 +48,15 @@ func (m *manifestV1Resolver) ResolveMetadata(ctx context.Context, manifest []byt
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *manifestV1Resolver) ResolveAddition(ctx context.Context, artifact *artifact.Artifact, addition string) (*resolver.Addition, error) {
|
||||
func (m *manifestV1Processor) AbstractAddition(ctx context.Context, artifact *artifact.Artifact, addition string) (*processor.Addition, error) {
|
||||
return nil, ierror.New(nil).WithCode(ierror.BadRequestCode).
|
||||
WithMessage("addition %s isn't supported for %s(manifest version 1)", addition, ArtifactTypeImage)
|
||||
}
|
||||
|
||||
func (m *manifestV1Resolver) GetArtifactType() string {
|
||||
func (m *manifestV1Processor) GetArtifactType() string {
|
||||
return ArtifactTypeImage
|
||||
}
|
||||
|
||||
func (m *manifestV1Resolver) ListAdditionTypes() []string {
|
||||
func (m *manifestV1Processor) ListAdditionTypes() []string {
|
||||
return nil
|
||||
}
|
@ -21,17 +21,17 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
type manifestV1ResolverTestSuite struct {
|
||||
type manifestV1ProcessorTestSuite struct {
|
||||
suite.Suite
|
||||
resolver *manifestV1Resolver
|
||||
processor *manifestV1Processor
|
||||
}
|
||||
|
||||
func (m *manifestV1ResolverTestSuite) SetupSuite() {
|
||||
m.resolver = &manifestV1Resolver{}
|
||||
func (m *manifestV1ProcessorTestSuite) SetupSuite() {
|
||||
m.processor = &manifestV1Processor{}
|
||||
|
||||
}
|
||||
|
||||
func (m *manifestV1ResolverTestSuite) TestResolveMetadata() {
|
||||
func (m *manifestV1ProcessorTestSuite) TestAbstractMetadata() {
|
||||
manifest := `{
|
||||
"name": "hello-world",
|
||||
"tag": "latest",
|
||||
@ -78,25 +78,25 @@ func (m *manifestV1ResolverTestSuite) TestResolveMetadata() {
|
||||
}
|
||||
`
|
||||
artifact := &artifact.Artifact{}
|
||||
err := m.resolver.ResolveMetadata(nil, []byte(manifest), artifact)
|
||||
err := m.processor.AbstractMetadata(nil, []byte(manifest), artifact)
|
||||
m.Require().Nil(err)
|
||||
m.Assert().Equal("amd64", artifact.ExtraAttrs["architecture"].(string))
|
||||
}
|
||||
|
||||
func (m *manifestV1ResolverTestSuite) TestResolveAddition() {
|
||||
_, err := m.resolver.ResolveAddition(nil, nil, AdditionTypeBuildHistory)
|
||||
func (m *manifestV1ProcessorTestSuite) TestAbstractAddition() {
|
||||
_, err := m.processor.AbstractAddition(nil, nil, AdditionTypeBuildHistory)
|
||||
m.True(ierror.IsErr(err, ierror.BadRequestCode))
|
||||
}
|
||||
|
||||
func (m *manifestV1ResolverTestSuite) TestGetArtifactType() {
|
||||
m.Assert().Equal(ArtifactTypeImage, m.resolver.GetArtifactType())
|
||||
func (m *manifestV1ProcessorTestSuite) TestGetArtifactType() {
|
||||
m.Assert().Equal(ArtifactTypeImage, m.processor.GetArtifactType())
|
||||
}
|
||||
|
||||
func (m *manifestV1ResolverTestSuite) TestListAdditionTypes() {
|
||||
additions := m.resolver.ListAdditionTypes()
|
||||
func (m *manifestV1ProcessorTestSuite) TestListAdditionTypes() {
|
||||
additions := m.processor.ListAdditionTypes()
|
||||
m.Len(additions, 0)
|
||||
}
|
||||
|
||||
func TestManifestV1ResolverTestSuite(t *testing.T) {
|
||||
suite.Run(t, &manifestV1ResolverTestSuite{})
|
||||
func TestManifestV1ProcessorTestSuite(t *testing.T) {
|
||||
suite.Run(t, &manifestV1ProcessorTestSuite{})
|
||||
}
|
@ -18,9 +18,9 @@ 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/api/artifact/descriptor"
|
||||
"github.com/goharbor/harbor/src/api/artifact/processor"
|
||||
"github.com/goharbor/harbor/src/api/artifact/processor/base"
|
||||
"github.com/goharbor/harbor/src/api/artifact/processor/blob"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
ierror "github.com/goharbor/harbor/src/internal/error"
|
||||
"github.com/goharbor/harbor/src/pkg/artifact"
|
||||
@ -35,53 +35,27 @@ const (
|
||||
)
|
||||
|
||||
func init() {
|
||||
rslver := &manifestV2Resolver{
|
||||
pc := &manifestV2Processor{
|
||||
blobFetcher: blob.Fcher,
|
||||
}
|
||||
pc.ManifestProcessor = base.NewManifestProcessor("created", "author", "architecture", "os")
|
||||
mediaTypes := []string{
|
||||
v1.MediaTypeImageConfig,
|
||||
schema2.MediaTypeImageConfig,
|
||||
}
|
||||
if err := resolver.Register(rslver, mediaTypes...); err != nil {
|
||||
log.Errorf("failed to register resolver for media type %v: %v", mediaTypes, err)
|
||||
return
|
||||
}
|
||||
if err := descriptor.Register(rslver, mediaTypes...); err != nil {
|
||||
log.Errorf("failed to register descriptor for media type %v: %v", mediaTypes, err)
|
||||
if err := processor.Register(pc, mediaTypes...); err != nil {
|
||||
log.Errorf("failed to register processor for media type %v: %v", mediaTypes, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// manifestV2Resolver resolve artifact with OCI manifest and docker v2 manifest
|
||||
type manifestV2Resolver struct {
|
||||
// manifestV2Processor processes image with OCI manifest and docker v2 manifest
|
||||
type manifestV2Processor struct {
|
||||
*base.ManifestProcessor
|
||||
blobFetcher blob.Fetcher
|
||||
}
|
||||
|
||||
func (m *manifestV2Resolver) ResolveMetadata(ctx context.Context, content []byte, artifact *artifact.Artifact) error {
|
||||
manifest := &v1.Manifest{}
|
||||
if err := json.Unmarshal(content, manifest); err != nil {
|
||||
return err
|
||||
}
|
||||
digest := manifest.Config.Digest.String()
|
||||
layer, err := m.blobFetcher.FetchLayer(artifact.RepositoryName, digest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
image := &v1.Image{}
|
||||
if err := json.Unmarshal(layer, image); err != nil {
|
||||
return err
|
||||
}
|
||||
if artifact.ExtraAttrs == nil {
|
||||
artifact.ExtraAttrs = map[string]interface{}{}
|
||||
}
|
||||
artifact.ExtraAttrs["created"] = image.Created
|
||||
artifact.ExtraAttrs["author"] = image.Author
|
||||
artifact.ExtraAttrs["architecture"] = image.Architecture
|
||||
artifact.ExtraAttrs["os"] = image.OS
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *manifestV2Resolver) ResolveAddition(ctx context.Context, artifact *artifact.Artifact, addition string) (*resolver.Addition, error) {
|
||||
func (m *manifestV2Processor) AbstractAddition(ctx context.Context, artifact *artifact.Artifact, addition string) (*processor.Addition, error) {
|
||||
if addition != AdditionTypeBuildHistory {
|
||||
return nil, ierror.New(nil).WithCode(ierror.BadRequestCode).
|
||||
WithMessage("addition %s isn't supported for %s(manifest version 2)", addition, ArtifactTypeImage)
|
||||
@ -106,16 +80,16 @@ func (m *manifestV2Resolver) ResolveAddition(ctx context.Context, artifact *arti
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &resolver.Addition{
|
||||
return &processor.Addition{
|
||||
Content: content,
|
||||
ContentType: "application/json; charset=utf-8",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *manifestV2Resolver) GetArtifactType() string {
|
||||
func (m *manifestV2Processor) GetArtifactType() string {
|
||||
return ArtifactTypeImage
|
||||
}
|
||||
|
||||
func (m *manifestV2Resolver) ListAdditionTypes() []string {
|
||||
func (m *manifestV2Processor) ListAdditionTypes() []string {
|
||||
return []string{AdditionTypeBuildHistory}
|
||||
}
|
@ -17,7 +17,7 @@ package image
|
||||
import (
|
||||
ierror "github.com/goharbor/harbor/src/internal/error"
|
||||
"github.com/goharbor/harbor/src/pkg/artifact"
|
||||
"github.com/goharbor/harbor/src/testing/api/artifact/abstractor/blob"
|
||||
"github.com/goharbor/harbor/src/testing/api/artifact/processor/blob"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"testing"
|
||||
)
|
||||
@ -118,54 +118,43 @@ var (
|
||||
}`
|
||||
)
|
||||
|
||||
type manifestV2ResolverTestSuite struct {
|
||||
type manifestV2ProcessorTestSuite struct {
|
||||
suite.Suite
|
||||
resolver *manifestV2Resolver
|
||||
processor *manifestV2Processor
|
||||
blobFetcher *blob.FakeFetcher
|
||||
}
|
||||
|
||||
func (m *manifestV2ResolverTestSuite) SetupTest() {
|
||||
func (m *manifestV2ProcessorTestSuite) SetupTest() {
|
||||
m.blobFetcher = &blob.FakeFetcher{}
|
||||
m.resolver = &manifestV2Resolver{
|
||||
m.processor = &manifestV2Processor{
|
||||
blobFetcher: m.blobFetcher,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (m *manifestV2ResolverTestSuite) TestResolveMetadata() {
|
||||
artifact := &artifact.Artifact{}
|
||||
m.blobFetcher.On("FetchLayer").Return([]byte(config), nil)
|
||||
err := m.resolver.ResolveMetadata(nil, []byte(manifest), artifact)
|
||||
m.Require().Nil(err)
|
||||
m.blobFetcher.AssertExpectations(m.T())
|
||||
m.Assert().Equal("amd64", artifact.ExtraAttrs["architecture"].(string))
|
||||
m.Assert().Equal("linux", artifact.ExtraAttrs["os"].(string))
|
||||
}
|
||||
|
||||
func (m *manifestV2ResolverTestSuite) TestResolveAddition() {
|
||||
func (m *manifestV2ProcessorTestSuite) TestAbstractAddition() {
|
||||
// unknown addition
|
||||
_, err := m.resolver.ResolveAddition(nil, nil, "unknown_addition")
|
||||
_, err := m.processor.AbstractAddition(nil, nil, "unknown_addition")
|
||||
m.True(ierror.IsErr(err, ierror.BadRequestCode))
|
||||
|
||||
// build history
|
||||
artifact := &artifact.Artifact{}
|
||||
m.blobFetcher.On("FetchManifest").Return("", []byte(manifest), nil)
|
||||
m.blobFetcher.On("FetchLayer").Return([]byte(config), nil)
|
||||
addition, err := m.resolver.ResolveAddition(nil, artifact, AdditionTypeBuildHistory)
|
||||
addition, err := m.processor.AbstractAddition(nil, artifact, AdditionTypeBuildHistory)
|
||||
m.Require().Nil(err)
|
||||
m.Equal("application/json; charset=utf-8", addition.ContentType)
|
||||
m.Equal(`[{"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}]`, string(addition.Content))
|
||||
}
|
||||
|
||||
func (m *manifestV2ResolverTestSuite) TestGetArtifactType() {
|
||||
m.Assert().Equal(ArtifactTypeImage, m.resolver.GetArtifactType())
|
||||
func (m *manifestV2ProcessorTestSuite) TestGetArtifactType() {
|
||||
m.Assert().Equal(ArtifactTypeImage, m.processor.GetArtifactType())
|
||||
}
|
||||
|
||||
func (m *manifestV2ResolverTestSuite) TestListAdditionTypes() {
|
||||
additions := m.resolver.ListAdditionTypes()
|
||||
func (m *manifestV2ProcessorTestSuite) TestListAdditionTypes() {
|
||||
additions := m.processor.ListAdditionTypes()
|
||||
m.EqualValues([]string{AdditionTypeBuildHistory}, additions)
|
||||
}
|
||||
|
||||
func TestManifestV2ResolverTestSuite(t *testing.T) {
|
||||
suite.Run(t, &manifestV2ResolverTestSuite{})
|
||||
func TestManifestV2ProcessorTestSuite(t *testing.T) {
|
||||
suite.Run(t, &manifestV2ProcessorTestSuite{})
|
||||
}
|
72
src/api/artifact/processor/processor.go
Normal file
72
src/api/artifact/processor/processor.go
Normal file
@ -0,0 +1,72 @@
|
||||
// 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 processor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/pkg/artifact"
|
||||
)
|
||||
|
||||
var (
|
||||
// Registry for registered artifact processors
|
||||
Registry = map[string]Processor{}
|
||||
)
|
||||
|
||||
// Addition defines the specific addition of different artifacts: build history for image, values.yaml for chart, etc
|
||||
type Addition struct {
|
||||
Content []byte // the content of the addition
|
||||
ContentType string // the content type of the addition, returned as "Content-Type" header in API
|
||||
}
|
||||
|
||||
// Processor processes specified artifact
|
||||
type Processor interface {
|
||||
// GetArtifactType returns the type of one kind of artifact specified by media type
|
||||
GetArtifactType() string
|
||||
// ListAdditionTypes returns the supported addition types of one kind of artifact specified by media type
|
||||
ListAdditionTypes() []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
|
||||
// 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
|
||||
AbstractAddition(ctx context.Context, artifact *artifact.Artifact, additionType string) (addition *Addition, err error)
|
||||
}
|
||||
|
||||
// Register artifact processor, one processor can process multiple media types for one kind of artifact
|
||||
func Register(processor Processor, mediaTypes ...string) error {
|
||||
for _, mediaType := range mediaTypes {
|
||||
_, exist := Registry[mediaType]
|
||||
if exist {
|
||||
return fmt.Errorf("the processor to process media type %s already exists", mediaType)
|
||||
}
|
||||
Registry[mediaType] = processor
|
||||
log.Infof("the processor to process media type %s registered", mediaType)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get the artifact processor according to the media type
|
||||
func Get(mediaType string) Processor {
|
||||
processor := Registry[mediaType]
|
||||
// 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}
|
||||
}
|
||||
return processor
|
||||
}
|
79
src/api/artifact/processor/processor_test.go
Normal file
79
src/api/artifact/processor/processor_test.go
Normal file
@ -0,0 +1,79 @@
|
||||
// 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 processor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/goharbor/harbor/src/pkg/artifact"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type fakeProcessor struct{}
|
||||
|
||||
func (f *fakeProcessor) GetArtifactType() string {
|
||||
return ""
|
||||
}
|
||||
func (f *fakeProcessor) ListAdditionTypes() []string {
|
||||
return nil
|
||||
}
|
||||
func (f *fakeProcessor) AbstractMetadata(ctx context.Context, manifest []byte, artifact *artifact.Artifact) error {
|
||||
return nil
|
||||
}
|
||||
func (f *fakeProcessor) AbstractAddition(ctx context.Context, artifact *artifact.Artifact, additionType string) (*Addition, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
type processorTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func (p *processorTestSuite) SetupTest() {
|
||||
Registry = map[string]Processor{}
|
||||
}
|
||||
|
||||
func (p *processorTestSuite) TestRegister() {
|
||||
// success
|
||||
mediaType := "fake_media_type"
|
||||
err := Register(nil, mediaType)
|
||||
p.Require().Nil(err)
|
||||
|
||||
// conflict
|
||||
err = Register(nil, mediaType)
|
||||
p.Require().NotNil(err)
|
||||
}
|
||||
|
||||
func (p *processorTestSuite) TestGet() {
|
||||
// register a processor
|
||||
mediaType := "fake_media_type"
|
||||
err := Register(&fakeProcessor{}, mediaType)
|
||||
p.Require().Nil(err)
|
||||
|
||||
// get the processor
|
||||
processor := Get(mediaType)
|
||||
p.Require().NotNil(processor)
|
||||
_, ok := processor.(*fakeProcessor)
|
||||
p.True(ok)
|
||||
|
||||
// get the not existing processor
|
||||
processor = Get("not_existing_media_type")
|
||||
p.Require().NotNil(processor)
|
||||
_, ok = processor.(*defaultProcessor)
|
||||
p.True(ok)
|
||||
}
|
||||
|
||||
func TestProcessorTestSuite(t *testing.T) {
|
||||
suite.Run(t, &processorTestSuite{})
|
||||
}
|
@ -16,7 +16,7 @@ package migration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/goharbor/harbor/src/api/artifact/abstractor"
|
||||
art "github.com/goharbor/harbor/src/api/artifact"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/pkg/artifact"
|
||||
"github.com/goharbor/harbor/src/pkg/project"
|
||||
@ -25,7 +25,7 @@ import (
|
||||
)
|
||||
|
||||
func upgradeData(ctx context.Context) error {
|
||||
abstractor := abstractor.NewAbstractor()
|
||||
abstractor := art.NewAbstractor()
|
||||
pros, err := project.Mgr.List()
|
||||
if err != nil {
|
||||
return err
|
||||
@ -69,7 +69,7 @@ func upgradeData(ctx context.Context) error {
|
||||
return setDataVersion(ctx, 30)
|
||||
}
|
||||
|
||||
func abstract(ctx context.Context, abstractor abstractor.Abstractor, art *artifact.Artifact) error {
|
||||
func abstract(ctx context.Context, abstractor art.Abstractor, art *artifact.Artifact) error {
|
||||
// abstract the children
|
||||
for _, reference := range art.References {
|
||||
child, err := artifact.Mgr.Get(ctx, reference.ChildID)
|
||||
|
@ -25,7 +25,7 @@ import (
|
||||
"github.com/go-openapi/runtime"
|
||||
"github.com/go-openapi/runtime/middleware"
|
||||
"github.com/goharbor/harbor/src/api/artifact"
|
||||
"github.com/goharbor/harbor/src/api/artifact/abstractor/resolver"
|
||||
"github.com/goharbor/harbor/src/api/artifact/processor"
|
||||
"github.com/goharbor/harbor/src/api/event"
|
||||
"github.com/goharbor/harbor/src/api/repository"
|
||||
"github.com/goharbor/harbor/src/api/scan"
|
||||
@ -301,7 +301,7 @@ func (a *artifactAPI) GetAddition(ctx context.Context, params operation.GetAddit
|
||||
return a.SendError(ctx, err)
|
||||
}
|
||||
|
||||
var addition *resolver.Addition
|
||||
var addition *processor.Addition
|
||||
|
||||
if params.Addition == vulnerabilitiesAddition {
|
||||
addition, err = resolveVulnerabilitiesAddition(ctx, artifact)
|
||||
|
@ -22,7 +22,7 @@ import (
|
||||
"reflect"
|
||||
|
||||
"github.com/goharbor/harbor/src/api/artifact"
|
||||
"github.com/goharbor/harbor/src/api/artifact/abstractor/resolver"
|
||||
"github.com/goharbor/harbor/src/api/artifact/processor"
|
||||
"github.com/goharbor/harbor/src/api/scan"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/report"
|
||||
@ -37,7 +37,7 @@ func boolValue(v *bool) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func resolveVulnerabilitiesAddition(ctx context.Context, artifact *artifact.Artifact) (*resolver.Addition, error) {
|
||||
func resolveVulnerabilitiesAddition(ctx context.Context, artifact *artifact.Artifact) (*processor.Addition, error) {
|
||||
art := &v1.Artifact{
|
||||
NamespaceID: artifact.ProjectID,
|
||||
Repository: artifact.RepositoryName,
|
||||
@ -67,7 +67,7 @@ func resolveVulnerabilitiesAddition(ctx context.Context, artifact *artifact.Arti
|
||||
|
||||
content, _ := json.Marshal(vulnerabilities)
|
||||
|
||||
return &resolver.Addition{
|
||||
return &processor.Addition{
|
||||
Content: content,
|
||||
ContentType: "application/json",
|
||||
}, nil
|
||||
|
@ -1,43 +0,0 @@
|
||||
// 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"
|
||||
"github.com/goharbor/harbor/src/api/artifact/abstractor/resolver"
|
||||
"github.com/goharbor/harbor/src/pkg/artifact"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// FakeResolver is a fake resolver that implement the src/api/artifact/abstractor/resolver.Resolver interface
|
||||
type FakeResolver struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// ResolveMetadata ...
|
||||
func (f *FakeResolver) ResolveMetadata(ctx context.Context, manifest []byte, artifact *artifact.Artifact) error {
|
||||
args := f.Called()
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
// ResolveAddition ...
|
||||
func (f *FakeResolver) ResolveAddition(ctx context.Context, artifact *artifact.Artifact, additionType string) (*resolver.Addition, error) {
|
||||
args := f.Called()
|
||||
var addition *resolver.Addition
|
||||
if args.Get(0) != nil {
|
||||
addition = args.Get(0).(*resolver.Addition)
|
||||
}
|
||||
return addition, args.Error(1)
|
||||
}
|
@ -9,9 +9,9 @@ import (
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
q "github.com/goharbor/harbor/src/pkg/q"
|
||||
processor "github.com/goharbor/harbor/src/api/artifact/processor"
|
||||
|
||||
resolver "github.com/goharbor/harbor/src/api/artifact/abstractor/resolver"
|
||||
q "github.com/goharbor/harbor/src/pkg/q"
|
||||
|
||||
time "time"
|
||||
)
|
||||
@ -150,15 +150,15 @@ func (_m *Controller) Get(ctx context.Context, id int64, option *artifact.Option
|
||||
}
|
||||
|
||||
// GetAddition provides a mock function with given fields: ctx, artifactID, additionType
|
||||
func (_m *Controller) GetAddition(ctx context.Context, artifactID int64, additionType string) (*resolver.Addition, error) {
|
||||
func (_m *Controller) GetAddition(ctx context.Context, artifactID int64, additionType string) (*processor.Addition, error) {
|
||||
ret := _m.Called(ctx, artifactID, additionType)
|
||||
|
||||
var r0 *resolver.Addition
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64, string) *resolver.Addition); ok {
|
||||
var r0 *processor.Addition
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64, string) *processor.Addition); ok {
|
||||
r0 = rf(ctx, artifactID, additionType)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*resolver.Addition)
|
||||
r0 = ret.Get(0).(*processor.Addition)
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user