diff --git a/icons/wasm.png b/icons/wasm.png new file mode 100644 index 000000000..9df412654 Binary files /dev/null and b/icons/wasm.png differ diff --git a/src/controller/artifact/abstractor.go b/src/controller/artifact/abstractor.go index f8c52f842..84120c688 100644 --- a/src/controller/artifact/abstractor.go +++ b/src/controller/artifact/abstractor.go @@ -18,6 +18,7 @@ import ( "context" "encoding/json" "fmt" + "github.com/goharbor/harbor/src/controller/artifact/processor/wasm" "github.com/goharbor/harbor/src/lib/q" "github.com/goharbor/harbor/src/pkg" @@ -126,6 +127,11 @@ func (a *abstractor) abstractManifestV2Metadata(artifact *artifact.Artifact, con } // use the "manifest.config.mediatype" as the media type of the artifact artifact.MediaType = manifest.Config.MediaType + + if manifest.Annotations[wasm.AnnotationVariantKey] == wasm.AnnotationVariantValue || manifest.Annotations[wasm.AnnotationHandlerKey] == wasm.AnnotationHandlerValue { + artifact.MediaType = wasm.MediaType + } + // set size artifact.Size = int64(len(content)) + manifest.Config.Size for _, layer := range manifest.Layers { diff --git a/src/controller/artifact/controller.go b/src/controller/artifact/controller.go index 7b2ecfe99..bf0d76f56 100644 --- a/src/controller/artifact/controller.go +++ b/src/controller/artifact/controller.go @@ -28,6 +28,7 @@ import ( "github.com/goharbor/harbor/src/controller/artifact/processor/chart" "github.com/goharbor/harbor/src/controller/artifact/processor/cnab" "github.com/goharbor/harbor/src/controller/artifact/processor/image" + "github.com/goharbor/harbor/src/controller/artifact/processor/wasm" "github.com/goharbor/harbor/src/lib/icon" "github.com/goharbor/harbor/src/controller/artifact/processor" @@ -72,6 +73,7 @@ var ( image.ArtifactTypeImage: icon.DigestOfIconImage, chart.ArtifactTypeChart: icon.DigestOfIconChart, cnab.ArtifactTypeCNAB: icon.DigestOfIconCNAB, + wasm.ArtifactTypeWASM: icon.DigestOfIconWASM, } ) diff --git a/src/controller/artifact/processor/wasm/wasm.go b/src/controller/artifact/processor/wasm/wasm.go new file mode 100644 index 000000000..ad0fbc4a2 --- /dev/null +++ b/src/controller/artifact/processor/wasm/wasm.go @@ -0,0 +1,135 @@ +// 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 wasm + +import ( + "context" + "encoding/json" + "github.com/goharbor/harbor/src/controller/artifact/processor" + "github.com/goharbor/harbor/src/controller/artifact/processor/base" + "github.com/goharbor/harbor/src/lib/errors" + "github.com/goharbor/harbor/src/lib/log" + "github.com/goharbor/harbor/src/pkg/artifact" + "github.com/opencontainers/image-spec/specs-go/v1" +) + +// const definitions +const ( + // ArtifactTypeWASM is the artifact type for image + ArtifactTypeWASM = "WASM" + AdditionTypeBuildHistory = "BUILD_HISTORY" + + // AnnotationVariantKey and AnnotationVariantValue is available key-value pair to identify an annotation fashion wasm artifact + AnnotationVariantKey = "module.wasm.image/variant" + AnnotationVariantValue = "compat" + + // AnnotationHandlerKey and AnnotationHandlerValue is another available key-value pair to identify an annotation fashion wasm artifact + AnnotationHandlerKey = "run.oci.handler" + AnnotationHandlerValue = "wasm" + + MediaType = "application/vnd.wasm.config.v1+json" +) + +func init() { + pc := &Processor{} + pc.ManifestProcessor = base.NewManifestProcessor() + mediaTypes := []string{ + MediaType, + } + if err := processor.Register(pc, mediaTypes...); err != nil { + log.Errorf("failed to register processor for media type %v: %v", mediaTypes, err) + return + } +} + +// Processor processes image with OCI manifest and docker v2 manifest +type Processor struct { + *base.ManifestProcessor +} + +func (m *Processor) AbstractMetadata(ctx context.Context, art *artifact.Artifact, manifestBody []byte) error { + art.ExtraAttrs = map[string]interface{}{} + manifest := &v1.Manifest{} + if err := json.Unmarshal(manifestBody, manifest); err != nil { + return err + } + + if art.ExtraAttrs == nil { + art.ExtraAttrs = map[string]interface{}{} + } + if manifest.Annotations[AnnotationVariantKey] == AnnotationVariantValue || manifest.Annotations[AnnotationHandlerKey] == AnnotationHandlerValue { + // for annotation way + config := &v1.Image{} + if err := m.UnmarshalConfig(ctx, art.RepositoryName, manifestBody, config); err != nil { + return err + } + art.ExtraAttrs["manifest.config.mediaType"] = manifest.Config.MediaType + art.ExtraAttrs["created"] = config.Created + art.ExtraAttrs["architecture"] = config.Architecture + art.ExtraAttrs["os"] = config.OS + art.ExtraAttrs["config"] = config.Config + // if the author is null, try to get it from labels: + // https://docs.docker.com/engine/reference/builder/#maintainer-deprecated + author := config.Author + if len(author) == 0 && len(config.Config.Labels) > 0 { + author = config.Config.Labels["maintainer"] + } + art.ExtraAttrs["author"] = author + } else { + // for wasm-to-oci way + art.ExtraAttrs["manifest.config.mediaType"] = MediaType + if len(manifest.Layers) > 0 { + art.ExtraAttrs["manifest.layers.mediaType"] = manifest.Layers[0].MediaType + art.ExtraAttrs["org.opencontainers.image.title"] = manifest.Layers[0].Annotations["org.opencontainers.image.title"] + } + } + return nil +} + +func (m *Processor) AbstractAddition(ctx context.Context, artifact *artifact.Artifact, addition string) (*processor.Addition, error) { + if addition != AdditionTypeBuildHistory { + return nil, errors.New(nil).WithCode(errors.BadRequestCode). + WithMessage("addition %s isn't supported for %s(manifest version 2)", addition, ArtifactTypeWASM) + } + + mani, _, err := m.RegCli.PullManifest(artifact.RepositoryName, artifact.Digest) + if err != nil { + return nil, err + } + _, content, err := mani.Payload() + if err != nil { + return nil, err + } + config := &v1.Image{} + if err = m.ManifestProcessor.UnmarshalConfig(ctx, artifact.RepositoryName, content, config); err != nil { + return nil, err + } + content, err = json.Marshal(config.History) + if err != nil { + return nil, err + } + return &processor.Addition{ + Content: content, + ContentType: "application/json; charset=utf-8", + }, nil +} + +func (m *Processor) GetArtifactType(ctx context.Context, artifact *artifact.Artifact) string { + return ArtifactTypeWASM +} + +func (m *Processor) ListAdditionTypes(ctx context.Context, artifact *artifact.Artifact) []string { + return []string{AdditionTypeBuildHistory} +} diff --git a/src/controller/artifact/processor/wasm/wasm_test.go b/src/controller/artifact/processor/wasm/wasm_test.go new file mode 100644 index 000000000..acb57d92c --- /dev/null +++ b/src/controller/artifact/processor/wasm/wasm_test.go @@ -0,0 +1,180 @@ +// 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 wasm + +import ( + "bytes" + "encoding/json" + + "io/ioutil" + "strings" + "testing" + + "github.com/docker/distribution/manifest/schema2" + + "github.com/goharbor/harbor/src/controller/artifact/processor/base" + "github.com/goharbor/harbor/src/lib/errors" + "github.com/goharbor/harbor/src/pkg/artifact" + "github.com/goharbor/harbor/src/testing/mock" + "github.com/goharbor/harbor/src/testing/pkg/registry" + "github.com/stretchr/testify/suite" +) + +var ( + // For OCI fashion wasm artifact + oci_manifest = `{ + "schemaVersion":2, + "config":{ + "mediaType":"application/vnd.wasm.config.v1+json", + "digest":"sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", + "size":2 + }, + "layers":[ + { + "mediaType":"application/vnd.wasm.content.layer.v1+wasm", + "digest":"sha256:d43012458290e4e2a350055bbe4a9f49fd4fb6b51d412089301e63ea4397ab4f", + "size":3951005, + "annotations":{ + "org.opencontainers.image.title":"test.wasm" + } + } + ] +}` + oci_config = `{}` + + // For annotation fashion wasm artifact + annnotated_manifest = `{ + "schemaVersion":2, + "mediaType":"application/vnd.oci.image.manifest.v1+json", + "config":{ + "mediaType":"application/vnd.oci.image.config.v1+json", + "digest":"sha256:6fd90b7cd05366c82ca32a3ff259e62dcb15b3b5e9672fe7d45609f29b6c1e95", + "size":637 + }, + "layers":[ + { + "mediaType":"application/vnd.oci.image.layer.v1.tar+gzip", + "digest":"sha256:818ab7fdcd8d16270f01795d6aee7af0d1a06c71ce2cd3c1e8d8f946e9475450", + "size":500361 + } + ], + "annotations":{ + "module.wasm.image/variant":"compat", + "org.opencontainers.image.base.digest":"", + "org.opencontainers.image.base.name":"" + } +}` + annnotated_config = `{ + "created":"2022-03-02T09:02:41.01773982Z", + "architecture":"amd64", + "os":"linux", + "config":{ + "Env":[ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ], + "Cmd":[ + "/sleep.wasm" + ], + "Labels":{ + "io.buildah.version":"1.25.0-dev" + } + }, + "rootfs":{ + "type":"layers", + "diff_ids":[ + "sha256:65885aa5fd4c157de98241de75669c4bf0d6f17d220c40069de31a572371a80d" + ] + }, + "history":[ + { + "created":"2022-03-02T09:02:41.011039932Z", + "created_by":"/bin/sh -c #(nop) COPY file:9fc00231cd29a2b8f76cfeaa9bc2355a7df54585b51d1f5dc98daf51557614b9 in / ", + "empty_layer":true + }, + { + "created":"2022-03-02T09:02:41.043350231Z", + "created_by":"/bin/sh -c #(nop) CMD [\"/sleep.wasm\"]" + } + ] +}` +) + +type WASMProcessorTestSuite struct { + suite.Suite + processor *Processor + regCli *registry.FakeClient +} + +func (m *WASMProcessorTestSuite) SetupTest() { + m.regCli = ®istry.FakeClient{} + m.processor = &Processor{} + m.processor.ManifestProcessor = &base.ManifestProcessor{RegCli: m.regCli} +} + +func (m *WASMProcessorTestSuite) TestAbstractMetadataForAnnotationFashion() { + artifact := &artifact.Artifact{} + m.regCli.On("PullBlob", mock.Anything, mock.Anything).Return(0, ioutil.NopCloser(bytes.NewReader([]byte(annnotated_config))), nil) + err := m.processor.AbstractMetadata(nil, artifact, []byte(annnotated_manifest)) + m.Require().Nil(err) + m.NotNil(artifact.ExtraAttrs["created"]) + m.Equal("amd64", artifact.ExtraAttrs["architecture"]) + m.Equal("linux", artifact.ExtraAttrs["os"]) + m.NotNil(artifact.ExtraAttrs["config"]) + m.regCli.AssertExpectations(m.T()) + +} + +func (m *WASMProcessorTestSuite) TestAbstractMetadataForOCIFashion() { + artifact := &artifact.Artifact{} + err := m.processor.AbstractMetadata(nil, artifact, []byte(oci_manifest)) + m.Require().Nil(err) + m.NotNil(artifact.ExtraAttrs["org.opencontainers.image.title"]) + m.Equal(MediaType, artifact.ExtraAttrs["manifest.config.mediaType"]) + m.NotNil(artifact.ExtraAttrs["manifest.layers.mediaType"]) + m.regCli.AssertExpectations(m.T()) +} + +func (m *WASMProcessorTestSuite) TestAbstractAdditionForAnnotationFashion() { + // unknown addition + _, err := m.processor.AbstractAddition(nil, nil, "unknown_addition") + m.True(errors.IsErr(err, errors.BadRequestCode)) + + // build history + artifact := &artifact.Artifact{} + + manifest := schema2.Manifest{} + err = json.Unmarshal([]byte(annnotated_manifest), &manifest) + deserializedManifest, err := schema2.FromStruct(manifest) + m.Require().Nil(err) + m.regCli.On("PullManifest").Return(deserializedManifest, "", nil) + m.regCli.On("PullBlob").Return(0, ioutil.NopCloser(strings.NewReader(annnotated_config)), nil) + addition, err := m.processor.AbstractAddition(nil, artifact, AdditionTypeBuildHistory) + m.Require().Nil(err) + m.Equal("application/json; charset=utf-8", addition.ContentType) + m.Equal(`[{"created":"2022-03-02T09:02:41.011039932Z","created_by":"/bin/sh -c #(nop) COPY file:9fc00231cd29a2b8f76cfeaa9bc2355a7df54585b51d1f5dc98daf51557614b9 in / ","empty_layer":true},{"created":"2022-03-02T09:02:41.043350231Z","created_by":"/bin/sh -c #(nop) CMD [\"/sleep.wasm\"]"}]`, string(addition.Content)) +} + +func (m *WASMProcessorTestSuite) TestGetArtifactType() { + m.Assert().Equal(ArtifactTypeWASM, m.processor.GetArtifactType(nil, nil)) +} + +func (m *WASMProcessorTestSuite) TestListAdditionTypes() { + additions := m.processor.ListAdditionTypes(nil, nil) + m.EqualValues([]string{AdditionTypeBuildHistory}, additions) +} + +func TestManifestV2ProcessorTestSuite(t *testing.T) { + suite.Run(t, &WASMProcessorTestSuite{}) +} diff --git a/src/controller/icon/controller.go b/src/controller/icon/controller.go index 34ac9eea8..afa19a56f 100644 --- a/src/controller/icon/controller.go +++ b/src/controller/icon/controller.go @@ -60,6 +60,10 @@ var ( path: "./icons/nydus.png", resize: false, }, + icon.DigestOfIconWASM: { + path: "./icons/wasm.png", + resize: true, + }, icon.DigestOfIconDefault: { path: "./icons/default.png", resize: true, diff --git a/src/lib/icon/const.go b/src/lib/icon/const.go index 0c3f1c525..69c7b691c 100644 --- a/src/lib/icon/const.go +++ b/src/lib/icon/const.go @@ -6,6 +6,7 @@ const ( DigestOfIconChart = "sha256:61cf3a178ff0f75bf08a25d96b75cf7355dc197749a9f128ed3ef34b0df05518" DigestOfIconCNAB = "sha256:089bdda265c14d8686111402c8ad629e8177a1ceb7dcd0f7f39b6480f623b3bd" DigestOfIconDefault = "sha256:da834479c923584f4cbcdecc0dac61f32bef1d51e8aae598cf16bd154efab49f" + DigestOfIconWASM = "sha256:badd7693bcaf115be202748241dd0ea6ee3b0524bfab9ac22d1e1c43721afec6" // ToDo add the accessories images DigestOfIconAccDefault = ""