mirror of
https://github.com/goharbor/harbor
synced 2024-09-21 00:39:58 +00: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"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/goharbor/harbor/src/api/artifact/processor"
|
||||||
"github.com/goharbor/harbor/src/api/event"
|
"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/api/tag"
|
||||||
"github.com/goharbor/harbor/src/internal"
|
"github.com/goharbor/harbor/src/internal"
|
||||||
"github.com/goharbor/harbor/src/internal/orm"
|
"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"
|
||||||
"github.com/goharbor/harbor/src/pkg/immutabletag/match/rule"
|
"github.com/goharbor/harbor/src/pkg/immutabletag/match/rule"
|
||||||
"github.com/goharbor/harbor/src/pkg/label"
|
"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/registry"
|
||||||
"github.com/goharbor/harbor/src/pkg/signature"
|
"github.com/goharbor/harbor/src/pkg/signature"
|
||||||
"github.com/opencontainers/go-digest"
|
"github.com/opencontainers/go-digest"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
// registry image resolvers
|
// 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
|
// 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
|
// 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"
|
"github.com/goharbor/harbor/src/common/utils/log"
|
||||||
ierror "github.com/goharbor/harbor/src/internal/error"
|
ierror "github.com/goharbor/harbor/src/internal/error"
|
||||||
"github.com/goharbor/harbor/src/pkg/artifact"
|
"github.com/goharbor/harbor/src/pkg/artifact"
|
||||||
|
@ -94,7 +91,7 @@ type Controller interface {
|
||||||
// GetAddition returns the addition of the artifact.
|
// GetAddition returns the addition of the artifact.
|
||||||
// The addition is different according to the artifact type:
|
// The addition is different according to the artifact type:
|
||||||
// build history for image; values.yaml, readme and dependencies for chart, etc
|
// 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 to the specified artifact
|
||||||
AddLabel(ctx context.Context, artifactID int64, labelID int64) (err error)
|
AddLabel(ctx context.Context, artifactID int64, labelID int64) (err error)
|
||||||
// RemoveLabel from the specified artifact
|
// RemoveLabel from the specified artifact
|
||||||
|
@ -113,9 +110,9 @@ func NewController() Controller {
|
||||||
blobMgr: blob.Mgr,
|
blobMgr: blob.Mgr,
|
||||||
sigMgr: signature.GetManager(),
|
sigMgr: signature.GetManager(),
|
||||||
labelMgr: label.Mgr,
|
labelMgr: label.Mgr,
|
||||||
abstractor: abstractor.NewAbstractor(),
|
|
||||||
immutableMtr: rule.NewRuleMatcher(),
|
immutableMtr: rule.NewRuleMatcher(),
|
||||||
regCli: registry.Cli,
|
regCli: registry.Cli,
|
||||||
|
abstractor: NewAbstractor(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -129,9 +126,9 @@ type controller struct {
|
||||||
blobMgr blob.Manager
|
blobMgr blob.Manager
|
||||||
sigMgr signature.Manager
|
sigMgr signature.Manager
|
||||||
labelMgr label.Manager
|
labelMgr label.Manager
|
||||||
abstractor abstractor.Abstractor
|
|
||||||
immutableMtr match.ImmutableTagMatcher
|
immutableMtr match.ImmutableTagMatcher
|
||||||
regCli registry.Client
|
regCli registry.Client
|
||||||
|
abstractor Abstractor
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *controller) Ensure(ctx context.Context, repository, digest string, tags ...string) (bool, int64, error) {
|
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
|
// populate the artifact type
|
||||||
artifact.Type = descriptor.GetArtifactType(artifact.MediaType)
|
artifact.Type = processor.Get(artifact.MediaType).GetArtifactType()
|
||||||
|
|
||||||
// create it
|
// create it
|
||||||
// use orm.WithTransaction here to avoid the issue:
|
// 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")
|
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)
|
artifact, err := c.artMgr.Get(ctx, artifactID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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 {
|
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) {
|
func (c *controller) populateAdditionLinks(ctx context.Context, artifact *Artifact) {
|
||||||
types := descriptor.ListAdditionTypes(artifact.MediaType)
|
types := processor.Get(artifact.MediaType).ListAdditionTypes()
|
||||||
if len(types) > 0 {
|
if len(types) > 0 {
|
||||||
version := internal.GetAPIVersion(ctx)
|
version := internal.GetAPIVersion(ctx)
|
||||||
for _, t := range types {
|
for _, t := range types {
|
||||||
|
|
|
@ -19,8 +19,6 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"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/api/tag"
|
||||||
"github.com/goharbor/harbor/src/common/models"
|
"github.com/goharbor/harbor/src/common/models"
|
||||||
"github.com/goharbor/harbor/src/internal"
|
"github.com/goharbor/harbor/src/internal"
|
||||||
|
@ -53,26 +51,6 @@ func (f *fakeAbstractor) AbstractMetadata(ctx context.Context, artifact *artifac
|
||||||
args := f.Called()
|
args := f.Called()
|
||||||
return args.Error(0)
|
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 {
|
type controllerTestSuite struct {
|
||||||
suite.Suite
|
suite.Suite
|
||||||
|
@ -109,7 +87,6 @@ func (c *controllerTestSuite) SetupTest() {
|
||||||
immutableMtr: c.immutableMtr,
|
immutableMtr: c.immutableMtr,
|
||||||
regCli: c.regCli,
|
regCli: c.regCli,
|
||||||
}
|
}
|
||||||
descriptor.Register(&fakeDescriptor{}, "")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *controllerTestSuite) TestAssembleArtifact() {
|
func (c *controllerTestSuite) TestAssembleArtifact() {
|
||||||
|
@ -148,11 +125,6 @@ func (c *controllerTestSuite) TestAssembleArtifact() {
|
||||||
c.Require().NotNil(artifact)
|
c.Require().NotNil(artifact)
|
||||||
c.Equal(art.ID, artifact.ID)
|
c.Equal(art.ID, artifact.ID)
|
||||||
c.Contains(artifact.Tags, tg)
|
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)
|
c.Contains(artifact.Labels, lb)
|
||||||
// TODO check other fields of option
|
// TODO check other fields of option
|
||||||
}
|
}
|
||||||
|
@ -496,10 +468,9 @@ func (c *controllerTestSuite) TestUpdatePullTime() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *controllerTestSuite) TestGetAddition() {
|
func (c *controllerTestSuite) TestGetAddition() {
|
||||||
c.artMgr.On("Get").Return(nil, nil)
|
c.artMgr.On("Get").Return(&artifact.Artifact{}, nil)
|
||||||
c.abstractor.On("AbstractAddition").Return(nil, nil)
|
|
||||||
_, err := c.ctl.GetAddition(nil, 1, "addition")
|
_, err := c.ctl.GetAddition(nil, 1, "addition")
|
||||||
c.Require().Nil(err)
|
c.Require().NotNil(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *controllerTestSuite) TestAddTo() {
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"github.com/goharbor/harbor/src/api/artifact/abstractor/blob"
|
ps "github.com/goharbor/harbor/src/api/artifact/processor"
|
||||||
resolv "github.com/goharbor/harbor/src/api/artifact/abstractor/resolver"
|
"github.com/goharbor/harbor/src/api/artifact/processor/base"
|
||||||
"github.com/goharbor/harbor/src/api/artifact/descriptor"
|
"github.com/goharbor/harbor/src/api/artifact/processor/blob"
|
||||||
"github.com/goharbor/harbor/src/common/utils/log"
|
"github.com/goharbor/harbor/src/common/utils/log"
|
||||||
ierror "github.com/goharbor/harbor/src/internal/error"
|
ierror "github.com/goharbor/harbor/src/internal/error"
|
||||||
"github.com/goharbor/harbor/src/pkg/artifact"
|
"github.com/goharbor/harbor/src/pkg/artifact"
|
||||||
|
@ -34,61 +34,36 @@ const (
|
||||||
AdditionTypeValues = "VALUES.YAML"
|
AdditionTypeValues = "VALUES.YAML"
|
||||||
AdditionTypeReadme = "README.MD"
|
AdditionTypeReadme = "README.MD"
|
||||||
AdditionTypeDependencies = "DEPENDENCIES"
|
AdditionTypeDependencies = "DEPENDENCIES"
|
||||||
|
|
||||||
// TODO import it from helm chart repository
|
// TODO import it from helm chart repository
|
||||||
mediaType = "application/vnd.cncf.helm.config.v1+json"
|
mediaType = "application/vnd.cncf.helm.config.v1+json"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
resolver := &resolver{
|
pc := &processor{
|
||||||
blobFetcher: blob.Fcher,
|
blobFetcher: blob.Fcher,
|
||||||
chartOperator: chart.Optr,
|
chartOperator: chart.Optr,
|
||||||
}
|
}
|
||||||
if err := resolv.Register(resolver, mediaType); err != nil {
|
pc.ManifestProcessor = base.NewManifestProcessor()
|
||||||
log.Errorf("failed to register resolver for media type %s: %v", mediaType, err)
|
if err := ps.Register(pc, mediaType); err != nil {
|
||||||
return
|
log.Errorf("failed to register processor for media type %s: %v", mediaType, err)
|
||||||
}
|
|
||||||
if err := descriptor.Register(resolver, mediaType); err != nil {
|
|
||||||
log.Errorf("failed to register descriptor for media type %s: %v", mediaType, err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type resolver struct {
|
type processor struct {
|
||||||
|
*base.ManifestProcessor
|
||||||
blobFetcher blob.Fetcher
|
blobFetcher blob.Fetcher
|
||||||
chartOperator chart.Operator
|
chartOperator chart.Operator
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *resolver) ResolveMetadata(ctx context.Context, manifest []byte, artifact *artifact.Artifact) error {
|
func (p *processor) AbstractAddition(ctx context.Context, artifact *artifact.Artifact, addition string) (*ps.Addition, 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) {
|
|
||||||
if addition != AdditionTypeValues && addition != AdditionTypeReadme && addition != AdditionTypeDependencies {
|
if addition != AdditionTypeValues && addition != AdditionTypeReadme && addition != AdditionTypeDependencies {
|
||||||
return nil, ierror.New(nil).WithCode(ierror.BadRequestCode).
|
return nil, ierror.New(nil).WithCode(ierror.BadRequestCode).
|
||||||
WithMessage("addition %s isn't supported for %s", addition, ArtifactTypeChart)
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
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.
|
// chart do have two layers, one is config, we should resolve the other one.
|
||||||
layerDgst := layer.Digest.String()
|
layerDgst := layer.Digest.String()
|
||||||
if layerDgst != manifest.Config.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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
chartDetails, err := r.chartOperator.GetDetails(content)
|
chartDetails, err := p.chartOperator.GetDetails(content)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -128,7 +103,7 @@ func (r *resolver) ResolveAddition(ctx context.Context, artifact *artifact.Artif
|
||||||
additionContentType = "application/json; charset=utf-8"
|
additionContentType = "application/json; charset=utf-8"
|
||||||
}
|
}
|
||||||
|
|
||||||
return &resolv.Addition{
|
return &ps.Addition{
|
||||||
Content: additionContent,
|
Content: additionContent,
|
||||||
ContentType: additionContentType,
|
ContentType: additionContentType,
|
||||||
}, nil
|
}, nil
|
||||||
|
@ -137,10 +112,10 @@ func (r *resolver) ResolveAddition(ctx context.Context, artifact *artifact.Artif
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *resolver) GetArtifactType() string {
|
func (p *processor) GetArtifactType() string {
|
||||||
return ArtifactTypeChart
|
return ArtifactTypeChart
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *resolver) ListAdditionTypes() []string {
|
func (p *processor) ListAdditionTypes() []string {
|
||||||
return []string{AdditionTypeValues, AdditionTypeReadme, AdditionTypeDependencies}
|
return []string{AdditionTypeValues, AdditionTypeReadme, AdditionTypeDependencies}
|
||||||
}
|
}
|
|
@ -18,87 +18,34 @@ import (
|
||||||
ierror "github.com/goharbor/harbor/src/internal/error"
|
ierror "github.com/goharbor/harbor/src/internal/error"
|
||||||
"github.com/goharbor/harbor/src/pkg/artifact"
|
"github.com/goharbor/harbor/src/pkg/artifact"
|
||||||
chartserver "github.com/goharbor/harbor/src/pkg/chart"
|
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/goharbor/harbor/src/testing/pkg/chart"
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
helm_chart "helm.sh/helm/v3/pkg/chart"
|
helm_chart "helm.sh/helm/v3/pkg/chart"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
type resolverTestSuite struct {
|
type processorTestSuite struct {
|
||||||
suite.Suite
|
suite.Suite
|
||||||
resolver *resolver
|
processor *processor
|
||||||
blobFetcher *blob.FakeFetcher
|
blobFetcher *blob.FakeFetcher
|
||||||
chartOptr *chart.FakeOpertaor
|
chartOptr *chart.FakeOpertaor
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *resolverTestSuite) SetupTest() {
|
func (p *processorTestSuite) SetupTest() {
|
||||||
r.blobFetcher = &blob.FakeFetcher{}
|
p.blobFetcher = &blob.FakeFetcher{}
|
||||||
r.chartOptr = &chart.FakeOpertaor{}
|
p.chartOptr = &chart.FakeOpertaor{}
|
||||||
r.resolver = &resolver{
|
p.processor = &processor{
|
||||||
blobFetcher: r.blobFetcher,
|
blobFetcher: p.blobFetcher,
|
||||||
chartOperator: r.chartOptr,
|
chartOperator: p.chartOptr,
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *resolverTestSuite) TestResolveMetadata() {
|
func (p *processorTestSuite) TestAbstractAddition() {
|
||||||
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() {
|
|
||||||
// unknown addition
|
// unknown addition
|
||||||
_, err := r.resolver.ResolveAddition(nil, nil, "unknown_addition")
|
_, err := p.processor.AbstractAddition(nil, nil, "unknown_addition")
|
||||||
r.True(ierror.IsErr(err, ierror.BadRequestCode))
|
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}]}`
|
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{}
|
artifact := &artifact.Artifact{}
|
||||||
r.blobFetcher.On("FetchManifest").Return("", []byte(chartManifest), nil)
|
p.blobFetcher.On("FetchManifest").Return("", []byte(chartManifest), nil)
|
||||||
r.blobFetcher.On("FetchLayer").Return([]byte(chartYaml), nil)
|
p.blobFetcher.On("FetchLayer").Return([]byte(chartYaml), nil)
|
||||||
r.chartOptr.On("GetDetails").Return(chartDetails, nil)
|
p.chartOptr.On("GetDetails").Return(chartDetails, nil)
|
||||||
|
|
||||||
// values.yaml
|
// values.yaml
|
||||||
addition, err := r.resolver.ResolveAddition(nil, artifact, AdditionTypeValues)
|
addition, err := p.processor.AbstractAddition(nil, artifact, AdditionTypeValues)
|
||||||
r.Require().Nil(err)
|
p.Require().Nil(err)
|
||||||
r.Equal("text/plain; charset=utf-8", addition.ContentType)
|
p.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))
|
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
|
// README.md
|
||||||
addition, err = r.resolver.ResolveAddition(nil, artifact, AdditionTypeReadme)
|
addition, err = p.processor.AbstractAddition(nil, artifact, AdditionTypeReadme)
|
||||||
r.Require().Nil(err)
|
p.Require().Nil(err)
|
||||||
r.Equal("text/markdown; charset=utf-8", addition.ContentType)
|
p.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))
|
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
|
// README.md
|
||||||
addition, err = r.resolver.ResolveAddition(nil, artifact, AdditionTypeDependencies)
|
addition, err = p.processor.AbstractAddition(nil, artifact, AdditionTypeDependencies)
|
||||||
r.Require().Nil(err)
|
p.Require().Nil(err)
|
||||||
r.Equal("application/json; charset=utf-8", addition.ContentType)
|
p.Equal("application/json; charset=utf-8", addition.ContentType)
|
||||||
r.Equal(`[{"name":"harbor","version":"v1.10","repository":"github.com/goharbor"}]`, string(addition.Content))
|
p.Equal(`[{"name":"harbor","version":"v1.10","repository":"github.com/goharbor"}]`, string(addition.Content))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *resolverTestSuite) TestGetArtifactType() {
|
func (p *processorTestSuite) TestGetArtifactType() {
|
||||||
r.Assert().Equal(ArtifactTypeChart, r.resolver.GetArtifactType())
|
p.Assert().Equal(ArtifactTypeChart, p.processor.GetArtifactType())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *resolverTestSuite) TestListAdditionTypes() {
|
func (p *processorTestSuite) TestListAdditionTypes() {
|
||||||
additions := r.resolver.ListAdditionTypes()
|
additions := p.processor.ListAdditionTypes()
|
||||||
r.EqualValues([]string{AdditionTypeValues, AdditionTypeReadme, AdditionTypeDependencies}, additions)
|
p.EqualValues([]string{AdditionTypeValues, AdditionTypeReadme, AdditionTypeDependencies}, additions)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestResolverTestSuite(t *testing.T) {
|
func TestProcessorTestSuite(t *testing.T) {
|
||||||
suite.Run(t, &resolverTestSuite{})
|
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
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
package descriptor
|
package processor
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
type descriptorTestSuite struct {
|
type defaultProcessorTestSuite struct {
|
||||||
suite.Suite
|
suite.Suite
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *descriptorTestSuite) TestParseArtifactType() {
|
func (d *defaultProcessorTestSuite) TestGetArtifactType() {
|
||||||
mediaType := ""
|
mediaType := ""
|
||||||
typee := parseArtifactType(mediaType)
|
processor := &defaultProcessor{mediaType: mediaType}
|
||||||
|
typee := processor.GetArtifactType()
|
||||||
d.Equal(ArtifactTypeUnknown, typee)
|
d.Equal(ArtifactTypeUnknown, typee)
|
||||||
|
|
||||||
mediaType = "unknown"
|
mediaType = "unknown"
|
||||||
typee = parseArtifactType(mediaType)
|
processor = &defaultProcessor{mediaType: mediaType}
|
||||||
|
typee = processor.GetArtifactType()
|
||||||
d.Equal(ArtifactTypeUnknown, typee)
|
d.Equal(ArtifactTypeUnknown, typee)
|
||||||
|
|
||||||
mediaType = "application/vnd.oci.image.config.v1+json"
|
mediaType = "application/vnd.oci.image.config.v1+json"
|
||||||
typee = parseArtifactType(mediaType)
|
processor = &defaultProcessor{mediaType: mediaType}
|
||||||
|
typee = processor.GetArtifactType()
|
||||||
d.Equal("IMAGE", typee)
|
d.Equal("IMAGE", typee)
|
||||||
|
|
||||||
mediaType = "application/vnd.cncf.helm.chart.config.v1+json"
|
mediaType = "application/vnd.cncf.helm.chart.config.v1+json"
|
||||||
typee = parseArtifactType(mediaType)
|
processor = &defaultProcessor{mediaType: mediaType}
|
||||||
|
typee = processor.GetArtifactType()
|
||||||
d.Equal("HELM.CHART", typee)
|
d.Equal("HELM.CHART", typee)
|
||||||
|
|
||||||
mediaType = "application/vnd.sylabs.sif.config.v1+json"
|
mediaType = "application/vnd.sylabs.sif.config.v1+json"
|
||||||
typee = parseArtifactType(mediaType)
|
processor = &defaultProcessor{mediaType: mediaType}
|
||||||
|
typee = processor.GetArtifactType()
|
||||||
d.Equal("SIF", typee)
|
d.Equal("SIF", typee)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDescriptorTestSuite(t *testing.T) {
|
func TestDefaultProcessorTestSuite(t *testing.T) {
|
||||||
suite.Run(t, &descriptorTestSuite{})
|
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"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"github.com/docker/distribution/manifest/schema1"
|
"github.com/docker/distribution/manifest/schema1"
|
||||||
"github.com/goharbor/harbor/src/api/artifact/abstractor/resolver"
|
"github.com/goharbor/harbor/src/api/artifact/processor"
|
||||||
"github.com/goharbor/harbor/src/api/artifact/descriptor"
|
|
||||||
"github.com/goharbor/harbor/src/common/utils/log"
|
"github.com/goharbor/harbor/src/common/utils/log"
|
||||||
ierror "github.com/goharbor/harbor/src/internal/error"
|
ierror "github.com/goharbor/harbor/src/internal/error"
|
||||||
"github.com/goharbor/harbor/src/pkg/artifact"
|
"github.com/goharbor/harbor/src/pkg/artifact"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
rslver := &manifestV1Resolver{}
|
pc := &manifestV1Processor{}
|
||||||
if err := resolver.Register(rslver, schema1.MediaTypeSignedManifest); err != nil {
|
if err := processor.Register(pc, schema1.MediaTypeSignedManifest); err != nil {
|
||||||
log.Errorf("failed to register resolver for media type %s: %v", schema1.MediaTypeSignedManifest, err)
|
log.Errorf("failed to register processor 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)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// manifestV1Resolver resolve artifact with docker v1 manifest
|
// manifestV1Processor processes image with docker v1 manifest
|
||||||
type manifestV1Resolver struct {
|
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{}
|
mani := &schema1.Manifest{}
|
||||||
if err := json.Unmarshal([]byte(manifest), mani); err != nil {
|
if err := json.Unmarshal(manifest, mani); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if artifact.ExtraAttrs == nil {
|
if artifact.ExtraAttrs == nil {
|
||||||
|
@ -53,15 +48,15 @@ func (m *manifestV1Resolver) ResolveMetadata(ctx context.Context, manifest []byt
|
||||||
return nil
|
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).
|
return nil, ierror.New(nil).WithCode(ierror.BadRequestCode).
|
||||||
WithMessage("addition %s isn't supported for %s(manifest version 1)", addition, ArtifactTypeImage)
|
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
|
return ArtifactTypeImage
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *manifestV1Resolver) ListAdditionTypes() []string {
|
func (m *manifestV1Processor) ListAdditionTypes() []string {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
|
@ -21,17 +21,17 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
type manifestV1ResolverTestSuite struct {
|
type manifestV1ProcessorTestSuite struct {
|
||||||
suite.Suite
|
suite.Suite
|
||||||
resolver *manifestV1Resolver
|
processor *manifestV1Processor
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *manifestV1ResolverTestSuite) SetupSuite() {
|
func (m *manifestV1ProcessorTestSuite) SetupSuite() {
|
||||||
m.resolver = &manifestV1Resolver{}
|
m.processor = &manifestV1Processor{}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *manifestV1ResolverTestSuite) TestResolveMetadata() {
|
func (m *manifestV1ProcessorTestSuite) TestAbstractMetadata() {
|
||||||
manifest := `{
|
manifest := `{
|
||||||
"name": "hello-world",
|
"name": "hello-world",
|
||||||
"tag": "latest",
|
"tag": "latest",
|
||||||
|
@ -78,25 +78,25 @@ func (m *manifestV1ResolverTestSuite) TestResolveMetadata() {
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
artifact := &artifact.Artifact{}
|
artifact := &artifact.Artifact{}
|
||||||
err := m.resolver.ResolveMetadata(nil, []byte(manifest), artifact)
|
err := m.processor.AbstractMetadata(nil, []byte(manifest), artifact)
|
||||||
m.Require().Nil(err)
|
m.Require().Nil(err)
|
||||||
m.Assert().Equal("amd64", artifact.ExtraAttrs["architecture"].(string))
|
m.Assert().Equal("amd64", artifact.ExtraAttrs["architecture"].(string))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *manifestV1ResolverTestSuite) TestResolveAddition() {
|
func (m *manifestV1ProcessorTestSuite) TestAbstractAddition() {
|
||||||
_, err := m.resolver.ResolveAddition(nil, nil, AdditionTypeBuildHistory)
|
_, err := m.processor.AbstractAddition(nil, nil, AdditionTypeBuildHistory)
|
||||||
m.True(ierror.IsErr(err, ierror.BadRequestCode))
|
m.True(ierror.IsErr(err, ierror.BadRequestCode))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *manifestV1ResolverTestSuite) TestGetArtifactType() {
|
func (m *manifestV1ProcessorTestSuite) TestGetArtifactType() {
|
||||||
m.Assert().Equal(ArtifactTypeImage, m.resolver.GetArtifactType())
|
m.Assert().Equal(ArtifactTypeImage, m.processor.GetArtifactType())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *manifestV1ResolverTestSuite) TestListAdditionTypes() {
|
func (m *manifestV1ProcessorTestSuite) TestListAdditionTypes() {
|
||||||
additions := m.resolver.ListAdditionTypes()
|
additions := m.processor.ListAdditionTypes()
|
||||||
m.Len(additions, 0)
|
m.Len(additions, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestManifestV1ResolverTestSuite(t *testing.T) {
|
func TestManifestV1ProcessorTestSuite(t *testing.T) {
|
||||||
suite.Run(t, &manifestV1ResolverTestSuite{})
|
suite.Run(t, &manifestV1ProcessorTestSuite{})
|
||||||
}
|
}
|
|
@ -18,9 +18,9 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"github.com/docker/distribution/manifest/schema2"
|
"github.com/docker/distribution/manifest/schema2"
|
||||||
"github.com/goharbor/harbor/src/api/artifact/abstractor/blob"
|
"github.com/goharbor/harbor/src/api/artifact/processor"
|
||||||
"github.com/goharbor/harbor/src/api/artifact/abstractor/resolver"
|
"github.com/goharbor/harbor/src/api/artifact/processor/base"
|
||||||
"github.com/goharbor/harbor/src/api/artifact/descriptor"
|
"github.com/goharbor/harbor/src/api/artifact/processor/blob"
|
||||||
"github.com/goharbor/harbor/src/common/utils/log"
|
"github.com/goharbor/harbor/src/common/utils/log"
|
||||||
ierror "github.com/goharbor/harbor/src/internal/error"
|
ierror "github.com/goharbor/harbor/src/internal/error"
|
||||||
"github.com/goharbor/harbor/src/pkg/artifact"
|
"github.com/goharbor/harbor/src/pkg/artifact"
|
||||||
|
@ -35,53 +35,27 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
rslver := &manifestV2Resolver{
|
pc := &manifestV2Processor{
|
||||||
blobFetcher: blob.Fcher,
|
blobFetcher: blob.Fcher,
|
||||||
}
|
}
|
||||||
|
pc.ManifestProcessor = base.NewManifestProcessor("created", "author", "architecture", "os")
|
||||||
mediaTypes := []string{
|
mediaTypes := []string{
|
||||||
v1.MediaTypeImageConfig,
|
v1.MediaTypeImageConfig,
|
||||||
schema2.MediaTypeImageConfig,
|
schema2.MediaTypeImageConfig,
|
||||||
}
|
}
|
||||||
if err := resolver.Register(rslver, mediaTypes...); err != nil {
|
if err := processor.Register(pc, mediaTypes...); err != nil {
|
||||||
log.Errorf("failed to register resolver for media type %v: %v", mediaTypes, err)
|
log.Errorf("failed to register processor 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
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// manifestV2Resolver resolve artifact with OCI manifest and docker v2 manifest
|
// manifestV2Processor processes image with OCI manifest and docker v2 manifest
|
||||||
type manifestV2Resolver struct {
|
type manifestV2Processor struct {
|
||||||
|
*base.ManifestProcessor
|
||||||
blobFetcher blob.Fetcher
|
blobFetcher blob.Fetcher
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *manifestV2Resolver) ResolveMetadata(ctx context.Context, content []byte, artifact *artifact.Artifact) error {
|
func (m *manifestV2Processor) AbstractAddition(ctx context.Context, artifact *artifact.Artifact, addition string) (*processor.Addition, 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) {
|
|
||||||
if addition != AdditionTypeBuildHistory {
|
if addition != AdditionTypeBuildHistory {
|
||||||
return nil, ierror.New(nil).WithCode(ierror.BadRequestCode).
|
return nil, ierror.New(nil).WithCode(ierror.BadRequestCode).
|
||||||
WithMessage("addition %s isn't supported for %s(manifest version 2)", addition, ArtifactTypeImage)
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &resolver.Addition{
|
return &processor.Addition{
|
||||||
Content: content,
|
Content: content,
|
||||||
ContentType: "application/json; charset=utf-8",
|
ContentType: "application/json; charset=utf-8",
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *manifestV2Resolver) GetArtifactType() string {
|
func (m *manifestV2Processor) GetArtifactType() string {
|
||||||
return ArtifactTypeImage
|
return ArtifactTypeImage
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *manifestV2Resolver) ListAdditionTypes() []string {
|
func (m *manifestV2Processor) ListAdditionTypes() []string {
|
||||||
return []string{AdditionTypeBuildHistory}
|
return []string{AdditionTypeBuildHistory}
|
||||||
}
|
}
|
|
@ -17,7 +17,7 @@ package image
|
||||||
import (
|
import (
|
||||||
ierror "github.com/goharbor/harbor/src/internal/error"
|
ierror "github.com/goharbor/harbor/src/internal/error"
|
||||||
"github.com/goharbor/harbor/src/pkg/artifact"
|
"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"
|
"github.com/stretchr/testify/suite"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
@ -118,54 +118,43 @@ var (
|
||||||
}`
|
}`
|
||||||
)
|
)
|
||||||
|
|
||||||
type manifestV2ResolverTestSuite struct {
|
type manifestV2ProcessorTestSuite struct {
|
||||||
suite.Suite
|
suite.Suite
|
||||||
resolver *manifestV2Resolver
|
processor *manifestV2Processor
|
||||||
blobFetcher *blob.FakeFetcher
|
blobFetcher *blob.FakeFetcher
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *manifestV2ResolverTestSuite) SetupTest() {
|
func (m *manifestV2ProcessorTestSuite) SetupTest() {
|
||||||
m.blobFetcher = &blob.FakeFetcher{}
|
m.blobFetcher = &blob.FakeFetcher{}
|
||||||
m.resolver = &manifestV2Resolver{
|
m.processor = &manifestV2Processor{
|
||||||
blobFetcher: m.blobFetcher,
|
blobFetcher: m.blobFetcher,
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *manifestV2ResolverTestSuite) TestResolveMetadata() {
|
func (m *manifestV2ProcessorTestSuite) TestAbstractAddition() {
|
||||||
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() {
|
|
||||||
// unknown addition
|
// 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))
|
m.True(ierror.IsErr(err, ierror.BadRequestCode))
|
||||||
|
|
||||||
// build history
|
// build history
|
||||||
artifact := &artifact.Artifact{}
|
artifact := &artifact.Artifact{}
|
||||||
m.blobFetcher.On("FetchManifest").Return("", []byte(manifest), nil)
|
m.blobFetcher.On("FetchManifest").Return("", []byte(manifest), nil)
|
||||||
m.blobFetcher.On("FetchLayer").Return([]byte(config), 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.Require().Nil(err)
|
||||||
m.Equal("application/json; charset=utf-8", addition.ContentType)
|
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))
|
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() {
|
func (m *manifestV2ProcessorTestSuite) TestGetArtifactType() {
|
||||||
m.Assert().Equal(ArtifactTypeImage, m.resolver.GetArtifactType())
|
m.Assert().Equal(ArtifactTypeImage, m.processor.GetArtifactType())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *manifestV2ResolverTestSuite) TestListAdditionTypes() {
|
func (m *manifestV2ProcessorTestSuite) TestListAdditionTypes() {
|
||||||
additions := m.resolver.ListAdditionTypes()
|
additions := m.processor.ListAdditionTypes()
|
||||||
m.EqualValues([]string{AdditionTypeBuildHistory}, additions)
|
m.EqualValues([]string{AdditionTypeBuildHistory}, additions)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestManifestV2ResolverTestSuite(t *testing.T) {
|
func TestManifestV2ProcessorTestSuite(t *testing.T) {
|
||||||
suite.Run(t, &manifestV2ResolverTestSuite{})
|
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 (
|
import (
|
||||||
"context"
|
"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/common/utils/log"
|
||||||
"github.com/goharbor/harbor/src/pkg/artifact"
|
"github.com/goharbor/harbor/src/pkg/artifact"
|
||||||
"github.com/goharbor/harbor/src/pkg/project"
|
"github.com/goharbor/harbor/src/pkg/project"
|
||||||
|
@ -25,7 +25,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func upgradeData(ctx context.Context) error {
|
func upgradeData(ctx context.Context) error {
|
||||||
abstractor := abstractor.NewAbstractor()
|
abstractor := art.NewAbstractor()
|
||||||
pros, err := project.Mgr.List()
|
pros, err := project.Mgr.List()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -69,7 +69,7 @@ func upgradeData(ctx context.Context) error {
|
||||||
return setDataVersion(ctx, 30)
|
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
|
// abstract the children
|
||||||
for _, reference := range art.References {
|
for _, reference := range art.References {
|
||||||
child, err := artifact.Mgr.Get(ctx, reference.ChildID)
|
child, err := artifact.Mgr.Get(ctx, reference.ChildID)
|
||||||
|
|
|
@ -25,7 +25,7 @@ import (
|
||||||
"github.com/go-openapi/runtime"
|
"github.com/go-openapi/runtime"
|
||||||
"github.com/go-openapi/runtime/middleware"
|
"github.com/go-openapi/runtime/middleware"
|
||||||
"github.com/goharbor/harbor/src/api/artifact"
|
"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/event"
|
||||||
"github.com/goharbor/harbor/src/api/repository"
|
"github.com/goharbor/harbor/src/api/repository"
|
||||||
"github.com/goharbor/harbor/src/api/scan"
|
"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)
|
return a.SendError(ctx, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var addition *resolver.Addition
|
var addition *processor.Addition
|
||||||
|
|
||||||
if params.Addition == vulnerabilitiesAddition {
|
if params.Addition == vulnerabilitiesAddition {
|
||||||
addition, err = resolveVulnerabilitiesAddition(ctx, artifact)
|
addition, err = resolveVulnerabilitiesAddition(ctx, artifact)
|
||||||
|
|
|
@ -22,7 +22,7 @@ import (
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
||||||
"github.com/goharbor/harbor/src/api/artifact"
|
"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/api/scan"
|
||||||
"github.com/goharbor/harbor/src/common/utils/log"
|
"github.com/goharbor/harbor/src/common/utils/log"
|
||||||
"github.com/goharbor/harbor/src/pkg/scan/report"
|
"github.com/goharbor/harbor/src/pkg/scan/report"
|
||||||
|
@ -37,7 +37,7 @@ func boolValue(v *bool) bool {
|
||||||
return false
|
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{
|
art := &v1.Artifact{
|
||||||
NamespaceID: artifact.ProjectID,
|
NamespaceID: artifact.ProjectID,
|
||||||
Repository: artifact.RepositoryName,
|
Repository: artifact.RepositoryName,
|
||||||
|
@ -67,7 +67,7 @@ func resolveVulnerabilitiesAddition(ctx context.Context, artifact *artifact.Arti
|
||||||
|
|
||||||
content, _ := json.Marshal(vulnerabilities)
|
content, _ := json.Marshal(vulnerabilities)
|
||||||
|
|
||||||
return &resolver.Addition{
|
return &processor.Addition{
|
||||||
Content: content,
|
Content: content,
|
||||||
ContentType: "application/json",
|
ContentType: "application/json",
|
||||||
}, nil
|
}, 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"
|
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"
|
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
|
// 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)
|
ret := _m.Called(ctx, artifactID, additionType)
|
||||||
|
|
||||||
var r0 *resolver.Addition
|
var r0 *processor.Addition
|
||||||
if rf, ok := ret.Get(0).(func(context.Context, int64, string) *resolver.Addition); ok {
|
if rf, ok := ret.Get(0).(func(context.Context, int64, string) *processor.Addition); ok {
|
||||||
r0 = rf(ctx, artifactID, additionType)
|
r0 = rf(ctx, artifactID, additionType)
|
||||||
} else {
|
} else {
|
||||||
if ret.Get(0) != nil {
|
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