Fire event when create/delete resources (#11010)

1. Create/delete project
2. Create/delete repository
3. Push/pull/delete artifact
4. Create/delete tag

Signed-off-by: Wenkai Yin <yinw@vmware.com>
This commit is contained in:
Wenkai Yin(尹文开) 2020-03-11 14:39:01 +08:00 committed by GitHub
parent ba6ef67993
commit b02cab434f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 1013 additions and 72 deletions

View File

@ -19,6 +19,8 @@ import (
"context"
"errors"
"fmt"
"github.com/goharbor/harbor/src/api/event"
evt "github.com/goharbor/harbor/src/pkg/notifier/event"
"strings"
"time"
@ -139,6 +141,15 @@ func (c *controller) Ensure(ctx context.Context, repository, digest string, tags
return false, 0, err
}
}
// fire event
e := &event.PushArtifactEventMetadata{
Ctx: ctx,
Artifact: artifact,
}
if len(tags) > 0 {
e.Tag = tags[0]
}
evt.BuildAndPublish(e)
return created, artifact.ID, nil
}
@ -363,7 +374,18 @@ func (c *controller) deleteDeeply(ctx context.Context, id int64, isRoot bool) er
return err
}
// TODO fire delete artifact event
// only fire event for the root parent artifact
if isRoot {
var tags []string
for _, tag := range art.Tags {
tags = append(tags, tag.Name)
}
evt.BuildAndPublish(&event.DeleteArtifactEventMetadata{
Ctx: ctx,
Artifact: &art.Artifact,
Tags: tags,
})
}
return nil
}
@ -428,7 +450,6 @@ func (c *controller) copyDeeply(ctx context.Context, srcRepo, reference, dstRepo
if err != nil {
return 0, err
}
// TODO fire event
return id, nil
}

95
src/api/event/artifact.go Normal file
View File

@ -0,0 +1,95 @@
// 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 event
import (
"context"
"github.com/goharbor/harbor/src/common/security"
"github.com/goharbor/harbor/src/pkg/artifact"
"github.com/goharbor/harbor/src/pkg/notifier/event"
"time"
)
// PushArtifactEventMetadata is the metadata from which the push artifact event can be resolved
type PushArtifactEventMetadata struct {
Ctx context.Context
Artifact *artifact.Artifact
Tag string
}
// Resolve to the event from the metadata
func (p *PushArtifactEventMetadata) Resolve(event *event.Event) error {
data := &PushArtifactEvent{
Repository: p.Artifact.RepositoryName,
Artifact: p.Artifact,
Tag: p.Tag,
OccurAt: time.Now(),
}
ctx, exist := security.FromContext(p.Ctx)
if exist {
data.Operator = ctx.GetUsername()
}
event.Topic = TopicPushArtifact
event.Data = data
return nil
}
// PullArtifactEventMetadata is the metadata from which the pull artifact event can be resolved
type PullArtifactEventMetadata struct {
Ctx context.Context
Artifact *artifact.Artifact
Tag string
}
// Resolve to the event from the metadata
func (p *PullArtifactEventMetadata) Resolve(event *event.Event) error {
data := &PullArtifactEvent{
Repository: p.Artifact.RepositoryName,
Artifact: p.Artifact,
Tag: p.Tag,
OccurAt: time.Now(),
}
ctx, exist := security.FromContext(p.Ctx)
if exist {
data.Operator = ctx.GetUsername()
}
event.Topic = TopicPullArtifact
event.Data = data
return nil
}
// DeleteArtifactEventMetadata is the metadata from which the delete artifact event can be resolved
type DeleteArtifactEventMetadata struct {
Ctx context.Context
Artifact *artifact.Artifact
Tags []string
}
// Resolve to the event from the metadata
func (d *DeleteArtifactEventMetadata) Resolve(event *event.Event) error {
data := &DeleteArtifactEvent{
Repository: d.Artifact.RepositoryName,
Artifact: d.Artifact,
Tags: d.Tags,
OccurAt: time.Now(),
}
ctx, exist := security.FromContext(d.Ctx)
if exist {
data.Operator = ctx.GetUsername()
}
event.Topic = TopicDeleteArtifact
event.Data = data
return nil
}

View File

@ -0,0 +1,83 @@
// 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 event
import (
"context"
"github.com/goharbor/harbor/src/pkg/artifact"
"github.com/goharbor/harbor/src/pkg/notifier/event"
"github.com/stretchr/testify/suite"
"testing"
)
type artifactEventTestSuite struct {
suite.Suite
}
func (a *artifactEventTestSuite) TestResolveOfPushArtifactEventMetadata() {
e := &event.Event{}
metadata := &PushArtifactEventMetadata{
Ctx: context.Background(),
Artifact: &artifact.Artifact{ID: 1},
Tag: "latest",
}
err := metadata.Resolve(e)
a.Require().Nil(err)
a.Equal(TopicPushArtifact, e.Topic)
a.Require().NotNil(e.Data)
data, ok := e.Data.(*PushArtifactEvent)
a.Require().True(ok)
a.Equal(int64(1), data.Artifact.ID)
a.Equal("latest", data.Tag)
}
func (a *artifactEventTestSuite) TestResolveOfPullArtifactEventMetadata() {
e := &event.Event{}
metadata := &PullArtifactEventMetadata{
Ctx: context.Background(),
Artifact: &artifact.Artifact{ID: 1},
Tag: "latest",
}
err := metadata.Resolve(e)
a.Require().Nil(err)
a.Equal(TopicPullArtifact, e.Topic)
a.Require().NotNil(e.Data)
data, ok := e.Data.(*PullArtifactEvent)
a.Require().True(ok)
a.Equal(int64(1), data.Artifact.ID)
a.Equal("latest", data.Tag)
}
func (a *artifactEventTestSuite) TestResolveOfDeleteArtifactEventMetadata() {
e := &event.Event{}
metadata := &DeleteArtifactEventMetadata{
Ctx: context.Background(),
Artifact: &artifact.Artifact{ID: 1},
Tags: []string{"latest"},
}
err := metadata.Resolve(e)
a.Require().Nil(err)
a.Equal(TopicDeleteArtifact, e.Topic)
a.Require().NotNil(e.Data)
data, ok := e.Data.(*DeleteArtifactEvent)
a.Require().True(ok)
a.Equal(int64(1), data.Artifact.ID)
a.Require().Len(data.Tags, 1)
a.Equal("latest", data.Tags[0])
}
func TestArtifactEventTestSuite(t *testing.T) {
suite.Run(t, &artifactEventTestSuite{})
}

View File

@ -0,0 +1,149 @@
//
// 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 handler
import (
"github.com/goharbor/harbor/src/api/event"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/pkg/notifier"
"github.com/goharbor/harbor/src/pkg/project"
"github.com/goharbor/harbor/src/replication"
repevent "github.com/goharbor/harbor/src/replication/event"
"github.com/goharbor/harbor/src/replication/model"
"strconv"
)
func init() {
handler := &replicationHandler{
proMgr: project.Mgr,
}
notifier.Subscribe(event.TopicPushArtifact, handler)
notifier.Subscribe(event.TopicDeleteArtifact, handler)
notifier.Subscribe(event.TopicCreateTag, handler)
}
type replicationHandler struct {
proMgr project.Manager
}
func (r *replicationHandler) Handle(value interface{}) error {
pushArtEvent, ok := value.(*event.PushArtifactEvent)
if ok {
return r.handlePushArtifact(pushArtEvent)
}
deleteArtEvent, ok := value.(*event.DeleteArtifactEvent)
if ok {
return r.handleDeleteArtifact(deleteArtEvent)
}
createTagEvent, ok := value.(*event.CreateTagEvent)
if ok {
return r.handleCreateTag(createTagEvent)
}
return nil
}
func (r *replicationHandler) IsStateful() bool {
return false
}
// TODO handle create tag
func (r *replicationHandler) handlePushArtifact(event *event.PushArtifactEvent) error {
art := event.Artifact
public := false
project, err := r.proMgr.Get(art.ProjectID)
if err == nil && project != nil {
public = project.IsPublic()
} else {
log.Error(err)
}
project.IsPublic()
e := &repevent.Event{
Type: repevent.EventTypeArtifactPush,
Resource: &model.Resource{
Type: model.ResourceTypeArtifact,
Metadata: &model.ResourceMetadata{
Repository: &model.Repository{
Name: event.Repository,
Metadata: map[string]interface{}{
"public": strconv.FormatBool(public),
},
},
Artifacts: []*model.Artifact{
{
Type: art.Type,
Digest: art.Digest,
Tags: []string{event.Tag},
}},
},
},
}
return replication.EventHandler.Handle(e)
}
func (r *replicationHandler) handleDeleteArtifact(event *event.DeleteArtifactEvent) error {
art := event.Artifact
e := &repevent.Event{
Type: repevent.EventTypeArtifactDelete,
Resource: &model.Resource{
Type: model.ResourceTypeArtifact,
Metadata: &model.ResourceMetadata{
Repository: &model.Repository{
Name: event.Repository,
},
Artifacts: []*model.Artifact{
{
Type: art.Type,
Digest: art.Digest,
Tags: event.Tags,
}},
},
Deleted: true,
},
}
return replication.EventHandler.Handle(e)
}
func (r *replicationHandler) handleCreateTag(event *event.CreateTagEvent) error {
art := event.AttachedArtifact
public := false
project, err := r.proMgr.Get(art.ProjectID)
if err == nil && project != nil {
public = project.IsPublic()
} else {
log.Error(err)
}
project.IsPublic()
e := &repevent.Event{
Type: repevent.EventTypeArtifactPush,
Resource: &model.Resource{
Type: model.ResourceTypeArtifact,
Metadata: &model.ResourceMetadata{
Repository: &model.Repository{
Name: event.Repository,
Metadata: map[string]interface{}{
"public": strconv.FormatBool(public),
},
},
Artifacts: []*model.Artifact{
{
Type: art.Type,
Digest: art.Digest,
Tags: []string{event.Tag},
}},
},
},
}
return replication.EventHandler.Handle(e)
}

54
src/api/event/project.go Normal file
View File

@ -0,0 +1,54 @@
// 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 event
import (
"github.com/goharbor/harbor/src/pkg/notifier/event"
"time"
)
// CreateProjectEventMetadata is the metadata from which the create project event can be resolved
type CreateProjectEventMetadata struct {
Project string
Operator string
}
// Resolve to the event from the metadata
func (c *CreateProjectEventMetadata) Resolve(event *event.Event) error {
event.Topic = TopicCreateProject
event.Data = &CreateProjectEvent{
Project: c.Project,
Operator: c.Operator,
OccurAt: time.Now(),
}
return nil
}
// DeleteProjectEventMetadata is the metadata from which the delete project event can be resolved
type DeleteProjectEventMetadata struct {
Project string
Operator string
}
// Resolve to the event from the metadata
func (d *DeleteProjectEventMetadata) Resolve(event *event.Event) error {
event.Topic = TopicDeleteProject
event.Data = &DeleteProjectEvent{
Project: d.Project,
Operator: d.Operator,
OccurAt: time.Now(),
}
return nil
}

View File

@ -0,0 +1,61 @@
// 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 event
import (
"github.com/goharbor/harbor/src/pkg/notifier/event"
"github.com/stretchr/testify/suite"
"testing"
)
type projectEventTestSuite struct {
suite.Suite
}
func (p *projectEventTestSuite) TestResolveOfCreateProjectEventMetadata() {
e := &event.Event{}
metadata := &CreateProjectEventMetadata{
Project: "library",
Operator: "admin",
}
err := metadata.Resolve(e)
p.Require().Nil(err)
p.Equal(TopicCreateProject, e.Topic)
p.Require().NotNil(e.Data)
data, ok := e.Data.(*CreateProjectEvent)
p.Require().True(ok)
p.Equal("library", data.Project)
p.Equal("admin", data.Operator)
}
func (p *projectEventTestSuite) TestResolveOfDeleteProjectEventMetadata() {
e := &event.Event{}
metadata := &DeleteProjectEventMetadata{
Project: "library",
Operator: "admin",
}
err := metadata.Resolve(e)
p.Require().Nil(err)
p.Equal(TopicDeleteProject, e.Topic)
p.Require().NotNil(e.Data)
data, ok := e.Data.(*DeleteProjectEvent)
p.Require().True(ok)
p.Equal("library", data.Project)
p.Equal("admin", data.Operator)
}
func TestProjectEventTestSuite(t *testing.T) {
suite.Run(t, &projectEventTestSuite{})
}

View File

@ -0,0 +1,43 @@
// 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 event
import (
"context"
"github.com/goharbor/harbor/src/common/security"
"github.com/goharbor/harbor/src/pkg/notifier/event"
"time"
)
// DeleteRepositoryEventMetadata is the metadata from which the delete repository event can be resolved
type DeleteRepositoryEventMetadata struct {
Ctx context.Context
Repository string
}
// Resolve to the event from the metadata
func (d *DeleteRepositoryEventMetadata) Resolve(event *event.Event) error {
data := &DeleteRepositoryEvent{
Repository: d.Repository,
OccurAt: time.Now(),
}
cx, exist := security.FromContext(d.Ctx)
if exist {
data.Operator = cx.GetUsername()
}
event.Topic = TopicDeleteRepository
event.Data = data
return nil
}

View 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 event
import (
"context"
"github.com/goharbor/harbor/src/pkg/notifier/event"
"github.com/stretchr/testify/suite"
"testing"
)
type repositoryEventTestSuite struct {
suite.Suite
}
func (r *repositoryEventTestSuite) TestResolveOfDeleteRepositoryEventMetadata() {
e := &event.Event{}
metadata := &DeleteRepositoryEventMetadata{
Ctx: context.Background(),
Repository: "library/hello-world",
}
err := metadata.Resolve(e)
r.Require().Nil(err)
r.Equal(TopicDeleteRepository, e.Topic)
r.Require().NotNil(e.Data)
data, ok := e.Data.(*DeleteRepositoryEvent)
r.Require().True(ok)
r.Equal("library/hello-world", data.Repository)
}
func TestRepositoryEventTestSuite(t *testing.T) {
suite.Run(t, &repositoryEventTestSuite{})
}

71
src/api/event/tag.go Normal file
View File

@ -0,0 +1,71 @@
// 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 event
import (
"context"
"github.com/goharbor/harbor/src/common/security"
"github.com/goharbor/harbor/src/pkg/artifact"
"github.com/goharbor/harbor/src/pkg/notifier/event"
"time"
)
// CreateTagEventMetadata is the metadata from which the create tag event can be resolved
type CreateTagEventMetadata struct {
Ctx context.Context
Tag string
AttachedArtifact *artifact.Artifact
}
// Resolve to the event from the metadata
func (c *CreateTagEventMetadata) Resolve(event *event.Event) error {
data := &CreateTagEvent{
Repository: c.AttachedArtifact.RepositoryName,
Tag: c.Tag,
AttachedArtifact: c.AttachedArtifact,
OccurAt: time.Now(),
}
cx, exist := security.FromContext(c.Ctx)
if exist {
data.Operator = cx.GetUsername()
}
event.Topic = TopicCreateTag
event.Data = data
return nil
}
// DeleteTagEventMetadata is the metadata from which the delete tag event can be resolved
type DeleteTagEventMetadata struct {
Ctx context.Context
Tag string
AttachedArtifact *artifact.Artifact
}
// Resolve to the event from the metadata
func (d *DeleteTagEventMetadata) Resolve(event *event.Event) error {
data := &DeleteTagEvent{
Repository: d.AttachedArtifact.RepositoryName,
Tag: d.Tag,
AttachedArtifact: d.AttachedArtifact,
OccurAt: time.Now(),
}
ctx, exist := security.FromContext(d.Ctx)
if exist {
data.Operator = ctx.GetUsername()
}
event.Topic = TopicDeleteTag
event.Data = data
return nil
}

65
src/api/event/tag_test.go Normal file
View File

@ -0,0 +1,65 @@
// 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 event
import (
"context"
"github.com/goharbor/harbor/src/pkg/artifact"
"github.com/goharbor/harbor/src/pkg/notifier/event"
"github.com/stretchr/testify/suite"
"testing"
)
type tagEventTestSuite struct {
suite.Suite
}
func (t *tagEventTestSuite) TestResolveOfCreateTagEventMetadata() {
e := &event.Event{}
metadata := &CreateTagEventMetadata{
Ctx: context.Background(),
Tag: "latest",
AttachedArtifact: &artifact.Artifact{ID: 1},
}
err := metadata.Resolve(e)
t.Require().Nil(err)
t.Equal(TopicCreateTag, e.Topic)
t.Require().NotNil(e.Data)
data, ok := e.Data.(*CreateTagEvent)
t.Require().True(ok)
t.Equal(int64(1), data.AttachedArtifact.ID)
t.Equal("latest", data.Tag)
}
func (t *tagEventTestSuite) TestResolveOfDeleteTagEventMetadata() {
e := &event.Event{}
metadata := &DeleteTagEventMetadata{
Ctx: context.Background(),
Tag: "latest",
AttachedArtifact: &artifact.Artifact{ID: 1},
}
err := metadata.Resolve(e)
t.Require().Nil(err)
t.Equal(TopicDeleteTag, e.Topic)
t.Require().NotNil(e.Data)
data, ok := e.Data.(*DeleteTagEvent)
t.Require().True(ok)
t.Equal(int64(1), data.AttachedArtifact.ID)
t.Equal("latest", data.Tag)
}
func TestTagEventTestSuite(t *testing.T) {
suite.Run(t, &tagEventTestSuite{})
}

100
src/api/event/topic.go Normal file
View File

@ -0,0 +1,100 @@
// 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 event
import (
"github.com/goharbor/harbor/src/pkg/artifact"
"time"
)
// the event consumers can refer to this file to find all topics and the corresponding event structures
// const definition
const (
TopicCreateProject = "CREATE_PROJECT"
TopicDeleteProject = "DELETE_PROJECT"
TopicDeleteRepository = "DELETE_REPOSITORY"
TopicPushArtifact = "PUSH_ARTIFACT"
TopicPullArtifact = "PULL_ARTIFACT"
TopicDeleteArtifact = "DELETE_ARTIFACT"
TopicCreateTag = "CREATE_TAG"
TopicDeleteTag = "DELETE_TAG"
)
// CreateProjectEvent is the creating project event
type CreateProjectEvent struct {
Project string
Operator string
OccurAt time.Time
}
// DeleteProjectEvent is the deleting project event
type DeleteProjectEvent struct {
Project string
Operator string
OccurAt time.Time
}
// DeleteRepositoryEvent is the deleting repository event
type DeleteRepositoryEvent struct {
Repository string
Operator string
OccurAt time.Time
}
// PushArtifactEvent is the pushing artifact event
type PushArtifactEvent struct {
Repository string
Artifact *artifact.Artifact
Tag string // when the artifact is pushed by digest, the tag here will be null
Operator string
OccurAt time.Time
}
// PullArtifactEvent is the pulling artifact event
type PullArtifactEvent struct {
Repository string
Artifact *artifact.Artifact
Tag string // when the artifact is pulled by digest, the tag here will be null
Operator string
OccurAt time.Time
}
// DeleteArtifactEvent is the deleting artifact event
type DeleteArtifactEvent struct {
Repository string
Artifact *artifact.Artifact
Tags []string // all the tags that attached to the deleted artifact
Operator string
OccurAt time.Time
}
// CreateTagEvent is the creating tag event
type CreateTagEvent struct {
Repository string
Tag string
AttachedArtifact *artifact.Artifact
Operator string
OccurAt time.Time
}
// DeleteTagEvent is the deleting tag event
type DeleteTagEvent struct {
Repository string
Tag string
AttachedArtifact *artifact.Artifact
Operator string
OccurAt time.Time
}

View File

@ -16,13 +16,7 @@ package api
import (
"fmt"
"net/http"
"regexp"
"strconv"
"strings"
"sync"
"time"
"github.com/goharbor/harbor/src/api/event"
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/dao/project"
@ -33,9 +27,15 @@ import (
errutil "github.com/goharbor/harbor/src/common/utils/error"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/core/config"
evt "github.com/goharbor/harbor/src/pkg/notifier/event"
"github.com/goharbor/harbor/src/pkg/scan/vuln"
"github.com/goharbor/harbor/src/pkg/types"
"github.com/pkg/errors"
"net/http"
"regexp"
"strconv"
"strings"
"sync"
)
type deletableResp struct {
@ -211,19 +211,11 @@ func (p *ProjectAPI) Post() {
}
}
go func() {
if err = dao.AddAccessLog(
models.AccessLog{
Username: p.SecurityCtx.GetUsername(),
ProjectID: projectID,
RepoName: pro.Name + "/",
RepoTag: "N/A",
Operation: "create",
OpTime: time.Now(),
}); err != nil {
log.Errorf("failed to add access log: %v", err)
}
}()
// fire event
evt.BuildAndPublish(&event.CreateProjectEventMetadata{
Project: pro.Name,
Operator: owner,
})
p.Redirect(http.StatusCreated, strconv.FormatInt(projectID, 10))
}
@ -301,18 +293,11 @@ func (p *ProjectAPI) Delete() {
return
}
go func() {
if err := dao.AddAccessLog(models.AccessLog{
Username: p.SecurityCtx.GetUsername(),
ProjectID: p.project.ProjectID,
RepoName: p.project.Name + "/",
RepoTag: "N/A",
Operation: "delete",
OpTime: time.Now(),
}); err != nil {
log.Errorf("failed to add access log: %v", err)
}
}()
// fire event
evt.BuildAndPublish(&event.DeleteProjectEventMetadata{
Project: p.project.Name,
Operator: p.SecurityCtx.GetUsername(),
})
}
// Deletable ...

View File

@ -26,6 +26,7 @@ import (
"github.com/astaxie/beego"
_ "github.com/astaxie/beego/session/redis"
_ "github.com/goharbor/harbor/src/api/event/handler"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/job"
"github.com/goharbor/harbor/src/common/models"

View File

@ -0,0 +1,53 @@
// 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 internal
import "net/http"
// NewResponseRecorder creates a response recorder
func NewResponseRecorder(w http.ResponseWriter) *ResponseRecorder {
recorder := &ResponseRecorder{}
recorder.ResponseWriter = w
return recorder
}
// ResponseRecorder is a wrapper for the http.ResponseWriter to record the response status code
type ResponseRecorder struct {
StatusCode int
wroteHeader bool
http.ResponseWriter
}
// Write records the status code before writing data to the underlying writer
func (r *ResponseRecorder) Write(data []byte) (int, error) {
if !r.wroteHeader {
r.WriteHeader(http.StatusOK)
}
return r.ResponseWriter.Write(data)
}
// WriteHeader records the status code before writing the code to the underlying writer
func (r *ResponseRecorder) WriteHeader(statusCode int) {
if !r.wroteHeader {
r.wroteHeader = true
r.StatusCode = statusCode
r.ResponseWriter.WriteHeader(statusCode)
}
}
// Success checks whether the status code is >= 200 & <= 399
func (r *ResponseRecorder) Success() bool {
return r.StatusCode >= http.StatusOK && r.StatusCode < http.StatusBadRequest
}

View File

@ -0,0 +1,56 @@
// 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 internal
import (
"github.com/stretchr/testify/suite"
"net/http"
"net/http/httptest"
"testing"
)
type responseRecorderTestSuite struct {
suite.Suite
recorder *ResponseRecorder
}
func (r *responseRecorderTestSuite) SetupTest() {
r.recorder = NewResponseRecorder(httptest.NewRecorder())
}
func (r *responseRecorderTestSuite) TestWriteHeader() {
// write once
r.recorder.WriteHeader(http.StatusInternalServerError)
r.Equal(http.StatusInternalServerError, r.recorder.StatusCode)
// write again
r.recorder.WriteHeader(http.StatusNotFound)
r.Equal(http.StatusInternalServerError, r.recorder.StatusCode)
}
func (r *responseRecorderTestSuite) TestWrite() {
_, err := r.recorder.Write([]byte{'a'})
r.Require().Nil(err)
r.Equal(http.StatusOK, r.recorder.StatusCode)
}
func (r *responseRecorderTestSuite) TestSuccess() {
r.recorder.WriteHeader(http.StatusInternalServerError)
r.False(r.recorder.Success())
}
func TestResponseRecorder(t *testing.T) {
suite.Run(t, &responseRecorderTestSuite{})
}

View File

@ -39,26 +39,6 @@ func (e *Event) WithTopicEvent(topicEvent TopicEvent) *Event {
return e
}
// Build an event by metadata
func (e *Event) Build(metadata ...Metadata) error {
for _, md := range metadata {
if err := md.Resolve(e); err != nil {
log.Debugf("failed to resolve event metadata: %v", md)
return errors.Wrap(err, "failed to resolve event metadata")
}
}
return nil
}
// Publish an event
func (e *Event) Publish() error {
if err := notifier.Publish(e.Topic, e.Data); err != nil {
log.Debugf("failed to publish topic %s with event: %v", e.Topic, e.Data)
return errors.Wrap(err, "failed to publish event")
}
return nil
}
// Metadata is the event raw data to be processed
type Metadata interface {
Resolve(event *Event) error
@ -322,3 +302,40 @@ func (h *HookMetaData) Resolve(evt *Event) error {
evt.Data = data
return nil
}
// Build an event by metadata
func (e *Event) Build(metadata ...Metadata) error {
for _, md := range metadata {
if err := md.Resolve(e); err != nil {
log.Debugf("failed to resolve event metadata: %v", md)
return errors.Wrap(err, "failed to resolve event metadata")
}
}
return nil
}
// Publish an event
func (e *Event) Publish() error {
if err := notifier.Publish(e.Topic, e.Data); err != nil {
log.Debugf("failed to publish topic %s with event: %v", e.Topic, e.Data)
return errors.Wrap(err, "failed to publish event")
}
return nil
}
// BuildAndPublish builds the event according to the metadata and publish the event
// The process is done in a separated goroutine
func BuildAndPublish(metadata ...Metadata) {
go func() {
event := &Event{}
if err := event.Build(metadata...); err != nil {
log.Errorf("failed to build the event from metadata: %v", err)
return
}
if err := event.Publish(); err != nil {
log.Errorf("failed to publish the event %s: %v", event.Topic, err)
return
}
log.Debugf("event %s published", event.Topic)
}()
}

View File

@ -62,7 +62,7 @@ func (a *adapter) FetchImages(filters []*model.Filter) ([]*model.Resource, error
}
rawResources[index] = &model.Resource{
Type: model.ResourceTypeImage,
Type: model.ResourceTypeArtifact,
Registry: a.registry,
Metadata: &model.ResourceMetadata{
Repository: &model.Repository{

View File

@ -83,7 +83,7 @@ func TestFetchImages(t *testing.T) {
resources, err := adapter.FetchImages(nil)
require.Nil(t, err)
assert.Equal(t, 1, len(resources))
assert.Equal(t, model.ResourceTypeImage, resources[0].Type)
assert.Equal(t, model.ResourceTypeArtifact, resources[0].Type)
assert.Equal(t, "library/hello-world", resources[0].Metadata.Repository.Name)
assert.Equal(t, 2, len(resources[0].Metadata.Artifacts))
assert.Equal(t, "1.0", resources[0].Metadata.Artifacts[0].Tags[0])
@ -102,7 +102,7 @@ func TestFetchImages(t *testing.T) {
resources, err = adapter.FetchImages(filters)
require.Nil(t, err)
assert.Equal(t, 1, len(resources))
assert.Equal(t, model.ResourceTypeImage, resources[0].Type)
assert.Equal(t, model.ResourceTypeArtifact, resources[0].Type)
assert.Equal(t, "library/hello-world", resources[0].Metadata.Repository.Name)
assert.Equal(t, 1, len(resources[0].Metadata.Artifacts))
assert.Equal(t, "1.0", resources[0].Metadata.Artifacts[0].Tags[0])

View File

@ -18,10 +18,10 @@ import "github.com/goharbor/harbor/src/replication/model"
// const definitions
const (
EventTypeImagePush = "image_push"
EventTypeImageDelete = "image_delete"
EventTypeChartUpload = "chart_upload"
EventTypeChartDelete = "chart_delete"
EventTypeArtifactPush = "artifact_push"
EventTypeArtifactDelete = "artifact_delete"
EventTypeChartUpload = "chart_upload"
EventTypeChartDelete = "chart_delete"
)
// Event is the model that defines the image/chart pull/push event

View File

@ -57,8 +57,8 @@ func (h *handler) Handle(event *Event) error {
var policies []*model.Policy
var err error
switch event.Type {
case EventTypeImagePush, EventTypeChartUpload,
EventTypeImageDelete, EventTypeChartDelete:
case EventTypeArtifactPush, EventTypeChartUpload,
EventTypeArtifactDelete, EventTypeChartDelete:
policies, err = h.getRelatedPolicies(event.Resource)
default:
return fmt.Errorf("unsupported event type %s", event.Type)

View File

@ -253,7 +253,7 @@ func TestHandle(t *testing.T) {
Vtags: []string{},
},
},
Type: EventTypeImagePush,
Type: EventTypeArtifactPush,
})
require.NotNil(t, err)
@ -285,7 +285,7 @@ func TestHandle(t *testing.T) {
},
},
},
Type: EventTypeImagePush,
Type: EventTypeArtifactPush,
})
require.Nil(t, err)
@ -303,7 +303,7 @@ func TestHandle(t *testing.T) {
},
},
},
Type: EventTypeImageDelete,
Type: EventTypeArtifactDelete,
})
require.Nil(t, err)
}

View File

@ -33,6 +33,9 @@ func init() {
if err := trans.RegisterFactory(model.ResourceTypeImage, factory); err != nil {
log.Errorf("failed to register transfer factory: %v", err)
}
if err := trans.RegisterFactory(model.ResourceTypeArtifact, factory); err != nil {
log.Errorf("failed to register transfer factory: %v", err)
}
}
type repository struct {

View File

@ -16,10 +16,12 @@ package registry
import (
"github.com/goharbor/harbor/src/api/artifact"
"github.com/goharbor/harbor/src/api/event"
"github.com/goharbor/harbor/src/api/repository"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/internal"
ierror "github.com/goharbor/harbor/src/internal/error"
evt "github.com/goharbor/harbor/src/pkg/notifier/event"
serror "github.com/goharbor/harbor/src/server/error"
"github.com/goharbor/harbor/src/server/router"
"github.com/opencontainers/go-digest"
@ -43,9 +45,23 @@ func getManifest(w http.ResponseWriter, req *http.Request) {
req.URL.Path = strings.TrimSuffix(req.URL.Path, reference) + artifact.Digest
req.URL.RawPath = req.URL.EscapedPath()
}
proxy.ServeHTTP(w, req)
// TODO fire event(only for GET method), add access log in the event handler
recorder := internal.NewResponseRecorder(w)
proxy.ServeHTTP(recorder, req)
// fire event
if recorder.Success() {
// TODO don't fire event for the pulling from replication
e := &event.PullArtifactEventMetadata{
Ctx: req.Context(),
Artifact: &artifact.Artifact,
}
// TODO provide a util function to determine whether the reference is tag or not
// the reference is tag
if _, err = digest.Parse(reference); err != nil {
e.Tag = reference
}
evt.BuildAndPublish(e)
}
}
// just delete the artifact from database
@ -74,8 +90,6 @@ func deleteManifest(w http.ResponseWriter, req *http.Request) {
return
}
w.WriteHeader(http.StatusAccepted)
// TODO fire event, add access log in the event handler
}
func putManifest(w http.ResponseWriter, req *http.Request) {
@ -121,6 +135,4 @@ func putManifest(w http.ResponseWriter, req *http.Request) {
if _, err := buffer.Flush(); err != nil {
log.Errorf("failed to flush: %v", err)
}
// TODO fire event, add access log in the event handler
}

View File

@ -17,6 +17,8 @@ package handler
import (
"context"
"fmt"
"github.com/goharbor/harbor/src/api/event"
evt "github.com/goharbor/harbor/src/pkg/notifier/event"
"net/http"
"strings"
"time"
@ -238,6 +240,14 @@ func (a *artifactAPI) CreateTag(ctx context.Context, params operation.CreateTagP
if _, err = a.tagCtl.Create(ctx, tag); err != nil {
return a.SendError(ctx, err)
}
// fire event
evt.BuildAndPublish(&event.CreateTagEventMetadata{
Ctx: ctx,
Tag: tag.Name,
AttachedArtifact: &art.Artifact,
})
// TODO as we provide no API for get the single tag, ignore setting the location header here
return operation.NewCreateTagCreated()
}
@ -269,6 +279,14 @@ func (a *artifactAPI) DeleteTag(ctx context.Context, params operation.DeleteTagP
if err = a.tagCtl.Delete(ctx, id); err != nil {
return a.SendError(ctx, err)
}
// fire event
evt.BuildAndPublish(&event.DeleteTagEventMetadata{
Ctx: ctx,
Tag: params.TagName,
AttachedArtifact: &artifact.Artifact,
})
return operation.NewDeleteTagOK()
}

View File

@ -20,11 +20,13 @@ import (
"github.com/go-openapi/runtime/middleware"
"github.com/goharbor/harbor/src/api/artifact"
"github.com/goharbor/harbor/src/api/event"
"github.com/goharbor/harbor/src/api/project"
"github.com/goharbor/harbor/src/api/repository"
cmodels "github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/common/utils/log"
evt "github.com/goharbor/harbor/src/pkg/notifier/event"
"github.com/goharbor/harbor/src/pkg/q"
"github.com/goharbor/harbor/src/server/v2.0/models"
operation "github.com/goharbor/harbor/src/server/v2.0/restapi/operations/repository"
@ -141,5 +143,12 @@ func (r *repositoryAPI) DeleteRepository(ctx context.Context, params operation.D
if err := r.repoCtl.Delete(ctx, repository.RepositoryID); err != nil {
return r.SendError(ctx, err)
}
// fire event
evt.BuildAndPublish(&event.DeleteRepositoryEventMetadata{
Ctx: ctx,
Repository: repository.Name,
})
return operation.NewDeleteRepositoryOK()
}