diff --git a/src/api/artifact/controller.go b/src/api/artifact/controller.go index 15bd524dc..0daa55943 100644 --- a/src/api/artifact/controller.go +++ b/src/api/artifact/controller.go @@ -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 } diff --git a/src/api/event/artifact.go b/src/api/event/artifact.go new file mode 100644 index 000000000..e5c4b2525 --- /dev/null +++ b/src/api/event/artifact.go @@ -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 +} diff --git a/src/api/event/artifact_test.go b/src/api/event/artifact_test.go new file mode 100644 index 000000000..a6cd231de --- /dev/null +++ b/src/api/event/artifact_test.go @@ -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{}) +} diff --git a/src/api/event/handler/replication.go b/src/api/event/handler/replication.go new file mode 100644 index 000000000..5bd96898b --- /dev/null +++ b/src/api/event/handler/replication.go @@ -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) +} diff --git a/src/api/event/project.go b/src/api/event/project.go new file mode 100644 index 000000000..2493b00b1 --- /dev/null +++ b/src/api/event/project.go @@ -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 +} diff --git a/src/api/event/project_test.go b/src/api/event/project_test.go new file mode 100644 index 000000000..cd69fa167 --- /dev/null +++ b/src/api/event/project_test.go @@ -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{}) +} diff --git a/src/api/event/repository.go b/src/api/event/repository.go new file mode 100644 index 000000000..a5882b710 --- /dev/null +++ b/src/api/event/repository.go @@ -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 +} diff --git a/src/api/event/repository_test.go b/src/api/event/repository_test.go new file mode 100644 index 000000000..e4cc427a1 --- /dev/null +++ b/src/api/event/repository_test.go @@ -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{}) +} diff --git a/src/api/event/tag.go b/src/api/event/tag.go new file mode 100644 index 000000000..d84bbbe58 --- /dev/null +++ b/src/api/event/tag.go @@ -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 +} diff --git a/src/api/event/tag_test.go b/src/api/event/tag_test.go new file mode 100644 index 000000000..97220cfea --- /dev/null +++ b/src/api/event/tag_test.go @@ -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{}) +} diff --git a/src/api/event/topic.go b/src/api/event/topic.go new file mode 100644 index 000000000..ed86ad77d --- /dev/null +++ b/src/api/event/topic.go @@ -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 +} diff --git a/src/core/api/project.go b/src/core/api/project.go index 6ed8ee4cc..f8b2e25aa 100644 --- a/src/core/api/project.go +++ b/src/core/api/project.go @@ -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 ... diff --git a/src/core/main.go b/src/core/main.go index cd90737e2..5bb23d64e 100755 --- a/src/core/main.go +++ b/src/core/main.go @@ -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" diff --git a/src/internal/response_recorder.go b/src/internal/response_recorder.go new file mode 100644 index 000000000..6378d5a41 --- /dev/null +++ b/src/internal/response_recorder.go @@ -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 +} diff --git a/src/internal/response_recorder_test.go b/src/internal/response_recorder_test.go new file mode 100644 index 000000000..5ed0cea37 --- /dev/null +++ b/src/internal/response_recorder_test.go @@ -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{}) +} diff --git a/src/pkg/notifier/event/event.go b/src/pkg/notifier/event/event.go index 09ed4473e..604b9fad7 100644 --- a/src/pkg/notifier/event/event.go +++ b/src/pkg/notifier/event/event.go @@ -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) + }() +} diff --git a/src/replication/adapter/harbor/image_registry.go b/src/replication/adapter/harbor/image_registry.go index fa91b3840..1a41c31c7 100644 --- a/src/replication/adapter/harbor/image_registry.go +++ b/src/replication/adapter/harbor/image_registry.go @@ -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{ diff --git a/src/replication/adapter/harbor/image_registry_test.go b/src/replication/adapter/harbor/image_registry_test.go index b58eefc78..38c3f56b6 100644 --- a/src/replication/adapter/harbor/image_registry_test.go +++ b/src/replication/adapter/harbor/image_registry_test.go @@ -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]) diff --git a/src/replication/event/event.go b/src/replication/event/event.go index 301897b94..a4b97b564 100644 --- a/src/replication/event/event.go +++ b/src/replication/event/event.go @@ -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 diff --git a/src/replication/event/handler.go b/src/replication/event/handler.go index e82430913..a5efef39f 100644 --- a/src/replication/event/handler.go +++ b/src/replication/event/handler.go @@ -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) diff --git a/src/replication/event/handler_test.go b/src/replication/event/handler_test.go index 52545b0aa..9a604ef65 100644 --- a/src/replication/event/handler_test.go +++ b/src/replication/event/handler_test.go @@ -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) } diff --git a/src/replication/transfer/image/transfer.go b/src/replication/transfer/image/transfer.go index e139c7e8a..e3dbf0b39 100644 --- a/src/replication/transfer/image/transfer.go +++ b/src/replication/transfer/image/transfer.go @@ -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 { diff --git a/src/server/registry/manifest.go b/src/server/registry/manifest.go index e5e1e176b..4896eafa0 100644 --- a/src/server/registry/manifest.go +++ b/src/server/registry/manifest.go @@ -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 } diff --git a/src/server/v2.0/handler/artifact.go b/src/server/v2.0/handler/artifact.go index c15687881..f4e1f26c0 100644 --- a/src/server/v2.0/handler/artifact.go +++ b/src/server/v2.0/handler/artifact.go @@ -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() } diff --git a/src/server/v2.0/handler/repository.go b/src/server/v2.0/handler/repository.go index 36543f514..2aec0bf14 100644 --- a/src/server/v2.0/handler/repository.go +++ b/src/server/v2.0/handler/repository.go @@ -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() }