refactor notification (#14406)

* Refactor webhook

refactor notification to new programming model

Signed-off-by: wang yan <wangyan@vmware.com>
This commit is contained in:
Wang Yan 2021-03-22 17:27:23 +08:00 committed by GitHub
parent b2f0a1f0f5
commit 9ef50ed430
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
73 changed files with 2739 additions and 3059 deletions

View File

@ -1473,300 +1473,6 @@ paths:
description: User does not have permission to call this API.
'500':
description: Unexpected internal errors.
'/projects/{project_id}/webhook/policies':
get:
summary: List project webhook policies.
description: |
This endpoint returns webhook policies of a project.
parameters:
- name: project_id
in: path
type: integer
format: int64
required: true
description: Relevant project ID.
tags:
- Products
responses:
'200':
description: List project webhook policies successfully.
schema:
type: array
items:
$ref: '#/definitions/WebhookPolicy'
'400':
description: Illegal format of provided ID value.
'401':
description: User need to log in first.
'403':
description: User have no permission to list webhook policies of the project.
'500':
description: Unexpected internal errors.
post:
summary: Create project webhook policy.
description: |
This endpoint create a webhook policy if the project does not have one.
parameters:
- name: project_id
in: path
type: integer
format: int64
required: true
description: Relevant project ID
- name: policy
in: body
description: Properties "targets" and "event_types" needed.
required: true
schema:
$ref: '#/definitions/WebhookPolicy'
tags:
- Products
responses:
'201':
description: Project webhook policy create successfully.
headers:
Location:
type: string
description: The URL of the created resource
'400':
description: Illegal format of provided ID value.
'401':
description: User need to log in first.
'403':
description: User have no permission to create webhook policy of the project.
'500':
description: Unexpected internal errors.
'/projects/{project_id}/webhook/policies/{policy_id}':
get:
summary: Get project webhook policy
description: |
This endpoint returns specified webhook policy of a project.
parameters:
- name: project_id
in: path
description: Relevant project ID.
required: true
type: integer
format: int64
- name: policy_id
in: path
description: The id of webhook policy.
required: true
type: integer
format: int64
tags:
- Products
responses:
'200':
description: Get webhook policy successfully.
schema:
$ref: '#/definitions/WebhookPolicy'
'400':
description: Illegal format of provided ID value.
'401':
description: User need to log in first.
'403':
description: User have no permission to get webhook policy of the project.
'404':
description: Webhook policy ID does not exist.
'500':
description: Internal server errors.
put:
summary: Update webhook policy of a project.
description: |
This endpoint is aimed to update the webhook policy of a project.
parameters:
- name: project_id
in: path
description: Relevant project ID.
required: true
type: integer
format: int64
- name: policy_id
in: path
description: The id of webhook policy.
required: true
type: integer
format: int64
- name: policy
in: body
description: All properties needed except "id", "project_id", "creation_time", "update_time".
required: true
schema:
$ref: '#/definitions/WebhookPolicy'
tags:
- Products
responses:
'200':
description: Update webhook policy successfully.
'400':
description: Illegal format of provided ID value.
'401':
description: User need to log in first.
'403':
description: User have no permission to update webhook policy of the project.
'404':
description: Webhook policy ID does not exist.
'500':
description: Internal server errors.
delete:
summary: Delete webhook policy of a project
description: |
This endpoint is aimed to delete webhookpolicy of a project.
parameters:
- name: project_id
in: path
description: Relevant project ID.
required: true
type: integer
format: int64
- name: policy_id
in: path
description: The id of webhook policy.
required: true
type: integer
format: int64
tags:
- Products
responses:
'200':
description: Delete webhook policy successfully.
'400':
description: Illegal format of provided ID value.
'401':
description: User need to log in first.
'403':
description: User have no permission to delete webhook policy of the project.
'404':
description: Webhook policy ID does not exist.
'500':
description: Internal server errors.
'/projects/{project_id}/webhook/policies/test':
post:
summary: Test project webhook connection
description: |
This endpoint tests webhook connection of a project.
parameters:
- name: project_id
in: path
description: Relevant project ID.
required: true
type: integer
format: int64
- name: policy
in: body
description: Only property "targets" needed.
required: true
schema:
$ref: '#/definitions/WebhookPolicy'
tags:
- Products
responses:
'200':
description: Test webhook connection successfully.
'400':
description: Illegal format of provided ID value.
'401':
description: User need to log in first.
'403':
description: User have no permission to get webhook policy of the project.
'500':
description: Internal server errors.
'/projects/{project_id}/webhook/lasttrigger':
get:
summary: Get project webhook policy last trigger info
description: |
This endpoint returns last trigger information of project webhook policy.
parameters:
- name: project_id
in: path
description: Relevant project ID.
required: true
type: integer
format: int64
tags:
- Products
responses:
'200':
description: Test webhook connection successfully.
schema:
type: array
items:
$ref: '#/definitions/WebhookLastTrigger'
'400':
description: Illegal format of provided ID value.
'401':
description: User need to log in first.
'403':
description: User have no permission to get webhook policy of the project.
'500':
description: Internal server errors.
'/projects/{project_id}/webhook/jobs':
get:
summary: List project webhook jobs
description: |
This endpoint returns webhook jobs of a project.
parameters:
- name: project_id
in: path
type: integer
format: int64
required: true
description: Relevant project ID.
- name: policy_id
in: query
type: integer
format: int64
required: true
description: The policy ID.
tags:
- Products
responses:
'200':
description: List project webhook jobs successfully.
schema:
type: array
items:
$ref: '#/definitions/WebhookJob'
headers:
X-Total-Count:
description: The total count of available items
type: integer
Link:
description: Link to previous page and next page
type: string
'400':
description: Illegal format of provided ID value.
'401':
description: User need to log in first.
'403':
description: User have no permission to list webhook jobs of the project.
'500':
description: Unexpected internal errors.
'/projects/{project_id}/webhook/events':
get:
summary: Get supported event types and notify types.
description: Get supportted event types and notify types.
tags:
- Products
parameters:
- name: project_id
in: path
type: integer
format: int64
required: true
description: Relevant project ID.
responses:
'200':
description: Success
schema:
$ref: '#/definitions/SupportedWebhookEventTypes'
'401':
description: User need to log in first.
'403':
description: User have no permission to list webhook jobs of the project.
'500':
description: Unexpected internal errors.
responses:
OK:
description: 'Success'
@ -2701,105 +2407,6 @@ definitions:
cve_id:
type: string
description: The ID of the CVE, such as "CVE-2019-10164"
WebhookTargetObject:
type: object
description: The webhook policy target object.
properties:
type:
type: string
description: The webhook target notify type.
address:
type: string
description: The webhook target address.
auth_header:
type: string
description: The webhook auth header.
skip_cert_verify:
type: boolean
description: Whether or not to skip cert verify.
WebhookPolicy:
type: object
description: The webhook policy object
properties:
id:
type: integer
format: int64
description: The webhook policy ID.
name:
type: string
description: The name of webhook policy.
description:
type: string
description: The description of webhook policy.
project_id:
type: integer
description: The project ID of webhook policy.
targets:
type: array
items:
$ref: '#/definitions/WebhookTargetObject'
event_types:
type: array
items:
type: string
creator:
type: string
description: The creator of the webhook policy.
creation_time:
type: string
description: The create time of the webhook policy.
update_time:
type: string
description: The update time of the webhook policy.
enabled:
type: boolean
description: Whether the webhook policy is enabled or not.
WebhookLastTrigger:
type: object
description: The webhook policy and last trigger time group by event type.
properties:
event_type:
type: string
description: The webhook event type.
enabled:
type: boolean
description: Whether or not the webhook policy enabled.
creation_time:
type: string
description: The creation time of webhook policy.
last_trigger_time:
type: string
description: The last trigger time of webhook policy.
WebhookJob:
type: object
description: The webhook job.
properties:
id:
type: integer
format: int64
description: The webhook job ID.
policy_id:
type: integer
format: int64
description: The webhook policy ID.
event_type:
type: string
description: The webhook job event type.
notify_type:
type: string
description: The webhook job notify type.
status:
type: string
description: The webhook job status.
job_detail:
type: string
description: The webhook job notify detailed data.
creation_time:
type: string
description: The webhook job creation time.
update_time:
type: string
description: The webhook job update time.
QuotaSwitcher:
type: object
@ -2808,27 +2415,6 @@ definitions:
type: boolean
description: The quota is enable or disable
SupportedWebhookEventTypes:
type: object
description: Supportted webhook event types and notify types.
properties:
event_type:
type: array
items:
$ref: '#/definitions/EventType'
notify_type:
type: array
items:
$ref: '#/definitions/NotifyType'
EventType:
type: string
description: Webhook supportted event type.
example: 'pullImage'
NotifyType:
type: string
description: Webhook supportted notify type.
example: 'http'
parameters:
query:
name: q

View File

@ -1973,6 +1973,263 @@ paths:
$ref: '#/responses/403'
'500':
$ref: '#/responses/500'
'/projects/{project_name_or_id}/webhook/policies':
get:
summary: List project webhook policies.
description: |
This endpoint returns webhook policies of a project.
tags:
- webhook
operationId: ListWebhookPoliciesOfProject
parameters:
- $ref: '#/parameters/requestId'
- $ref: '#/parameters/isResourceName'
- $ref: '#/parameters/projectNameOrId'
- $ref: '#/parameters/sort'
- $ref: '#/parameters/query'
- $ref: '#/parameters/page'
- $ref: '#/parameters/pageSize'
responses:
'200':
description: Success
headers:
X-Total-Count:
description: The total count of webhook policies.
type: integer
Link:
description: Link refers to the previous page and next page
type: string
schema:
type: array
items:
$ref: '#/definitions/WebhookPolicy'
'400':
$ref: '#/responses/400'
'401':
$ref: '#/responses/401'
'403':
$ref: '#/responses/403'
'500':
$ref: '#/responses/500'
post:
summary: Create project webhook policy.
description: |
This endpoint create a webhook policy if the project does not have one.
tags:
- webhook
operationId: CreateWebhookPolicyOfProject
parameters:
- $ref: '#/parameters/requestId'
- $ref: '#/parameters/isResourceName'
- $ref: '#/parameters/projectNameOrId'
- name: policy
in: body
description: Properties "targets" and "event_types" needed.
required: true
schema:
$ref: '#/definitions/WebhookPolicy'
responses:
'201':
description: Project webhook policy create successfully.
headers:
X-Request-Id:
description: The ID of the corresponding request for the response
type: string
Location:
description: The location of the resource
type: string
'400':
$ref: '#/responses/400'
'401':
$ref: '#/responses/401'
'403':
$ref: '#/responses/403'
'500':
$ref: '#/responses/500'
'/projects/{project_name_or_id}/webhook/policies/{webhook_policy_id}':
get:
summary: Get project webhook policy
description: |
This endpoint returns specified webhook policy of a project.
tags:
- webhook
operationId: GetWebhookPolicyOfProject
parameters:
- $ref: '#/parameters/requestId'
- $ref: '#/parameters/isResourceName'
- $ref: '#/parameters/projectNameOrId'
- $ref: '#/parameters/webhookPolicyId'
responses:
'200':
description: Get webhook policy successfully.
schema:
$ref: '#/definitions/WebhookPolicy'
'400':
$ref: '#/responses/400'
'401':
$ref: '#/responses/401'
'403':
$ref: '#/responses/403'
'404':
$ref: '#/responses/404'
'500':
$ref: '#/responses/500'
put:
summary: Update webhook policy of a project.
description: |
This endpoint is aimed to update the webhook policy of a project.
tags:
- webhook
operationId: UpdateWebhookPolicyOfProject
parameters:
- $ref: '#/parameters/requestId'
- $ref: '#/parameters/isResourceName'
- $ref: '#/parameters/projectNameOrId'
- $ref: '#/parameters/webhookPolicyId'
- name: policy
in: body
description: All properties needed except "id", "project_id", "creation_time", "update_time".
required: true
schema:
$ref: '#/definitions/WebhookPolicy'
responses:
'200':
$ref: '#/responses/200'
'400':
$ref: '#/responses/400'
'401':
$ref: '#/responses/401'
'403':
$ref: '#/responses/403'
'404':
$ref: '#/responses/404'
'500':
$ref: '#/responses/500'
delete:
summary: Delete webhook policy of a project
description: |
This endpoint is aimed to delete webhookpolicy of a project.
tags:
- webhook
operationId: DeleteWebhookPolicyOfProject
parameters:
- $ref: '#/parameters/requestId'
- $ref: '#/parameters/isResourceName'
- $ref: '#/parameters/projectNameOrId'
- $ref: '#/parameters/webhookPolicyId'
responses:
'200':
$ref: '#/responses/200'
'400':
$ref: '#/responses/400'
'401':
$ref: '#/responses/401'
'403':
$ref: '#/responses/403'
'404':
$ref: '#/responses/404'
'500':
$ref: '#/responses/500'
'/projects/{project_name_or_id}/webhook/lasttrigger':
get:
summary: Get project webhook policy last trigger info
description: |
This endpoint returns last trigger information of project webhook policy.
tags:
- webhook
operationId: LastTrigger
parameters:
- $ref: '#/parameters/requestId'
- $ref: '#/parameters/isResourceName'
- $ref: '#/parameters/projectNameOrId'
responses:
'200':
description: Test webhook connection successfully.
schema:
type: array
items:
$ref: '#/definitions/WebhookLastTrigger'
'400':
$ref: '#/responses/400'
'401':
$ref: '#/responses/401'
'403':
$ref: '#/responses/403'
'500':
$ref: '#/responses/500'
'/projects/{project_name_or_id}/webhook/jobs':
get:
summary: List project webhook jobs
description: |
This endpoint returns webhook jobs of a project.
tags:
- webhookjob
operationId: ListWebhookJobs
parameters:
- $ref: '#/parameters/requestId'
- $ref: '#/parameters/isResourceName'
- $ref: '#/parameters/projectNameOrId'
- $ref: '#/parameters/query'
- $ref: '#/parameters/sort'
- $ref: '#/parameters/page'
- $ref: '#/parameters/pageSize'
- name: policy_id
in: query
type: integer
format: int64
required: true
description: The policy ID.
- name: status
in: query
description: The status of webhook job.
required: false
type: array
items:
type: string
responses:
'200':
description: List project webhook jobs successfully.
headers:
X-Total-Count:
description: The total count of available items
type: integer
Link:
description: Link to previous page and next page
type: string
schema:
type: array
items:
$ref: '#/definitions/WebhookJob'
'400':
$ref: '#/responses/400'
'401':
$ref: '#/responses/401'
'403':
$ref: '#/responses/403'
'500':
$ref: '#/responses/500'
'/projects/{project_name_or_id}/webhook/events':
get:
summary: Get supported event types and notify types.
description: Get supportted event types and notify types.
tags:
- webhook
operationId: GetSupportedEventTypes
parameters:
- $ref: '#/parameters/requestId'
- $ref: '#/parameters/isResourceName'
- $ref: '#/parameters/projectNameOrId'
responses:
'200':
description: Success
schema:
$ref: '#/definitions/SupportedWebhookEventTypes'
'401':
$ref: '#/responses/401'
'403':
$ref: '#/responses/403'
'500':
$ref: '#/responses/500'
/icons/{digest}:
get:
summary: Get artifact icon
@ -3710,6 +3967,13 @@ parameters:
required: true
type: integer
format: int64
webhookPolicyId:
name: webhook_policy_id
in: path
description: The ID of the webhook policy
required: true
type: integer
format: int64
immutableRuleId:
name: immutable_rule_id
in: path
@ -5824,3 +6088,132 @@ definitions:
type: Group
import:
package: "github.com/goharbor/harbor/src/pkg/ldap/model"
SupportedWebhookEventTypes:
type: object
description: Supportted webhook event types and notify types.
properties:
event_type:
type: array
items:
$ref: '#/definitions/EventType'
notify_type:
type: array
items:
$ref: '#/definitions/NotifyType'
EventType:
type: string
description: Webhook supportted event type.
example: 'pullImage'
NotifyType:
type: string
description: Webhook supportted notify type.
example: 'http'
WebhookTargetObject:
type: object
description: The webhook policy target object.
properties:
type:
type: string
description: The webhook target notify type.
address:
type: string
description: The webhook target address.
auth_header:
type: string
description: The webhook auth header.
skip_cert_verify:
type: boolean
description: Whether or not to skip cert verify.
WebhookPolicy:
type: object
description: The webhook policy object
properties:
id:
type: integer
format: int64
description: The webhook policy ID.
name:
type: string
description: The name of webhook policy.
description:
type: string
description: The description of webhook policy.
project_id:
type: integer
description: The project ID of webhook policy.
targets:
type: array
items:
$ref: '#/definitions/WebhookTargetObject'
event_types:
type: array
items:
type: string
creator:
type: string
description: The creator of the webhook policy.
creation_time:
type: string
description: The create time of the webhook policy.
format: date-time
update_time:
type: string
description: The update time of the webhook policy.
format: date-time
enabled:
type: boolean
description: Whether the webhook policy is enabled or not.
WebhookLastTrigger:
type: object
description: The webhook policy and last trigger time group by event type.
properties:
policy_name:
type: string
description: The webhook policy name.
event_type:
type: string
description: The webhook event type.
enabled:
type: boolean
description: Whether or not the webhook policy enabled.
creation_time:
type: string
description: The creation time of webhook policy.
format: date-time
last_trigger_time:
type: string
description: The last trigger time of webhook policy.
format: date-time
WebhookJob:
type: object
description: The webhook job.
properties:
id:
type: integer
format: int64
description: The webhook job ID.
policy_id:
type: integer
format: int64
description: The webhook policy ID.
event_type:
type: string
description: The webhook job event type.
notify_type:
type: string
description: The webhook job notify type.
status:
type: string
description: The webhook job status.
job_detail:
type: string
description: The webhook job notify detailed data.
creation_time:
type: string
description: The webhook job creation time.
format: date-time
update_time:
type: string
description: The webhook job update time.
format: date-time

View File

@ -1,122 +0,0 @@
package notification
import (
"fmt"
"github.com/astaxie/beego/orm"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/lib/log"
)
// UpdateNotificationJob update notification job
func UpdateNotificationJob(job *models.NotificationJob, props ...string) (int64, error) {
if job == nil {
return 0, errors.New("nil job")
}
if job.ID == 0 {
return 0, fmt.Errorf("notification job ID is empty")
}
o := dao.GetOrmer()
return o.Update(job, props...)
}
// AddNotificationJob insert new notification job to DB
func AddNotificationJob(job *models.NotificationJob) (int64, error) {
if job == nil {
return 0, errors.New("nil job")
}
o := dao.GetOrmer()
if len(job.Status) == 0 {
job.Status = models.JobPending
}
return o.Insert(job)
}
// GetNotificationJob ...
func GetNotificationJob(id int64) (*models.NotificationJob, error) {
o := dao.GetOrmer()
j := &models.NotificationJob{
ID: id,
}
err := o.Read(j)
if err == orm.ErrNoRows {
return nil, nil
}
return j, nil
}
// GetTotalCountOfNotificationJobs ...
func GetTotalCountOfNotificationJobs(query ...*models.NotificationJobQuery) (int64, error) {
qs := notificationJobQueryConditions(query...)
return qs.Count()
}
// GetNotificationJobs ...
func GetNotificationJobs(query ...*models.NotificationJobQuery) ([]*models.NotificationJob, error) {
var jobs []*models.NotificationJob
qs := notificationJobQueryConditions(query...)
if len(query) > 0 && query[0] != nil {
qs = dao.PaginateForQuerySetter(qs, query[0].Page, query[0].Size)
}
qs = qs.OrderBy("-UpdateTime")
_, err := qs.All(&jobs)
return jobs, err
}
// GetLastTriggerJobsGroupByEventType get notification jobs info of policy, including event type and last trigger time
func GetLastTriggerJobsGroupByEventType(policyID int64) ([]*models.NotificationJob, error) {
o := dao.GetOrmer()
// get jobs last triggered(created) group by event_type. postgres group by usage reference:
// https://stackoverflow.com/questions/13325583/postgresql-max-and-group-by
sql := `select distinct on (event_type) event_type, id, creation_time, status, notify_type, job_uuid, update_time,
creation_time, job_detail from notification_job where policy_id = ?
order by event_type, id desc, creation_time, status, notify_type, job_uuid, update_time, creation_time, job_detail`
jobs := []*models.NotificationJob{}
_, err := o.Raw(sql, policyID).QueryRows(&jobs)
if err != nil {
log.Errorf("query last trigger info group by event type failed: %v", err)
return nil, err
}
return jobs, nil
}
// DeleteNotificationJob ...
func DeleteNotificationJob(id int64) error {
o := dao.GetOrmer()
_, err := o.Delete(&models.NotificationJob{ID: id})
return err
}
// DeleteAllNotificationJobsByPolicyID ...
func DeleteAllNotificationJobsByPolicyID(policyID int64) (int64, error) {
o := dao.GetOrmer()
return o.Delete(&models.NotificationJob{PolicyID: policyID}, "policy_id")
}
func notificationJobQueryConditions(query ...*models.NotificationJobQuery) orm.QuerySeter {
qs := dao.GetOrmer().QueryTable(&models.NotificationJob{})
if len(query) == 0 || query[0] == nil {
return qs
}
q := query[0]
if q.PolicyID != 0 {
qs = qs.Filter("PolicyID", q.PolicyID)
}
if len(q.Statuses) > 0 {
qs = qs.Filter("Status__in", q.Statuses)
}
if len(q.EventTypes) > 0 {
qs = qs.Filter("EventType__in", q.EventTypes)
}
return qs
}

View File

@ -1,263 +0,0 @@
package notification
import (
"testing"
"github.com/goharbor/harbor/src/common/models"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
var (
testJob1 = &models.NotificationJob{
PolicyID: 1111,
EventType: "pushImage",
NotifyType: "http",
Status: "pending",
JobDetail: "{\"type\":\"pushImage\",\"occur_at\":1563536782,\"event_data\":{\"resources\":[{\"digest\":\"sha256:bf1684a6e3676389ec861c602e97f27b03f14178e5bc3f70dce198f9f160cce9\",\"tag\":\"v1.0\",\"resource_url\":\"10.194.32.23/myproj/alpine:v1.0\"}],\"repository\":{\"date_created\":1563505587,\"name\":\"alpine\",\"namespace\":\"myproj\",\"repo_full_name\":\"myproj/alpine\",\"repo_type\":\"private\"}},\"operator\":\"admin\"}",
UUID: "00000000",
}
testJob2 = &models.NotificationJob{
PolicyID: 111,
EventType: "pullImage",
NotifyType: "http",
Status: "",
JobDetail: "{\"type\":\"pushImage\",\"occur_at\":1563537782,\"event_data\":{\"resources\":[{\"digest\":\"sha256:bf1684a6e3676389ec861c602e97f27b03f14178e5bc3f70dce198f9f160cce9\",\"tag\":\"v1.0\",\"resource_url\":\"10.194.32.23/myproj/alpine:v1.0\"}],\"repository\":{\"date_created\":1563505587,\"name\":\"alpine\",\"namespace\":\"myproj\",\"repo_full_name\":\"myproj/alpine\",\"repo_type\":\"private\"}},\"operator\":\"admin\"}",
UUID: "00000000",
}
testJob3 = &models.NotificationJob{
PolicyID: 111,
EventType: "deleteImage",
NotifyType: "http",
Status: "pending",
JobDetail: "{\"type\":\"pushImage\",\"occur_at\":1563538782,\"event_data\":{\"resources\":[{\"digest\":\"sha256:bf1684a6e3676389ec861c602e97f27b03f14178e5bc3f70dce198f9f160cce9\",\"tag\":\"v1.0\",\"resource_url\":\"10.194.32.23/myproj/alpine:v1.0\"}],\"repository\":{\"date_created\":1563505587,\"name\":\"alpine\",\"namespace\":\"myproj\",\"repo_full_name\":\"myproj/alpine\",\"repo_type\":\"private\"}},\"operator\":\"admin\"}",
UUID: "00000000",
}
)
func TestAddNotificationJob(t *testing.T) {
tests := []struct {
name string
job *models.NotificationJob
want int64
wantErr bool
}{
{name: "AddNotificationJob nil", job: nil, wantErr: true},
{name: "AddNotificationJob 1", job: testJob1, want: 1},
{name: "AddNotificationJob 2", job: testJob2, want: 2},
{name: "AddNotificationJob 3", job: testJob3, want: 3},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := AddNotificationJob(tt.job)
if tt.wantErr {
require.NotNil(t, err, "wantErr: %s", err)
return
}
require.Nil(t, err)
assert.Equal(t, tt.want, got)
})
}
}
func TestGetTotalCountOfNotificationJobs(t *testing.T) {
type args struct {
query *models.NotificationJobQuery
}
tests := []struct {
name string
args args
want int64
wantErr bool
}{
{
name: "GetTotalCountOfNotificationJobs 1",
args: args{
query: &models.NotificationJobQuery{
PolicyID: 111,
},
},
want: 2,
},
{
name: "GetTotalCountOfNotificationJobs 2",
args: args{},
want: 3,
},
{
name: "GetTotalCountOfNotificationJobs 3",
args: args{
query: &models.NotificationJobQuery{
Statuses: []string{"pending"},
},
},
want: 3,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := GetTotalCountOfNotificationJobs(tt.args.query)
if tt.wantErr {
require.NotNil(t, err, "wantErr: %s", err)
return
}
require.Nil(t, err)
assert.Equal(t, tt.want, got)
})
}
}
func TestGetLastTriggerJobsGroupByEventType(t *testing.T) {
type args struct {
policyID int64
}
tests := []struct {
name string
args args
want []*models.NotificationJob
wantErr bool
}{
{
name: "GetLastTriggerJobsGroupByEventType",
args: args{
policyID: 111,
},
want: []*models.NotificationJob{
testJob2,
testJob3,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := GetLastTriggerJobsGroupByEventType(tt.args.policyID)
if tt.wantErr {
require.NotNil(t, err, "wantErr: %s", err)
return
}
require.Nil(t, err)
assert.Equal(t, len(tt.want), len(got))
})
}
}
func TestUpdateNotificationJob(t *testing.T) {
type args struct {
job *models.NotificationJob
props []string
}
tests := []struct {
name string
args args
want int64
wantErr bool
}{
{name: "UpdateNotificationJob Want Error 1", args: args{job: nil}, wantErr: true},
{name: "UpdateNotificationJob Want Error 2", args: args{job: &models.NotificationJob{ID: 0}}, wantErr: true},
{
name: "UpdateNotificationJob 1",
args: args{
job: &models.NotificationJob{ID: 1, UUID: "111111111111111"},
props: []string{"UUID"},
},
},
{
name: "UpdateNotificationJob 2",
args: args{
job: &models.NotificationJob{ID: 2, UUID: "222222222222222"},
props: []string{"UUID"},
},
},
{
name: "UpdateNotificationJob 3",
args: args{
job: &models.NotificationJob{ID: 3, UUID: "333333333333333"},
props: []string{"UUID"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := UpdateNotificationJob(tt.args.job, tt.args.props...)
if tt.wantErr {
require.NotNil(t, err, "Error: %s", err)
return
}
require.Nil(t, err)
gotJob, err := GetNotificationJob(tt.args.job.ID)
require.Nil(t, err)
assert.Equal(t, tt.args.job.UUID, gotJob.UUID)
})
}
}
func TestDeleteNotificationJob(t *testing.T) {
type args struct {
id int64
}
tests := []struct {
name string
args args
wantErr bool
}{
{name: "DeleteNotificationJob 1", args: args{id: 1}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := DeleteNotificationJob(tt.args.id)
if tt.wantErr {
require.NotNil(t, err, "Error: %s", err)
return
}
require.Nil(t, err)
job, err := GetNotificationJob(tt.args.id)
require.Nil(t, err)
assert.Nil(t, job)
})
}
}
func TestDeleteAllNotificationJobs(t *testing.T) {
type args struct {
policyID int64
query []*models.NotificationJobQuery
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "DeleteAllNotificationJobs 1",
args: args{
policyID: 111,
query: []*models.NotificationJobQuery{
{PolicyID: 111},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := DeleteAllNotificationJobsByPolicyID(tt.args.policyID)
if tt.wantErr {
require.NotNil(t, err, "Error: %s", err)
return
}
require.Nil(t, err)
jobs, err := GetNotificationJobs(tt.args.query...)
require.Nil(t, err)
assert.Equal(t, 0, len(jobs))
})
}
}

View File

@ -1,88 +0,0 @@
package notification
import (
"fmt"
"github.com/astaxie/beego/orm"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/lib/errors"
lib_orm "github.com/goharbor/harbor/src/lib/orm"
)
// GetNotificationPolicy return notification policy by id
func GetNotificationPolicy(id int64) (*models.NotificationPolicy, error) {
policy := new(models.NotificationPolicy)
o := dao.GetOrmer()
err := o.QueryTable(policy).Filter("id", id).One(policy)
if err == orm.ErrNoRows {
return nil, nil
}
return policy, err
}
// GetNotificationPolicyByName return notification policy by name
func GetNotificationPolicyByName(name string, projectID int64) (*models.NotificationPolicy, error) {
policy := new(models.NotificationPolicy)
o := dao.GetOrmer()
err := o.QueryTable(policy).Filter("name", name).Filter("projectID", projectID).One(policy)
if err == orm.ErrNoRows {
return nil, nil
}
return policy, err
}
// GetNotificationPolicies returns all notification policy in project
func GetNotificationPolicies(projectID int64) ([]*models.NotificationPolicy, error) {
var policies []*models.NotificationPolicy
qs := dao.GetOrmer().QueryTable(new(models.NotificationPolicy)).Filter("ProjectID", projectID).OrderBy("-CreationTime")
_, err := qs.All(&policies)
if err != nil {
return nil, err
}
return policies, nil
}
// AddNotificationPolicy insert new notification policy to DB
func AddNotificationPolicy(policy *models.NotificationPolicy) (int64, error) {
if policy == nil {
return 0, errors.New("nil policy")
}
o := dao.GetOrmer()
id, err := o.Insert(policy)
if err != nil {
if e := lib_orm.AsConflictError(err, "notification policy named %s already exists", policy.Name); e != nil {
err = e
return id, err
}
err = fmt.Errorf("failed to create the notification policy: %v", err)
return id, err
}
return id, err
}
// UpdateNotificationPolicy update t specified notification policy
func UpdateNotificationPolicy(policy *models.NotificationPolicy) error {
if policy == nil {
return errors.New("nil policy")
}
o := dao.GetOrmer()
_, err := o.Update(policy)
if err != nil {
if e := lib_orm.AsConflictError(err, "notification policy named %s already exists", policy.Name); e != nil {
return e
}
err = fmt.Errorf("failed to update the notification policy: %v", err)
return err
}
return err
}
// DeleteNotificationPolicy delete notification policy by id
func DeleteNotificationPolicy(id int64) error {
o := dao.GetOrmer()
_, err := o.Delete(&models.NotificationPolicy{ID: id})
return err
}

View File

@ -1,291 +0,0 @@
package notification
import (
"testing"
"time"
"github.com/goharbor/harbor/src/common/models"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
var (
testPly1 = &models.NotificationPolicy{
Name: "webhook test policy1",
Description: "webhook test policy1 description",
ProjectID: 111,
TargetsDB: "[{\"type\":\"http\",\"address\":\"http://10.173.32.58:9009\",\"token\":\"xxxxxxxxx\",\"skip_cert_verify\":true}]",
EventTypesDB: "[\"pushImage\",\"pullImage\",\"deleteImage\",\"uploadChart\",\"deleteChart\",\"downloadChart\",\"scanningFailed\",\"scanningCompleted\"]",
Creator: "no one",
CreationTime: time.Now(),
UpdateTime: time.Now(),
Enabled: true,
}
)
var (
testPly2 = &models.NotificationPolicy{
Name: "webhook test policy2",
Description: "webhook test policy2 description",
ProjectID: 222,
TargetsDB: "[{\"type\":\"http\",\"address\":\"http://10.173.32.58:9009\",\"token\":\"xxxxxxxxx\",\"skip_cert_verify\":true}]",
EventTypesDB: "[\"pushImage\",\"pullImage\",\"deleteImage\",\"uploadChart\",\"deleteChart\",\"downloadChart\",\"scanningFailed\",\"scanningCompleted\"]",
Creator: "no one",
CreationTime: time.Now(),
UpdateTime: time.Now(),
Enabled: true,
}
)
var (
testPly3 = &models.NotificationPolicy{
Name: "webhook test policy3",
Description: "webhook test policy3 description",
ProjectID: 333,
TargetsDB: "[{\"type\":\"http\",\"address\":\"http://10.173.32.58:9009\",\"token\":\"xxxxxxxxx\",\"skip_cert_verify\":true}]",
EventTypesDB: "[\"pushImage\",\"pullImage\",\"deleteImage\",\"uploadChart\",\"deleteChart\",\"downloadChart\",\"scanningFailed\",\"scanningCompleted\"]",
Creator: "no one",
CreationTime: time.Now(),
UpdateTime: time.Now(),
Enabled: true,
}
)
func TestAddNotificationPolicy(t *testing.T) {
tests := []struct {
name string
policy *models.NotificationPolicy
want int64
wantErr bool
}{
{name: "AddNotificationPolicy nil", policy: nil, wantErr: true},
{name: "AddNotificationPolicy 1", policy: testPly1, want: 1},
{name: "AddNotificationPolicy 2", policy: testPly2, want: 2},
{name: "AddNotificationPolicy 3", policy: testPly3, want: 3},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := AddNotificationPolicy(tt.policy)
if tt.wantErr {
require.NotNil(t, err, "wantErr: %s", err)
return
}
require.Nil(t, err)
assert.Equal(t, tt.want, got)
})
}
}
func TestGetNotificationPolicies(t *testing.T) {
tests := []struct {
name string
projectID int64
wantPolicies []*models.NotificationPolicy
wantErr bool
}{
{name: "GetNotificationPolicies nil", projectID: 0, wantPolicies: []*models.NotificationPolicy{}},
{name: "GetNotificationPolicies 1", projectID: 111, wantPolicies: []*models.NotificationPolicy{testPly1}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotPolicies, err := GetNotificationPolicies(tt.projectID)
if tt.wantErr {
require.NotNil(t, err, "wantErr: %s", err)
return
}
require.Nil(t, err)
for i, gotPolicy := range gotPolicies {
assert.Equal(t, tt.wantPolicies[i].Name, gotPolicy.Name)
assert.Equal(t, tt.wantPolicies[i].ID, gotPolicy.ID)
assert.Equal(t, tt.wantPolicies[i].EventTypesDB, gotPolicy.EventTypesDB)
assert.Equal(t, tt.wantPolicies[i].TargetsDB, gotPolicy.TargetsDB)
assert.Equal(t, tt.wantPolicies[i].Creator, gotPolicy.Creator)
assert.Equal(t, tt.wantPolicies[i].Enabled, gotPolicy.Enabled)
assert.Equal(t, tt.wantPolicies[i].Description, gotPolicy.Description)
}
})
}
}
func TestGetNotificationPolicy(t *testing.T) {
tests := []struct {
name string
id int64
wantPolicy *models.NotificationPolicy
wantErr bool
}{
{name: "GetRepPolicy 1", id: 1, wantPolicy: testPly1},
{name: "GetRepPolicy 2", id: 2, wantPolicy: testPly2},
{name: "GetRepPolicy 3", id: 3, wantPolicy: testPly3},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotPolicy, err := GetNotificationPolicy(tt.id)
if tt.wantErr {
require.NotNil(t, err, "wantErr: %s", err)
return
}
require.Nil(t, err)
assert.Equal(t, tt.wantPolicy.Name, gotPolicy.Name)
assert.Equal(t, tt.wantPolicy.ID, gotPolicy.ID)
assert.Equal(t, tt.wantPolicy.EventTypesDB, gotPolicy.EventTypesDB)
assert.Equal(t, tt.wantPolicy.TargetsDB, gotPolicy.TargetsDB)
assert.Equal(t, tt.wantPolicy.Creator, gotPolicy.Creator)
assert.Equal(t, tt.wantPolicy.Enabled, gotPolicy.Enabled)
assert.Equal(t, tt.wantPolicy.Description, gotPolicy.Description)
})
}
}
func TestGetNotificationPolicyByName(t *testing.T) {
type args struct {
name string
projectID int64
}
tests := []struct {
name string
args args
wantPolicy *models.NotificationPolicy
wantErr bool
}{
{name: "GetNotificationPolicyByName 1", args: args{name: testPly1.Name, projectID: testPly1.ProjectID}, wantPolicy: testPly1},
{name: "GetNotificationPolicyByName 2", args: args{name: testPly2.Name, projectID: testPly2.ProjectID}, wantPolicy: testPly2},
{name: "GetNotificationPolicyByName 3", args: args{name: testPly3.Name, projectID: testPly3.ProjectID}, wantPolicy: testPly3},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotPolicy, err := GetNotificationPolicyByName(tt.args.name, tt.args.projectID)
if tt.wantErr {
require.NotNil(t, err, "wantErr: %s", err)
return
}
require.Nil(t, err)
assert.Equal(t, tt.wantPolicy.Name, gotPolicy.Name)
assert.Equal(t, tt.wantPolicy.ID, gotPolicy.ID)
assert.Equal(t, tt.wantPolicy.EventTypesDB, gotPolicy.EventTypesDB)
assert.Equal(t, tt.wantPolicy.TargetsDB, gotPolicy.TargetsDB)
assert.Equal(t, tt.wantPolicy.Creator, gotPolicy.Creator)
assert.Equal(t, tt.wantPolicy.Enabled, gotPolicy.Enabled)
assert.Equal(t, tt.wantPolicy.Description, gotPolicy.Description)
})
}
}
func TestUpdateNotificationPolicy(t *testing.T) {
type args struct {
policy *models.NotificationPolicy
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "UpdateNotificationPolicy nil",
args: args{
policy: nil,
},
wantErr: true,
},
{
name: "UpdateNotificationPolicy 1",
args: args{
policy: &models.NotificationPolicy{
ID: 1,
Name: "webhook test policy1 new",
Description: "webhook test policy1 description new",
ProjectID: 111,
TargetsDB: "[{\"type\":\"http\",\"address\":\"http://10.173.32.58:9009\",\"token\":\"xxxxxxxxx\",\"skip_cert_verify\":true}]",
EventTypesDB: "[\"pushImage\",\"pullImage\",\"deleteImage\",\"uploadChart\",\"deleteChart\",\"downloadChart\",\"scanningFailed\",\"scanningCompleted\"]",
Creator: "no one",
CreationTime: time.Now(),
UpdateTime: time.Now(),
Enabled: true,
},
},
},
{
name: "UpdateNotificationPolicy 2",
args: args{
policy: &models.NotificationPolicy{
ID: 2,
Name: "webhook test policy2 new",
Description: "webhook test policy2 description new",
ProjectID: 222,
TargetsDB: "[{\"type\":\"http\",\"address\":\"http://10.173.32.58:9009\",\"token\":\"xxxxxxxxx\",\"skip_cert_verify\":true}]",
EventTypesDB: "[\"pushImage\",\"pullImage\",\"deleteImage\",\"uploadChart\",\"deleteChart\",\"downloadChart\",\"scanningFailed\",\"scanningCompleted\"]",
Creator: "no one",
CreationTime: time.Now(),
UpdateTime: time.Now(),
Enabled: true,
},
},
},
{
name: "UpdateNotificationPolicy 3",
args: args{
policy: &models.NotificationPolicy{
ID: 3,
Name: "webhook test policy3 new",
Description: "webhook test policy3 description new",
ProjectID: 333,
TargetsDB: "[{\"type\":\"http\",\"address\":\"http://10.173.32.58:9009\",\"token\":\"xxxxxxxxx\",\"skip_cert_verify\":true}]",
EventTypesDB: "[\"pushImage\",\"pullImage\",\"deleteImage\",\"uploadChart\",\"deleteChart\",\"downloadChart\",\"scanningFailed\",\"scanningCompleted\"]",
Creator: "no one",
CreationTime: time.Now(),
UpdateTime: time.Now(),
Enabled: true,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := UpdateNotificationPolicy(tt.args.policy)
if tt.wantErr {
require.NotNil(t, err, "Error: %s", err)
return
}
require.Nil(t, err)
gotPolicy, err := GetNotificationPolicy(tt.args.policy.ID)
require.Nil(t, err)
assert.Equal(t, tt.args.policy.Description, gotPolicy.Description)
assert.Equal(t, tt.args.policy.Name, gotPolicy.Name)
})
}
}
func TestDeleteNotificationPolicy(t *testing.T) {
tests := []struct {
name string
id int64
wantErr bool
}{
{name: "DeleteNotificationPolicy 1", id: 1, wantErr: false},
{name: "DeleteNotificationPolicy 2", id: 2, wantErr: false},
{name: "DeleteNotificationPolicy 3", id: 3, wantErr: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := DeleteNotificationPolicy(tt.id)
if tt.wantErr {
require.NotNil(t, err, "wantErr: %s", err)
return
}
require.Nil(t, err)
policy, err := GetNotificationPolicy(tt.id)
require.Nil(t, err)
assert.Nil(t, policy)
})
}
}

View File

@ -1,13 +0,0 @@
package notification
import (
"os"
"testing"
"github.com/goharbor/harbor/src/common/dao"
)
func TestMain(m *testing.M) {
dao.PrepareTestForPostgresSQL()
os.Exit(m.Run())
}

View File

@ -31,8 +31,6 @@ func init() {
new(UserGroup),
new(JobLog),
new(OIDCUser),
new(NotificationPolicy),
new(NotificationJob),
new(ProjectBlob),
new(ArtifactAndBlob),
)

View File

@ -16,10 +16,8 @@ package auditlog
import (
"context"
beegorm "github.com/astaxie/beego/orm"
"github.com/goharbor/harbor/src/controller/event"
"github.com/goharbor/harbor/src/lib/log"
"github.com/goharbor/harbor/src/lib/orm"
"github.com/goharbor/harbor/src/pkg/audit"
am "github.com/goharbor/harbor/src/pkg/audit/model"
)
@ -39,8 +37,7 @@ func (h *Handler) Name() string {
}
// Handle ...
func (h *Handler) Handle(value interface{}) error {
ctx := orm.NewContext(context.Background(), beegorm.NewOrm())
func (h *Handler) Handle(ctx context.Context, value interface{}) error {
var auditLog *am.AuditLog
switch v := value.(type) {
case *event.PushArtifactEvent, *event.PullArtifactEvent, *event.DeleteArtifactEvent,

View File

@ -14,7 +14,6 @@ import (
"github.com/goharbor/harbor/src/controller/event/handler/webhook/scan"
"github.com/goharbor/harbor/src/controller/event/metadata"
"github.com/goharbor/harbor/src/jobservice/job"
"github.com/goharbor/harbor/src/lib/orm"
"github.com/goharbor/harbor/src/pkg/notification"
"github.com/goharbor/harbor/src/pkg/notifier"
"github.com/goharbor/harbor/src/pkg/task"
@ -25,14 +24,14 @@ func init() {
notifier.Subscribe(event.TopicPushArtifact, &artifact.Handler{})
notifier.Subscribe(event.TopicPullArtifact, &artifact.Handler{})
notifier.Subscribe(event.TopicDeleteArtifact, &artifact.Handler{})
notifier.Subscribe(event.TopicUploadChart, &chart.Handler{Context: orm.Context})
notifier.Subscribe(event.TopicDeleteChart, &chart.Handler{Context: orm.Context})
notifier.Subscribe(event.TopicDownloadChart, &chart.Handler{Context: orm.Context})
notifier.Subscribe(event.TopicUploadChart, &chart.Handler{})
notifier.Subscribe(event.TopicDeleteChart, &chart.Handler{})
notifier.Subscribe(event.TopicDownloadChart, &chart.Handler{})
notifier.Subscribe(event.TopicQuotaExceed, &quota.Handler{})
notifier.Subscribe(event.TopicQuotaWarning, &quota.Handler{})
notifier.Subscribe(event.TopicScanningFailed, &scan.Handler{})
notifier.Subscribe(event.TopicScanningCompleted, &scan.Handler{})
notifier.Subscribe(event.TopicDeleteArtifact, &scan.DelArtHandler{Context: orm.Context})
notifier.Subscribe(event.TopicDeleteArtifact, &scan.DelArtHandler{})
notifier.Subscribe(event.TopicReplication, &artifact.ReplicationHandler{})
notifier.Subscribe(event.TopicTagRetention, &artifact.RetentionHandler{})
@ -43,9 +42,9 @@ func init() {
notifier.Subscribe(event.TopicDeleteTag, &replication.Handler{})
// p2p preheat
notifier.Subscribe(event.TopicPushArtifact, &p2p.Handler{Context: orm.Context})
notifier.Subscribe(event.TopicScanningCompleted, &p2p.Handler{Context: orm.Context})
notifier.Subscribe(event.TopicArtifactLabeled, &p2p.Handler{Context: orm.Context})
notifier.Subscribe(event.TopicPushArtifact, &p2p.Handler{})
notifier.Subscribe(event.TopicScanningCompleted, &p2p.Handler{})
notifier.Subscribe(event.TopicArtifactLabeled, &p2p.Handler{})
// audit logs
notifier.Subscribe(event.TopicPushArtifact, &auditlog.Handler{})
@ -58,8 +57,8 @@ func init() {
notifier.Subscribe(event.TopicDeleteTag, &auditlog.Handler{})
// internal
notifier.Subscribe(event.TopicPullArtifact, &internal.Handler{Context: orm.Context})
notifier.Subscribe(event.TopicPushArtifact, &internal.Handler{Context: orm.Context})
notifier.Subscribe(event.TopicPullArtifact, &internal.Handler{})
notifier.Subscribe(event.TopicPushArtifact, &internal.Handler{})
task.RegisterTaskStatusChangePostFunc(job.Replication, func(ctx context.Context, taskID int64, status string) error {
notification.AddEvent(ctx, &metadata.ReplicationMetaData{

View File

@ -28,7 +28,6 @@ import (
// Handler preprocess artifact event data
type Handler struct {
Context func() context.Context
}
// Name ...
@ -37,12 +36,12 @@ func (a *Handler) Name() string {
}
// Handle ...
func (a *Handler) Handle(value interface{}) error {
func (a *Handler) Handle(ctx context.Context, value interface{}) error {
switch v := value.(type) {
case *event.PullArtifactEvent:
return a.onPull(a.Context(), v.ArtifactEvent)
return a.onPull(ctx, v.ArtifactEvent)
case *event.PushArtifactEvent:
return a.onPush(a.Context(), v.ArtifactEvent)
return a.onPush(ctx, v.ArtifactEvent)
default:
log.Errorf("Can not handler this event type! %#v", v)
}

View File

@ -29,7 +29,6 @@ import (
// Handler ...
type Handler struct {
Context func() context.Context
}
// Name ...
@ -38,17 +37,17 @@ func (p *Handler) Name() string {
}
// Handle ...
func (p *Handler) Handle(value interface{}) error {
func (p *Handler) Handle(ctx context.Context, value interface{}) error {
switch value.(type) {
case *event.PushArtifactEvent:
pushArtEvent, _ := value.(*event.PushArtifactEvent)
return p.handlePushArtifact(pushArtEvent)
return p.handlePushArtifact(ctx, pushArtEvent)
case *event.ScanImageEvent:
scanImageEvent, _ := value.(*event.ScanImageEvent)
return p.handleImageScanned(scanImageEvent)
return p.handleImageScanned(ctx, scanImageEvent)
case *event.ArtifactLabeledEvent:
artifactLabeledEvent, _ := value.(*event.ArtifactLabeledEvent)
return p.handleArtifactLabeled(artifactLabeledEvent)
return p.handleArtifactLabeled(ctx, artifactLabeledEvent)
default:
return errors.New("unsupported type")
}
@ -59,7 +58,7 @@ func (p *Handler) IsStateful() bool {
return false
}
func (p *Handler) handlePushArtifact(event *event.PushArtifactEvent) error {
func (p *Handler) handlePushArtifact(ctx context.Context, event *event.PushArtifactEvent) error {
if event.Artifact.Type != image.ArtifactTypeImage {
return nil
}
@ -71,7 +70,7 @@ func (p *Handler) handlePushArtifact(event *event.PushArtifactEvent) error {
log.Debugf("preheat: artifact pushed %s:%s@%s", event.Artifact.RepositoryName, event.Tags, event.Artifact.Digest)
art, err := artifact.Ctl.Get(p.Context(), event.Artifact.ID, &artifact.Option{
art, err := artifact.Ctl.Get(ctx, event.Artifact.ID, &artifact.Option{
WithTag: true,
WithLabel: true,
})
@ -89,16 +88,16 @@ func (p *Handler) handlePushArtifact(event *event.PushArtifactEvent) error {
}
art.Tags = pt
_, err = preheat.Enf.PreheatArtifact(p.Context(), art)
_, err = preheat.Enf.PreheatArtifact(ctx, art)
return err
}
func (p *Handler) handleImageScanned(event *event.ScanImageEvent) error {
func (p *Handler) handleImageScanned(ctx context.Context, event *event.ScanImageEvent) error {
// TODO: If the scan is targeting an manifest list, here the artifacts we get are all the children
// artifacts of the manifest list. The children artifacts are high probably untagged ones that
// will be definitely ignored by the tag filter. We need to find a way to resolve this issue.
log.Debugf("preheat: image scanned %s:%s", event.Artifact.Repository, event.Artifact.Tag)
art, err := artifact.Ctl.GetByReference(p.Context(), event.Artifact.Repository, event.Artifact.Digest,
art, err := artifact.Ctl.GetByReference(ctx, event.Artifact.Repository, event.Artifact.Digest,
&artifact.Option{
WithTag: true,
WithLabel: true,
@ -106,12 +105,12 @@ func (p *Handler) handleImageScanned(event *event.ScanImageEvent) error {
if err != nil {
return err
}
_, err = preheat.Enf.PreheatArtifact(p.Context(), art)
_, err = preheat.Enf.PreheatArtifact(ctx, art)
return err
}
func (p *Handler) handleArtifactLabeled(event *event.ArtifactLabeledEvent) error {
art, err := artifact.Ctl.Get(p.Context(), event.ArtifactID, &artifact.Option{
func (p *Handler) handleArtifactLabeled(ctx context.Context, event *event.ArtifactLabeledEvent) error {
art, err := artifact.Ctl.Get(ctx, event.ArtifactID, &artifact.Option{
WithTag: true,
WithLabel: true,
})
@ -126,6 +125,6 @@ func (p *Handler) handleArtifactLabeled(event *event.ArtifactLabeledEvent) error
}
log.Debugf("preheat: artifact labeled %s:%s", art.Artifact.RepositoryName, art.Artifact.Digest)
_, err = preheat.Enf.PreheatArtifact(p.Context(), art)
_, err = preheat.Enf.PreheatArtifact(ctx, art)
return err
}

View File

@ -58,9 +58,7 @@ func (suite *PreheatTestSuite) SetupSuite() {
artifact.Ctl = fakeArtifactCtl
suite.preheatEnf = preheat.Enf
preheat.Enf = fakeEnforcer
suite.handler = &Handler{
Context: context.TODO,
}
suite.handler = &Handler{}
}
// TearDownSuite cleans the testing env
@ -138,7 +136,7 @@ func (suite *PreheatTestSuite) TestHandle() {
},
}
for _, tt := range tests {
err := suite.handler.Handle(tt.args.data)
err := suite.handler.Handle(context.TODO(), tt.args.data)
if tt.wantErr {
suite.Error(err, tt.name)
} else {

View File

@ -15,6 +15,7 @@
package replication
import (
"context"
"strconv"
"github.com/goharbor/harbor/src/controller/event"
@ -36,7 +37,7 @@ func (r *Handler) Name() string {
}
// Handle ...
func (r *Handler) Handle(value interface{}) error {
func (r *Handler) Handle(ctx context.Context, value interface{}) error {
pushArtEvent, ok := value.(*event.PushArtifactEvent)
if ok {
return r.handlePushArtifact(pushArtEvent)

View File

@ -3,18 +3,17 @@ package util
import (
"errors"
"fmt"
"strings"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/core/config"
"github.com/goharbor/harbor/src/lib/log"
"github.com/goharbor/harbor/src/pkg/distribution"
policy_model "github.com/goharbor/harbor/src/pkg/notification/policy/model"
"github.com/goharbor/harbor/src/pkg/notifier/event"
notifyModel "github.com/goharbor/harbor/src/pkg/notifier/model"
"strings"
)
// SendHookWithPolicies send hook by publishing topic of specified target type(notify type)
func SendHookWithPolicies(policies []*models.NotificationPolicy, payload *notifyModel.Payload, eventType string) error {
func SendHookWithPolicies(policies []*policy_model.Policy, payload *notifyModel.Payload, eventType string) error {
// if global notification configured disabled, return directly
if !config.NotificationEnable() {
log.Debug("notification feature is not enabled")

View File

@ -41,14 +41,14 @@ func (a *Handler) Name() string {
}
// Handle preprocess artifact event data and then publish hook event
func (a *Handler) Handle(value interface{}) error {
func (a *Handler) Handle(ctx context.Context, value interface{}) error {
switch v := value.(type) {
case *event.PushArtifactEvent:
return a.handle(v.ArtifactEvent)
return a.handle(ctx, v.ArtifactEvent)
case *event.PullArtifactEvent:
return a.handle(v.ArtifactEvent)
return a.handle(ctx, v.ArtifactEvent)
case *event.DeleteArtifactEvent:
return a.handle(v.ArtifactEvent)
return a.handle(ctx, v.ArtifactEvent)
default:
log.Errorf("Can not handler this event type! %#v", v)
}
@ -60,18 +60,19 @@ func (a *Handler) IsStateful() bool {
return false
}
func (a *Handler) handle(event *event.ArtifactEvent) error {
prj, err := project.Ctl.Get(orm.Context(), event.Artifact.ProjectID, project.Metadata(true))
func (a *Handler) handle(ctx context.Context, event *event.ArtifactEvent) error {
prj, err := project.Ctl.Get(ctx, event.Artifact.ProjectID, project.Metadata(true))
if err != nil {
log.Errorf("failed to get project: %d, error: %v", event.Artifact.ProjectID, err)
return err
}
policies, err := notification.PolicyMgr.GetRelatedPolices(prj.ProjectID, event.EventType)
policies, err := notification.PolicyMgr.GetRelatedPolices(ctx, prj.ProjectID, event.EventType)
if err != nil {
log.Errorf("failed to find policy for %s event: %v", event.EventType, err)
return err
}
log.Info(policies)
if len(policies) == 0 {
log.Debugf("cannot find policy for %s event: %v", event.EventType, event)
return nil

View File

@ -1,6 +1,7 @@
package artifact
import (
"context"
"errors"
"fmt"
"strings"
@ -31,7 +32,7 @@ func (r *ReplicationHandler) Name() string {
}
// Handle ...
func (r *ReplicationHandler) Handle(value interface{}) error {
func (r *ReplicationHandler) Handle(ctx context.Context, value interface{}) error {
if !config.NotificationEnable() {
log.Debug("notification feature is not enabled")
return nil
@ -50,7 +51,7 @@ func (r *ReplicationHandler) Handle(value interface{}) error {
return err
}
policies, err := notification.PolicyMgr.GetRelatedPolices(project.ProjectID, rpEvent.EventType)
policies, err := notification.PolicyMgr.GetRelatedPolices(orm.Context(), project.ProjectID, rpEvent.EventType)
if err != nil {
log.Errorf("failed to find policy for %s event: %v", rpEvent.EventType, err)
return err

View File

@ -15,6 +15,7 @@
package artifact
import (
"context"
"testing"
"time"
@ -26,65 +27,21 @@ import (
"github.com/goharbor/harbor/src/core/config"
"github.com/goharbor/harbor/src/lib/q"
"github.com/goharbor/harbor/src/pkg/notification"
policy_model "github.com/goharbor/harbor/src/pkg/notification/policy/model"
reppkg "github.com/goharbor/harbor/src/pkg/replication"
"github.com/goharbor/harbor/src/replication"
"github.com/goharbor/harbor/src/replication/model"
projecttesting "github.com/goharbor/harbor/src/testing/controller/project"
replicationtesting "github.com/goharbor/harbor/src/testing/controller/replication"
"github.com/goharbor/harbor/src/testing/mock"
testingnotification "github.com/goharbor/harbor/src/testing/pkg/notification/policy"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type fakedNotificationPolicyMgr struct {
}
type fakedReplicationRegistryMgr struct {
}
func (f *fakedNotificationPolicyMgr) Create(*models.NotificationPolicy) (int64, error) {
return 0, nil
}
// List the policies, returns the policy list and error
func (f *fakedNotificationPolicyMgr) List(int64) ([]*models.NotificationPolicy, error) {
return nil, nil
}
// Get policy with specified ID
func (f *fakedNotificationPolicyMgr) Get(int64) (*models.NotificationPolicy, error) {
return nil, nil
}
// GetByNameAndProjectID get policy by the name and projectID
func (f *fakedNotificationPolicyMgr) GetByNameAndProjectID(string, int64) (*models.NotificationPolicy, error) {
return nil, nil
}
// Update the specified policy
func (f *fakedNotificationPolicyMgr) Update(*models.NotificationPolicy) error {
return nil
}
// Delete the specified policy
func (f *fakedNotificationPolicyMgr) Delete(int64) error {
return nil
}
// Test the specified policy
func (f *fakedNotificationPolicyMgr) Test(*models.NotificationPolicy) error {
return nil
}
// GetRelatedPolices get event type related policies in project
func (f *fakedNotificationPolicyMgr) GetRelatedPolices(int64, string) ([]*models.NotificationPolicy, error) {
return []*models.NotificationPolicy{
{
ID: 0,
},
}, nil
}
// Add new registry
func (f *fakedReplicationRegistryMgr) Add(*model.Registry) (int64, error) {
return 0, nil
@ -141,7 +98,8 @@ func TestReplicationHandler_Handle(t *testing.T) {
project.Ctl = prj
repctl.Ctl = repCtl
}()
notification.PolicyMgr = &fakedNotificationPolicyMgr{}
policyMgrMock := &testingnotification.Manager{}
notification.PolicyMgr = policyMgrMock
replication.RegistryMgr = &fakedReplicationRegistryMgr{}
projectCtl := &projecttesting.Controller{}
project.Ctl = projectCtl
@ -150,6 +108,11 @@ func TestReplicationHandler_Handle(t *testing.T) {
mockRepCtl.On("GetPolicy", mock.Anything, mock.Anything).Return(&reppkg.Policy{ID: 1}, nil)
mockRepCtl.On("GetTask", mock.Anything, mock.Anything).Return(&repctl.Task{}, nil)
mockRepCtl.On("GetExecution", mock.Anything, mock.Anything).Return(&repctl.Execution{}, nil)
policyMgrMock.On("GetRelatedPolices", mock.Anything, mock.Anything, mock.Anything).Return([]*policy_model.Policy{
{
ID: 0,
},
}, nil)
mock.OnAnything(projectCtl, "GetByName").Return(&models.Project{ProjectID: 1}, nil)
@ -192,7 +155,7 @@ func TestReplicationHandler_Handle(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := handler.Handle(tt.args.data)
err := handler.Handle(context.TODO(), tt.args.data)
if tt.wantErr {
require.NotNil(t, err, "Error: %s", err)
return

View File

@ -1,6 +1,7 @@
package artifact
import (
"context"
"fmt"
"github.com/goharbor/harbor/src/controller/retention"
"github.com/goharbor/harbor/src/lib/orm"
@ -26,7 +27,7 @@ func (r *RetentionHandler) Name() string {
}
// Handle ...
func (r *RetentionHandler) Handle(value interface{}) error {
func (r *RetentionHandler) Handle(ctx context.Context, value interface{}) error {
if !config.NotificationEnable() {
log.Debug("notification feature is not enabled")
return nil
@ -54,7 +55,7 @@ func (r *RetentionHandler) Handle(value interface{}) error {
return nil
}
policies, err := notification.PolicyMgr.GetRelatedPolices(project, trEvent.EventType)
policies, err := notification.PolicyMgr.GetRelatedPolices(ctx, project, trEvent.EventType)
if err != nil {
log.Errorf("failed to find policy for %s event: %v", trEvent.EventType, err)
return err

View File

@ -1,21 +1,23 @@
package artifact
import (
"context"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/controller/retention"
ret "github.com/goharbor/harbor/src/pkg/retention"
"github.com/stretchr/testify/mock"
"os"
"testing"
"time"
"github.com/goharbor/harbor/src/controller/event"
"github.com/goharbor/harbor/src/controller/retention"
"github.com/goharbor/harbor/src/core/config"
"github.com/goharbor/harbor/src/lib/selector"
"github.com/goharbor/harbor/src/pkg/notification"
policy_model "github.com/goharbor/harbor/src/pkg/notification/policy/model"
ret "github.com/goharbor/harbor/src/pkg/retention"
retentiontesting "github.com/goharbor/harbor/src/testing/controller/retention"
testingnotification "github.com/goharbor/harbor/src/testing/pkg/notification/policy"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"os"
"testing"
"time"
)
func TestRetentionHandler_Handle(t *testing.T) {
@ -29,7 +31,8 @@ func TestRetentionHandler_Handle(t *testing.T) {
notification.PolicyMgr = policyMgr
retention.Ctl = oldretentionCtl
}()
notification.PolicyMgr = &fakedNotificationPolicyMgr{}
policyMgrMock := &testingnotification.Manager{}
notification.PolicyMgr = policyMgrMock
retentionCtl := &retentiontesting.Controller{}
retention.Ctl = retentionCtl
retentionCtl.On("GetRetentionExecTask", mock.Anything, mock.Anything).
@ -49,6 +52,11 @@ func TestRetentionHandler_Handle(t *testing.T) {
StartTime: time.Now(),
EndTime: time.Now(),
}, nil)
policyMgrMock.On("GetRelatedPolices", mock.Anything, mock.Anything, mock.Anything).Return([]*policy_model.Policy{
{
ID: 0,
},
}, nil)
type args struct {
data interface{}
@ -87,7 +95,7 @@ func TestRetentionHandler_Handle(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := handler.Handle(tt.args.data)
err := handler.Handle(context.TODO(), tt.args.data)
if tt.wantErr {
require.NotNil(t, err, "Error: %s", err)
return

View File

@ -31,7 +31,6 @@ import (
// Handler preprocess chart event data
type Handler struct {
Context func() context.Context
}
// Name ...
@ -40,7 +39,7 @@ func (cph *Handler) Name() string {
}
// Handle preprocess chart event data and then publish hook event
func (cph *Handler) Handle(value interface{}) error {
func (cph *Handler) Handle(ctx context.Context, value interface{}) error {
chartEvent, ok := value.(*event.ChartEvent)
if !ok {
return errors.New("invalid chart event type")
@ -50,12 +49,12 @@ func (cph *Handler) Handle(value interface{}) error {
return fmt.Errorf("data miss in chart event: %v", chartEvent)
}
prj, err := project.Ctl.Get(cph.Context(), chartEvent.ProjectName, project.Metadata(true))
prj, err := project.Ctl.Get(ctx, chartEvent.ProjectName, project.Metadata(true))
if err != nil {
log.Errorf("failed to find project[%s] for chart event: %v", chartEvent.ProjectName, err)
return err
}
policies, err := notification.PolicyMgr.GetRelatedPolices(prj.ProjectID, chartEvent.EventType)
policies, err := notification.PolicyMgr.GetRelatedPolices(ctx, prj.ProjectID, chartEvent.EventType)
if err != nil {
log.Errorf("failed to find policy for %s event: %v", chartEvent.EventType, err)
return err

View File

@ -23,9 +23,10 @@ import (
"github.com/goharbor/harbor/src/controller/project"
"github.com/goharbor/harbor/src/core/config"
"github.com/goharbor/harbor/src/pkg/notification"
"github.com/goharbor/harbor/src/pkg/notification/policy/model"
projecttesting "github.com/goharbor/harbor/src/testing/controller/project"
"github.com/goharbor/harbor/src/testing/mock"
testingnotification "github.com/goharbor/harbor/src/testing/pkg/notification"
testingnotification "github.com/goharbor/harbor/src/testing/pkg/notification/policy"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@ -35,7 +36,8 @@ func TestChartPreprocessHandler_Handle(t *testing.T) {
defer func() {
notification.PolicyMgr = PolicyMgr
}()
notification.PolicyMgr = &testingnotification.FakedPolicyMgr{}
policyMgrMock := &testingnotification.Manager{}
notification.PolicyMgr = policyMgrMock
ProjectCtl := project.Ctl
defer func() {
@ -58,8 +60,30 @@ func TestChartPreprocessHandler_Handle(t *testing.T) {
}
}, nil)
projectCtl.On("Get")
policyMgrMock.On("GetRelatedPolices", mock.Anything, mock.Anything, mock.Anything).Return([]*model.Policy{
{
ID: 1,
EventTypes: []string{
event.TopicUploadChart,
event.TopicDownloadChart,
event.TopicDeleteChart,
event.TopicPushArtifact,
event.TopicPullArtifact,
event.TopicDeleteArtifact,
event.TopicScanningFailed,
event.TopicScanningCompleted,
event.TopicQuotaExceed,
},
Targets: []model.EventTarget{
{
Type: "http",
Address: "http://127.0.0.1:8080",
},
},
},
}, nil)
handler := &Handler{Context: context.TODO}
handler := &Handler{}
config.Init()
type args struct {
@ -127,7 +151,7 @@ func TestChartPreprocessHandler_Handle(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := handler.Handle(tt.args.data)
err := handler.Handle(context.TODO(), tt.args.data)
if tt.wantErr {
require.NotNil(t, err, "Error: %s", err)
return

View File

@ -15,6 +15,7 @@
package quota
import (
"context"
"errors"
"fmt"
@ -39,7 +40,7 @@ func (qp *Handler) Name() string {
}
// Handle ...
func (qp *Handler) Handle(value interface{}) error {
func (qp *Handler) Handle(ctx context.Context, value interface{}) error {
quotaEvent, ok := value.(*event.QuotaEvent)
if !ok {
return errors.New("invalid quota event type")
@ -54,7 +55,7 @@ func (qp *Handler) Handle(value interface{}) error {
return err
}
policies, err := notification.PolicyMgr.GetRelatedPolices(prj.ProjectID, quotaEvent.EventType)
policies, err := notification.PolicyMgr.GetRelatedPolices(ctx, prj.ProjectID, quotaEvent.EventType)
if err != nil {
log.Errorf("failed to find policy for %s event: %v", quotaEvent.EventType, err)
return err

View File

@ -15,8 +15,11 @@
package quota
import (
"context"
common_dao "github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/controller/event"
policy_model "github.com/goharbor/harbor/src/pkg/notification/policy/model"
"github.com/goharbor/harbor/src/testing/mock"
"testing"
"time"
@ -27,7 +30,7 @@ import (
"github.com/goharbor/harbor/src/pkg/notification/policy"
"github.com/goharbor/harbor/src/pkg/notifier"
"github.com/goharbor/harbor/src/pkg/notifier/model"
testing_notification "github.com/goharbor/harbor/src/testing/pkg/notification"
testing_notification "github.com/goharbor/harbor/src/testing/pkg/notification/policy"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
@ -69,8 +72,13 @@ func (suite *QuotaPreprocessHandlerSuite) SetupSuite() {
}
suite.om = notification.PolicyMgr
mp := &testing_notification.FakedPolicyMgr{}
mp := &testing_notification.Manager{}
notification.PolicyMgr = mp
mp.On("GetRelatedPolices", mock.Anything, mock.Anything, mock.Anything).Return([]*policy_model.Policy{
{
ID: 1,
},
}, nil)
h := &MockHandler{}
@ -86,7 +94,7 @@ func (suite *QuotaPreprocessHandlerSuite) TearDownSuite() {
// TestHandle ...
func (suite *QuotaPreprocessHandlerSuite) TestHandle() {
handler := &Handler{}
err := handler.Handle(suite.evt)
err := handler.Handle(context.TODO(), suite.evt)
suite.NoError(err)
}
@ -99,7 +107,7 @@ func (m *MockHandler) Name() string {
}
// Handle ...
func (m *MockHandler) Handle(value interface{}) error {
func (m *MockHandler) Handle(ctx context.Context, value interface{}) error {
return nil
}

View File

@ -27,7 +27,6 @@ import (
// DelArtHandler is a handler to listen to the internal delete image event.
type DelArtHandler struct {
Context func() context.Context
}
// Name ...
@ -36,7 +35,7 @@ func (o *DelArtHandler) Name() string {
}
// Handle ...
func (o *DelArtHandler) Handle(value interface{}) error {
func (o *DelArtHandler) Handle(ctx context.Context, value interface{}) error {
if value == nil {
return errors.New("delete image event handler: nil value ")
}
@ -48,7 +47,6 @@ func (o *DelArtHandler) Handle(value interface{}) error {
log.Debugf("clear the scan reports as receiving event %s", evt.EventType)
ctx := o.Context()
// Check if it is safe to delete the reports.
count, err := artifact.Ctl.Count(ctx, q.New(q.KeyWords{"digest": evt.Artifact.Digest}))
if err != nil {

View File

@ -54,11 +54,11 @@ func (suite *DelArtHandlerTestSuite) TeardownSuite() {
}
func (suite *DelArtHandlerTestSuite) TestHandle() {
o := DelArtHandler{Context: context.TODO}
o := DelArtHandler{}
suite.Error(o.Handle(nil))
suite.Error(o.Handle(context.TODO(), nil))
suite.Error(o.Handle("string"))
suite.Error(o.Handle(context.TODO(), "string"))
art := &artifact.Artifact{}
art.Digest = "digest"
@ -67,18 +67,18 @@ func (suite *DelArtHandlerTestSuite) TestHandle() {
value := &event.DeleteArtifactEvent{ArtifactEvent: ev}
suite.artifactCtl.On("Count", context.TODO(), q.New(q.KeyWords{"digest": "digest"})).Return(int64(0), fmt.Errorf("failed")).Once()
suite.Require().NoError(o.Handle(value))
suite.Require().NoError(o.Handle(context.TODO(), value))
suite.artifactCtl.On("Count", context.TODO(), q.New(q.KeyWords{"digest": "digest"})).Return(int64(1), nil).Once()
suite.Require().NoError(o.Handle(value))
suite.Require().NoError(o.Handle(context.TODO(), value))
suite.artifactCtl.On("Count", context.TODO(), q.New(q.KeyWords{"digest": "digest"})).Return(int64(0), nil).Once()
suite.scanCtl.On("DeleteReports", context.TODO(), "digest").Return(fmt.Errorf("failed")).Once()
suite.Require().Error(o.Handle(value))
suite.Require().Error(o.Handle(context.TODO(), value))
suite.artifactCtl.On("Count", context.TODO(), q.New(q.KeyWords{"digest": "digest"})).Return(int64(0), nil).Once()
suite.scanCtl.On("DeleteReports", context.TODO(), "digest").Return(nil).Once()
suite.Require().NoError(o.Handle(value))
suite.Require().NoError(o.Handle(context.TODO(), value))
}
func TestDelArtHandlerTestSuite(t *testing.T) {

View File

@ -43,7 +43,7 @@ func (si *Handler) Name() string {
}
// Handle preprocess chart event data and then publish hook event
func (si *Handler) Handle(value interface{}) error {
func (si *Handler) Handle(ctx context.Context, value interface{}) error {
if value == nil {
return errors.New("empty scan artifact event")
}
@ -53,7 +53,7 @@ func (si *Handler) Handle(value interface{}) error {
return errors.New("invalid scan artifact event type")
}
policies, err := notification.PolicyMgr.GetRelatedPolices(e.Artifact.NamespaceID, e.EventType)
policies, err := notification.PolicyMgr.GetRelatedPolices(ctx, e.Artifact.NamespaceID, e.EventType)
if err != nil {
return errors.Wrap(err, "scan preprocess handler")
}

View File

@ -15,8 +15,10 @@
package scan
import (
"context"
common_dao "github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/controller/event"
policy_model "github.com/goharbor/harbor/src/pkg/notification/policy/model"
"testing"
"time"
@ -34,7 +36,7 @@ import (
artifacttesting "github.com/goharbor/harbor/src/testing/controller/artifact"
scantesting "github.com/goharbor/harbor/src/testing/controller/scan"
"github.com/goharbor/harbor/src/testing/mock"
notificationtesting "github.com/goharbor/harbor/src/testing/pkg/notification"
notificationtesting "github.com/goharbor/harbor/src/testing/pkg/notification/policy"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
@ -108,8 +110,13 @@ func (suite *ScanImagePreprocessHandlerSuite) SetupSuite() {
artifact.Ctl = artifactCtl
suite.om = notification.PolicyMgr
mp := &notificationtesting.FakedPolicyMgr{}
mp := &notificationtesting.Manager{}
notification.PolicyMgr = mp
mp.On("GetRelatedPolices", mock.Anything, mock.Anything, mock.Anything).Return([]*policy_model.Policy{
{
ID: 1,
},
}, nil)
h := &MockHTTPHandler{}
@ -128,7 +135,7 @@ func (suite *ScanImagePreprocessHandlerSuite) TearDownSuite() {
func (suite *ScanImagePreprocessHandlerSuite) TestHandle() {
handler := &Handler{}
err := handler.Handle(suite.evt)
err := handler.Handle(context.TODO(), suite.evt)
suite.NoError(err)
}
@ -143,7 +150,7 @@ func (m *MockHTTPHandler) Name() string {
}
// Handle ...
func (m *MockHTTPHandler) Handle(value interface{}) error {
func (m *MockHTTPHandler) Handle(ctx context.Context, value interface{}) error {
return nil
}

View File

@ -39,7 +39,6 @@ import (
_ "github.com/goharbor/harbor/src/core/auth/db"
_ "github.com/goharbor/harbor/src/core/auth/ldap"
"github.com/goharbor/harbor/src/core/config"
"github.com/goharbor/harbor/src/pkg/notification"
"github.com/goharbor/harbor/src/replication/model"
"github.com/goharbor/harbor/src/server/middleware"
"github.com/goharbor/harbor/src/server/middleware/orm"
@ -119,11 +118,6 @@ func init() {
beego.Router("/api/replication/adapters", &ReplicationAdapterAPI{}, "get:List")
beego.Router("/api/projects/:pid([0-9]+)/webhook/policies", &NotificationPolicyAPI{}, "get:List;post:Post")
beego.Router("/api/projects/:pid([0-9]+)/webhook/policies/:id([0-9]+)", &NotificationPolicyAPI{})
beego.Router("/api/projects/:pid([0-9]+)/webhook/policies/test", &NotificationPolicyAPI{}, "post:Test")
beego.Router("/api/projects/:pid([0-9]+)/webhook/lasttrigger", &NotificationPolicyAPI{}, "get:ListGroupByEventType")
beego.Router("/api/projects/:pid([0-9]+)/webhook/jobs/", &NotificationJobAPI{}, "get:List")
// Charts are controlled under projects
chartRepositoryAPIType := &ChartRepositoryAPI{}
beego.Router("/api/chartrepo/health", chartRepositoryAPIType, "get:GetHealthStatus")
@ -153,9 +147,6 @@ func init() {
unknownUsr = &usrInfo{"unknown", "unknown"}
testUser = &usrInfo{TestUserName, TestUserPwd}
// Init notification related check map
notification.Init()
// Init mock jobservice
mockServer := test.NewJobServiceServer()
defer mockServer.Close()

View File

@ -1,100 +0,0 @@
package api
import (
"fmt"
"github.com/goharbor/harbor/src/common/rbac/system"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/pkg/notification"
)
// NotificationJobAPI ...
type NotificationJobAPI struct {
BaseController
project *models.Project
}
// Prepare ...
func (w *NotificationJobAPI) Prepare() {
w.BaseController.Prepare()
if !w.SecurityCtx.IsAuthenticated() {
w.SendUnAuthorizedError(errors.New("UnAuthorized"))
return
}
pid, err := w.GetInt64FromPath(":pid")
if err != nil {
w.SendBadRequestError(fmt.Errorf("failed to get project ID: %v", err))
return
}
if pid <= 0 {
w.SendBadRequestError(fmt.Errorf("invalid project ID: %d", pid))
return
}
project, err := w.ProjectCtl.Get(w.Context(), pid)
if err != nil {
if errors.IsNotFoundErr(err) {
w.SendNotFoundError(fmt.Errorf("project %d not found", pid))
} else {
w.SendInternalServerError(fmt.Errorf("failed to get project %d: %v", pid, err))
}
return
}
w.project = project
}
// List ...
func (w *NotificationJobAPI) List() {
if !w.validateRBAC(rbac.ActionList, w.project.ProjectID) {
return
}
policyID, err := w.GetInt64("policy_id")
if err != nil || policyID <= 0 {
w.SendBadRequestError(fmt.Errorf("invalid policy_id: %s", w.GetString("policy_id")))
return
}
policy, err := notification.PolicyMgr.Get(policyID)
if err != nil {
w.SendInternalServerError(fmt.Errorf("failed to get policy %d: %v", policyID, err))
return
}
if policy == nil {
w.SendBadRequestError(fmt.Errorf("policy %d not found", policyID))
return
}
query := &models.NotificationJobQuery{
PolicyID: policyID,
}
query.Statuses = w.GetStrings("status")
query.Page, query.Size, err = w.GetPaginationParams()
if err != nil {
w.SendBadRequestError(err)
return
}
total, jobs, err := notification.JobMgr.List(query)
if err != nil {
w.SendInternalServerError(fmt.Errorf("failed to list notification jobs: %v", err))
return
}
w.SetPaginationHeader(total, query.Page, query.Size)
w.WriteJSONData(jobs)
}
func (w *NotificationJobAPI) validateRBAC(action rbac.Action, projectID int64) bool {
resource := system.NewNamespace().Resource(rbac.ResourceNotificationPolicy)
if w.SecurityCtx.Can(w.Context(), action, resource) {
return true
}
return w.RequireProjectAccess(projectID, action, rbac.ResourceNotificationPolicy)
}

View File

@ -1,107 +0,0 @@
package api
import (
"github.com/goharbor/harbor/src/controller/event"
"net/http"
"testing"
"time"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/pkg/notification"
)
type fakedNotificationJobMgr struct {
}
func (f *fakedNotificationJobMgr) Create(job *models.NotificationJob) (int64, error) {
return 1, nil
}
func (f *fakedNotificationJobMgr) List(...*models.NotificationJobQuery) (int64, []*models.NotificationJob, error) {
return 0, nil, nil
}
func (f *fakedNotificationJobMgr) Update(job *models.NotificationJob, props ...string) error {
return nil
}
func (f *fakedNotificationJobMgr) ListJobsGroupByEventType(policyID int64) ([]*models.NotificationJob, error) {
return []*models.NotificationJob{
{
EventType: event.TopicPullArtifact,
CreationTime: time.Now(),
},
{
EventType: event.TopicDeleteArtifact,
CreationTime: time.Now(),
},
}, nil
}
func TestNotificationJobAPI_List(t *testing.T) {
policyMgr := notification.PolicyMgr
jobMgr := notification.JobMgr
defer func() {
notification.PolicyMgr = policyMgr
notification.JobMgr = jobMgr
}()
notification.PolicyMgr = &fakedNotificationPlyMgr{}
notification.JobMgr = &fakedNotificationJobMgr{}
cases := []*codeCheckingCase{
// 401
{
request: &testingRequest{
method: http.MethodGet,
url: "/api/projects/1/webhook/jobs?policy_id=1",
},
code: http.StatusUnauthorized,
},
// 403
{
request: &testingRequest{
method: http.MethodGet,
url: "/api/projects/1/webhook/jobs?policy_id=1",
credential: nonSysAdmin,
},
code: http.StatusForbidden,
},
// 400 policyID invalid
{
request: &testingRequest{
method: http.MethodGet,
url: "/api/projects/1/webhook/jobs?policy_id=0",
credential: sysAdmin,
},
code: http.StatusBadRequest,
},
// 400 policyID not found
{
request: &testingRequest{
method: http.MethodGet,
url: "/api/projects/1/webhook/jobs?policy_id=123",
credential: sysAdmin,
},
code: http.StatusBadRequest,
},
// 404 project not found
{
request: &testingRequest{
method: http.MethodGet,
url: "/api/projects/123/webhook/jobs?policy_id=1",
credential: sysAdmin,
},
code: http.StatusNotFound,
},
// 200
{
request: &testingRequest{
method: http.MethodGet,
url: "/api/projects/1/webhook/jobs?policy_id=1",
credential: sysAdmin,
},
code: http.StatusOK,
},
}
runCodeCheckingCases(t, cases...)
}

View File

@ -1,428 +0,0 @@
package api
import (
"fmt"
"net/http"
"strconv"
"time"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/common/utils"
"github.com/goharbor/harbor/src/controller/event"
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/lib/log"
"github.com/goharbor/harbor/src/pkg/notification"
)
// NotificationPolicyAPI ...
type NotificationPolicyAPI struct {
BaseController
project *models.Project
supportedEvents map[string]struct{}
}
// notificationPolicyForUI defines the structure of notification policy info display in UI
type notificationPolicyForUI struct {
PolicyName string `json:"policy_name"`
EventType string `json:"event_type"`
Enabled bool `json:"enabled"`
CreationTime *time.Time `json:"creation_time"`
LastTriggerTime *time.Time `json:"last_trigger_time,omitempty"`
}
type notificationSupportedEventTypes struct {
EventType []string `json:"event_type"`
NotifyType []string `json:"notify_type"`
}
// Prepare ...
func (w *NotificationPolicyAPI) Prepare() {
w.BaseController.Prepare()
if !w.SecurityCtx.IsAuthenticated() {
w.SendUnAuthorizedError(errors.New("UnAuthorized"))
return
}
pid, err := w.GetInt64FromPath(":pid")
if err != nil {
w.SendBadRequestError(fmt.Errorf("failed to get project ID: %v", err))
return
}
if pid <= 0 {
w.SendBadRequestError(fmt.Errorf("invalid project ID: %d", pid))
return
}
project, err := w.ProjectCtl.Get(w.Context(), pid)
if err != nil {
if errors.IsNotFoundErr(err) {
w.SendNotFoundError(fmt.Errorf("project %d not found", pid))
} else {
w.SendInternalServerError(fmt.Errorf("failed to get project %d: %v", pid, err))
}
return
}
w.project = project
w.supportedEvents = initSupportedEvents()
}
// Get ...
func (w *NotificationPolicyAPI) Get() {
if !w.validateRBAC(rbac.ActionRead, w.project.ProjectID) {
return
}
id, err := w.GetIDFromURL()
if err != nil {
w.SendBadRequestError(err)
return
}
policy, err := notification.PolicyMgr.Get(id)
if err != nil {
w.SendInternalServerError(fmt.Errorf("failed to get the notification policy %d: %v", id, err))
return
}
if policy == nil {
w.SendNotFoundError(fmt.Errorf("notification policy %d not found", id))
return
}
if w.project.ProjectID != policy.ProjectID {
w.SendBadRequestError(fmt.Errorf("notification policy %d with projectID %d not belong to project %d in URL", id, policy.ProjectID, w.project.ProjectID))
return
}
w.WriteJSONData(policy)
}
// Post ...
func (w *NotificationPolicyAPI) Post() {
if !w.validateRBAC(rbac.ActionCreate, w.project.ProjectID) {
return
}
policy := &models.NotificationPolicy{}
isValid, err := w.DecodeJSONReqAndValidate(policy)
if !isValid {
w.SendBadRequestError(err)
return
}
if !w.validateTargets(policy) {
return
}
if !w.validateEventTypes(policy) {
return
}
if policy.ID != 0 {
w.SendBadRequestError(fmt.Errorf("cannot accept policy creating request with ID: %d", policy.ID))
return
}
policy.Creator = w.SecurityCtx.GetUsername()
policy.ProjectID = w.project.ProjectID
id, err := notification.PolicyMgr.Create(policy)
if err != nil {
w.SendError(err)
return
}
w.Redirect(http.StatusCreated, strconv.FormatInt(id, 10))
}
// Put ...
func (w *NotificationPolicyAPI) Put() {
if !w.validateRBAC(rbac.ActionUpdate, w.project.ProjectID) {
return
}
id, err := w.GetIDFromURL()
if id < 0 || err != nil {
w.SendBadRequestError(errors.New("invalid notification policy ID"))
return
}
oriPolicy, err := notification.PolicyMgr.Get(id)
if err != nil {
w.SendInternalServerError(fmt.Errorf("failed to get the notification policy %d: %v", id, err))
return
}
if oriPolicy == nil {
w.SendNotFoundError(fmt.Errorf("notification policy %d not found", id))
return
}
policy := &models.NotificationPolicy{}
isValid, err := w.DecodeJSONReqAndValidate(policy)
if !isValid {
w.SendBadRequestError(err)
return
}
if !w.validateTargets(policy) {
return
}
if !w.validateEventTypes(policy) {
return
}
if w.project.ProjectID != oriPolicy.ProjectID {
w.SendBadRequestError(fmt.Errorf("notification policy %d with projectID %d not belong to project %d in URL", id, oriPolicy.ProjectID, w.project.ProjectID))
return
}
policy.ID = id
policy.ProjectID = w.project.ProjectID
if err = notification.PolicyMgr.Update(policy); err != nil {
w.SendError(err)
return
}
}
// List ...
func (w *NotificationPolicyAPI) List() {
projectID := w.project.ProjectID
if !w.validateRBAC(rbac.ActionList, projectID) {
return
}
res, err := notification.PolicyMgr.List(projectID)
if err != nil {
w.SendInternalServerError(fmt.Errorf("failed to list notification policies by projectID %d: %v", projectID, err))
return
}
policies := []*models.NotificationPolicy{}
if res != nil {
for _, policy := range res {
policies = append(policies, policy)
}
}
w.WriteJSONData(policies)
}
// ListGroupByEventType lists notification policy trigger info grouped by event type for UI,
// displays event type, status(enabled/disabled), create time, last trigger time
func (w *NotificationPolicyAPI) ListGroupByEventType() {
projectID := w.project.ProjectID
if !w.validateRBAC(rbac.ActionList, projectID) {
return
}
res, err := notification.PolicyMgr.List(projectID)
if err != nil {
w.SendInternalServerError(fmt.Errorf("failed to list notification policies by projectID %d: %v", projectID, err))
return
}
policies, err := constructPolicyWithTriggerTime(res)
if err != nil {
w.SendInternalServerError(fmt.Errorf("failed to list the notification policy trigger information: %v", err))
return
}
w.WriteJSONData(policies)
}
// Delete ...
func (w *NotificationPolicyAPI) Delete() {
projectID := w.project.ProjectID
if !w.validateRBAC(rbac.ActionDelete, projectID) {
return
}
id, err := w.GetIDFromURL()
if id < 0 || err != nil {
w.SendBadRequestError(errors.New("invalid notification policy ID"))
return
}
policy, err := notification.PolicyMgr.Get(id)
if err != nil {
w.SendInternalServerError(fmt.Errorf("failed to get the notification policy %d: %v", id, err))
return
}
if policy == nil {
w.SendNotFoundError(fmt.Errorf("notification policy %d not found", id))
return
}
if projectID != policy.ProjectID {
w.SendBadRequestError(fmt.Errorf("notification policy %d with projectID %d not belong to project %d in URL", id, policy.ProjectID, projectID))
return
}
if err = notification.PolicyMgr.Delete(id); err != nil {
w.SendInternalServerError(fmt.Errorf("failed to delete notification policy %d: %v", id, err))
return
}
}
// GetSupportedEventTypes get supported trigger event types and notify types in module notification
func (w *NotificationPolicyAPI) GetSupportedEventTypes() {
projectID := w.project.ProjectID
if !w.validateRBAC(rbac.ActionList, projectID) {
return
}
var notificationTypes = notificationSupportedEventTypes{}
for key := range notification.SupportedNotifyTypes {
notificationTypes.NotifyType = append(notificationTypes.NotifyType, key)
}
for key := range w.supportedEvents {
notificationTypes.EventType = append(notificationTypes.EventType, key)
}
w.WriteJSONData(notificationTypes)
}
// Test ...
func (w *NotificationPolicyAPI) Test() {
projectID := w.project.ProjectID
if !w.validateRBAC(rbac.ActionCreate, projectID) {
return
}
policy := &models.NotificationPolicy{}
isValid, err := w.DecodeJSONReqAndValidate(policy)
if !isValid {
w.SendBadRequestError(err)
return
}
if !w.validateTargets(policy) {
return
}
if err := notification.PolicyMgr.Test(policy); err != nil {
log.Errorf("notification policy %s test failed: %v", policy.Name, err)
w.SendBadRequestError(fmt.Errorf("notification policy %s test failed", policy.Name))
return
}
}
func (w *NotificationPolicyAPI) validateRBAC(action rbac.Action, projectID int64) bool {
if w.SecurityCtx.IsSysAdmin() {
return true
}
return w.RequireProjectAccess(projectID, action, rbac.ResourceNotificationPolicy)
}
func (w *NotificationPolicyAPI) validateTargets(policy *models.NotificationPolicy) bool {
if len(policy.Targets) == 0 {
w.SendBadRequestError(fmt.Errorf("empty notification target with policy %s", policy.Name))
return false
}
for _, target := range policy.Targets {
url, err := utils.ParseEndpoint(target.Address)
if err != nil {
w.SendBadRequestError(err)
return false
}
// Prevent SSRF security issue #3755
target.Address = url.Scheme + "://" + url.Host + url.Path
_, ok := notification.SupportedNotifyTypes[target.Type]
if !ok {
w.SendBadRequestError(fmt.Errorf("unsupport target type %s with policy %s", target.Type, policy.Name))
return false
}
}
return true
}
func (w *NotificationPolicyAPI) validateEventTypes(policy *models.NotificationPolicy) bool {
if len(policy.EventTypes) == 0 {
w.SendBadRequestError(errors.New("empty event type"))
return false
}
for _, eventType := range policy.EventTypes {
_, ok := w.supportedEvents[eventType]
if !ok {
w.SendBadRequestError(fmt.Errorf("unsupport event type %s", eventType))
return false
}
}
return true
}
func getLastTriggerTimeGroupByEventType(eventType string, policyID int64) (time.Time, error) {
jobs, err := notification.JobMgr.ListJobsGroupByEventType(policyID)
if err != nil {
return time.Time{}, err
}
for _, job := range jobs {
if eventType == job.EventType {
return job.CreationTime, nil
}
}
return time.Time{}, nil
}
func initSupportedEvents() map[string]struct{} {
eventTypes := []string{
event.TopicPushArtifact,
event.TopicPullArtifact,
event.TopicDeleteArtifact,
event.TopicUploadChart,
event.TopicDeleteChart,
event.TopicDownloadChart,
event.TopicQuotaExceed,
event.TopicQuotaWarning,
event.TopicScanningFailed,
event.TopicScanningCompleted,
event.TopicReplication,
event.TopicTagRetention,
}
var supportedEventTypes = make(map[string]struct{})
for _, eventType := range eventTypes {
supportedEventTypes[eventType] = struct{}{}
}
return supportedEventTypes
}
// constructPolicyWithTriggerTime construct notification policy information displayed in UI
// including event type, enabled, creation time, last trigger time
func constructPolicyWithTriggerTime(policies []*models.NotificationPolicy) ([]*notificationPolicyForUI, error) {
res := []*notificationPolicyForUI{}
if policies != nil {
for _, policy := range policies {
for _, t := range policy.EventTypes {
ply := &notificationPolicyForUI{
PolicyName: policy.Name,
EventType: t,
Enabled: policy.Enabled,
CreationTime: &policy.CreationTime,
}
if !policy.CreationTime.IsZero() {
ply.CreationTime = &policy.CreationTime
}
ltTime, err := getLastTriggerTimeGroupByEventType(t, policy.ID)
if err != nil {
return nil, err
}
if !ltTime.IsZero() {
ply.LastTriggerTime = &ltTime
}
res = append(res, ply)
}
}
}
return res, nil
}

View File

@ -1,636 +0,0 @@
package api
import (
"github.com/goharbor/harbor/src/controller/event"
"net/http"
"testing"
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/pkg/notification"
)
type fakedNotificationPlyMgr struct {
}
func (f *fakedNotificationPlyMgr) Create(*models.NotificationPolicy) (int64, error) {
return 0, nil
}
func (f *fakedNotificationPlyMgr) List(id int64) ([]*models.NotificationPolicy, error) {
return []*models.NotificationPolicy{
{
ID: 1,
EventTypes: []string{
event.TopicPullArtifact,
event.TopicPushArtifact,
},
},
}, nil
}
func (f *fakedNotificationPlyMgr) Get(id int64) (*models.NotificationPolicy, error) {
switch id {
case 1:
return &models.NotificationPolicy{ID: 1, ProjectID: 1}, nil
case 2:
return &models.NotificationPolicy{ID: 2, ProjectID: 222}, nil
case 3:
return nil, errors.New("")
default:
return nil, nil
}
}
func (f *fakedNotificationPlyMgr) GetByNameAndProjectID(string, int64) (*models.NotificationPolicy, error) {
return nil, nil
}
func (f *fakedNotificationPlyMgr) Update(*models.NotificationPolicy) error {
return nil
}
func (f *fakedNotificationPlyMgr) Delete(int64) error {
return nil
}
func (f *fakedNotificationPlyMgr) Test(*models.NotificationPolicy) error {
return nil
}
func (f *fakedNotificationPlyMgr) GetRelatedPolices(int64, string) ([]*models.NotificationPolicy, error) {
return nil, nil
}
func TestNotificationPolicyAPI_List(t *testing.T) {
policyCtl := notification.PolicyMgr
defer func() {
notification.PolicyMgr = policyCtl
}()
notification.PolicyMgr = &fakedNotificationPlyMgr{}
cases := []*codeCheckingCase{
// 401
{
request: &testingRequest{
method: http.MethodGet,
url: "/api/projects/1/webhook/policies",
},
code: http.StatusUnauthorized,
},
// 403
{
request: &testingRequest{
method: http.MethodGet,
url: "/api/projects/1/webhook/policies",
credential: nonSysAdmin,
},
code: http.StatusForbidden,
},
// 404
{
request: &testingRequest{
method: http.MethodGet,
url: "/api/projects/123/webhook/policies",
credential: sysAdmin,
},
code: http.StatusNotFound,
},
// 200
{
request: &testingRequest{
method: http.MethodGet,
url: "/api/projects/1/webhook/policies",
credential: sysAdmin,
},
code: http.StatusOK,
},
}
runCodeCheckingCases(t, cases...)
}
func TestNotificationPolicyAPI_Post(t *testing.T) {
policyCtl := notification.PolicyMgr
defer func() {
notification.PolicyMgr = policyCtl
}()
notification.PolicyMgr = &fakedNotificationPlyMgr{}
cases := []*codeCheckingCase{
// 401
{
request: &testingRequest{
method: http.MethodPost,
url: "/api/projects/1/webhook/policies",
},
code: http.StatusUnauthorized,
},
// 403
{
request: &testingRequest{
method: http.MethodPost,
url: "/api/projects/1/webhook/policies",
credential: nonSysAdmin,
},
code: http.StatusForbidden,
},
// 400 invalid json body
{
request: &testingRequest{
method: http.MethodPost,
url: "/api/projects/1/webhook/policies",
credential: sysAdmin,
bodyJSON: "invalid json body",
},
code: http.StatusBadRequest,
},
// 400 empty targets
{
request: &testingRequest{
method: http.MethodPost,
url: "/api/projects/1/webhook/policies",
credential: sysAdmin,
bodyJSON: &models.NotificationPolicy{
Targets: []models.EventTarget{},
}},
code: http.StatusBadRequest,
},
// 400 invalid event target address
{
request: &testingRequest{
method: http.MethodPost,
url: "/api/projects/1/webhook/policies",
credential: sysAdmin,
bodyJSON: &models.NotificationPolicy{
EventTypes: []string{"PULL_ARTIFACT", "PUSH_ARTIFACT", "DELETE_ARTIFACT"},
Targets: []models.EventTarget{
{
Address: "tcp://127.0.0.1:8080",
},
},
}},
code: http.StatusBadRequest,
},
// 400 invalid event target type
{
request: &testingRequest{
method: http.MethodPost,
url: "/api/projects/1/webhook/policies",
credential: sysAdmin,
bodyJSON: &models.NotificationPolicy{
EventTypes: []string{"PULL_ARTIFACT", "PUSH_ARTIFACT", "DELETE_ARTIFACT"},
Targets: []models.EventTarget{
{
Type: "smn",
Address: "http://127.0.0.1:8080",
},
},
}},
code: http.StatusBadRequest,
},
// 400 invalid event type
{
request: &testingRequest{
method: http.MethodPost,
url: "/api/projects/1/webhook/policies",
credential: sysAdmin,
bodyJSON: &models.NotificationPolicy{
EventTypes: []string{"invalidType"},
Targets: []models.EventTarget{
{
Address: "tcp://127.0.0.1:8080",
},
},
}},
code: http.StatusBadRequest,
},
// 400 policy ID != 0
{
request: &testingRequest{
method: http.MethodPost,
url: "/api/projects/1/webhook/policies",
credential: sysAdmin,
bodyJSON: &models.NotificationPolicy{
ID: 111,
EventTypes: []string{"PULL_ARTIFACT", "PUSH_ARTIFACT", "DELETE_ARTIFACT"},
Targets: []models.EventTarget{
{
Type: "http",
Address: "http://10.173.32.58:9009",
AuthHeader: "xxxxxxxxx",
SkipCertVerify: true,
},
},
},
},
code: http.StatusBadRequest,
},
// 201
{
request: &testingRequest{
method: http.MethodPost,
url: "/api/projects/1/webhook/policies",
credential: sysAdmin,
bodyJSON: &models.NotificationPolicy{
EventTypes: []string{"PULL_ARTIFACT", "PUSH_ARTIFACT", "DELETE_ARTIFACT"},
Targets: []models.EventTarget{
{
Type: "http",
Address: "http://10.173.32.58:9009",
AuthHeader: "xxxxxxxxx",
SkipCertVerify: true,
},
},
},
},
code: http.StatusCreated,
},
}
runCodeCheckingCases(t, cases...)
}
func TestNotificationPolicyAPI_Get(t *testing.T) {
policyCtl := notification.PolicyMgr
defer func() {
notification.PolicyMgr = policyCtl
}()
notification.PolicyMgr = &fakedNotificationPlyMgr{}
cases := []*codeCheckingCase{
// 401
{
request: &testingRequest{
method: http.MethodGet,
url: "/api/projects/1/webhook/policies/111",
},
code: http.StatusUnauthorized,
},
// 403
{
request: &testingRequest{
method: http.MethodGet,
url: "/api/projects/1/webhook/policies/111",
credential: nonSysAdmin,
},
code: http.StatusForbidden,
},
// 404
{
request: &testingRequest{
method: http.MethodGet,
url: "/api/projects/1/webhook/policies/1234",
credential: sysAdmin,
},
code: http.StatusNotFound,
},
// 400 projectID not match with projectID in URL
{
request: &testingRequest{
method: http.MethodGet,
url: "/api/projects/1/webhook/policies/2",
credential: sysAdmin,
},
code: http.StatusBadRequest,
},
// 500
{
request: &testingRequest{
method: http.MethodGet,
url: "/api/projects/1/webhook/policies/3",
credential: sysAdmin,
},
code: http.StatusInternalServerError,
},
// 200
{
request: &testingRequest{
method: http.MethodGet,
url: "/api/projects/1/webhook/policies/1",
credential: sysAdmin,
},
code: http.StatusOK,
},
}
runCodeCheckingCases(t, cases...)
}
func TestNotificationPolicyAPI_Put(t *testing.T) {
policyCtl := notification.PolicyMgr
defer func() {
notification.PolicyMgr = policyCtl
}()
notification.PolicyMgr = &fakedNotificationPlyMgr{}
cases := []*codeCheckingCase{
// 401
{
request: &testingRequest{
method: http.MethodPut,
url: "/api/projects/1/webhook/policies/111",
},
code: http.StatusUnauthorized,
},
// 403
{
request: &testingRequest{
method: http.MethodPut,
url: "/api/projects/1/webhook/policies/111",
credential: nonSysAdmin,
},
code: http.StatusForbidden,
},
// 404
{
request: &testingRequest{
method: http.MethodPut,
url: "/api/projects/1/webhook/policies/1234",
credential: sysAdmin,
},
code: http.StatusNotFound,
},
// 400 invalid json body
{
request: &testingRequest{
method: http.MethodPut,
url: "/api/projects/1/webhook/policies/1",
credential: sysAdmin,
bodyJSON: "invalidJSONBody",
},
code: http.StatusBadRequest,
},
// 400 empty targets
{
request: &testingRequest{
method: http.MethodPut,
url: "/api/projects/1/webhook/policies/1",
credential: sysAdmin,
bodyJSON: &models.NotificationPolicy{
EventTypes: []string{"PULL_ARTIFACT", "PUSH_ARTIFACT", "DELETE_ARTIFACT"},
Targets: []models.EventTarget{},
}},
code: http.StatusBadRequest,
},
// 400 invalid event target address
{
request: &testingRequest{
method: http.MethodPut,
url: "/api/projects/1/webhook/policies/1",
credential: sysAdmin,
bodyJSON: &models.NotificationPolicy{
EventTypes: []string{"PULL_ARTIFACT", "PUSH_ARTIFACT", "DELETE_ARTIFACT"},
Targets: []models.EventTarget{
{
Address: "tcp://127.0.0.1:8080",
},
},
}},
code: http.StatusBadRequest,
},
// 400 invalid event target type
{
request: &testingRequest{
method: http.MethodPut,
url: "/api/projects/1/webhook/policies/1",
credential: sysAdmin,
bodyJSON: &models.NotificationPolicy{
EventTypes: []string{"PULL_ARTIFACT", "PUSH_ARTIFACT", "DELETE_ARTIFACT"},
Targets: []models.EventTarget{
{
Type: "smn",
Address: "http://127.0.0.1:8080",
},
},
}},
code: http.StatusBadRequest,
},
// 400 invalid event type
{
request: &testingRequest{
method: http.MethodPut,
url: "/api/projects/1/webhook/policies/1",
credential: sysAdmin,
bodyJSON: &models.NotificationPolicy{
EventTypes: []string{"invalidType"},
Targets: []models.EventTarget{
{
Address: "tcp://127.0.0.1:8080",
},
},
}},
code: http.StatusBadRequest,
},
// 200
{
request: &testingRequest{
method: http.MethodPut,
url: "/api/projects/1/webhook/policies/1",
credential: sysAdmin,
bodyJSON: &models.NotificationPolicy{
Name: "imagePolicyTest",
EventTypes: []string{"PULL_ARTIFACT", "PUSH_ARTIFACT", "DELETE_ARTIFACT"},
Targets: []models.EventTarget{
{
Type: "http",
Address: "http://10.173.32.58:9009",
AuthHeader: "xxxxxxxxx",
SkipCertVerify: true,
},
},
},
},
code: http.StatusOK,
},
}
runCodeCheckingCases(t, cases...)
}
func TestNotificationPolicyAPI_Test(t *testing.T) {
policyCtl := notification.PolicyMgr
defer func() {
notification.PolicyMgr = policyCtl
}()
notification.PolicyMgr = &fakedNotificationPlyMgr{}
cases := []*codeCheckingCase{
// 401
{
request: &testingRequest{
method: http.MethodPost,
url: "/api/projects/1/webhook/policies/test",
},
code: http.StatusUnauthorized,
},
// 403
{
request: &testingRequest{
method: http.MethodPost,
url: "/api/projects/1/webhook/policies/test",
credential: nonSysAdmin,
},
code: http.StatusForbidden,
},
// 404
{
request: &testingRequest{
method: http.MethodPost,
url: "/api/projects/123/webhook/policies/test",
credential: sysAdmin,
},
code: http.StatusNotFound,
},
// 400 invalid json body
{
request: &testingRequest{
method: http.MethodPost,
url: "/api/projects/1/webhook/policies/test",
credential: sysAdmin,
bodyJSON: 1234125,
},
code: http.StatusBadRequest,
},
// 200
{
request: &testingRequest{
method: http.MethodPost,
url: "/api/projects/1/webhook/policies/test",
credential: sysAdmin,
bodyJSON: &models.NotificationPolicy{
Targets: []models.EventTarget{
{
Type: "http",
Address: "http://10.173.32.58:9009",
AuthHeader: "xxxxxxxxx",
SkipCertVerify: true,
},
},
},
},
code: http.StatusOK,
},
}
runCodeCheckingCases(t, cases...)
}
func TestNotificationPolicyAPI_ListGroupByEventType(t *testing.T) {
policyCtl := notification.PolicyMgr
jobMgr := notification.JobMgr
defer func() {
notification.PolicyMgr = policyCtl
notification.JobMgr = jobMgr
}()
notification.PolicyMgr = &fakedNotificationPlyMgr{}
notification.JobMgr = &fakedNotificationJobMgr{}
cases := []*codeCheckingCase{
// 401
{
request: &testingRequest{
method: http.MethodGet,
url: "/api/projects/1/webhook/lasttrigger",
},
code: http.StatusUnauthorized,
},
// 403
{
request: &testingRequest{
method: http.MethodGet,
url: "/api/projects/1/webhook/lasttrigger",
credential: nonSysAdmin,
},
code: http.StatusForbidden,
},
// 404
{
request: &testingRequest{
method: http.MethodGet,
url: "/api/projects/123/webhook/lasttrigger",
credential: sysAdmin,
},
code: http.StatusNotFound,
},
// 200
{
request: &testingRequest{
method: http.MethodGet,
url: "/api/projects/1/webhook/lasttrigger",
credential: sysAdmin,
},
code: http.StatusOK,
},
}
runCodeCheckingCases(t, cases...)
}
func TestNotificationPolicyAPI_Delete(t *testing.T) {
policyCtl := notification.PolicyMgr
defer func() {
notification.PolicyMgr = policyCtl
}()
notification.PolicyMgr = &fakedNotificationPlyMgr{}
cases := []*codeCheckingCase{
// 401
{
request: &testingRequest{
method: http.MethodDelete,
url: "/api/projects/1/webhook/policies/111",
},
code: http.StatusUnauthorized,
},
// 403
{
request: &testingRequest{
method: http.MethodDelete,
url: "/api/projects/1/webhook/policies/111",
credential: nonSysAdmin,
},
code: http.StatusForbidden,
},
// 404
{
request: &testingRequest{
method: http.MethodDelete,
url: "/api/projects/1/webhook/policies/1234",
credential: sysAdmin,
},
code: http.StatusNotFound,
},
// 400 projectID not match
{
request: &testingRequest{
method: http.MethodDelete,
url: "/api/projects/1/webhook/policies/2",
credential: sysAdmin,
},
code: http.StatusBadRequest,
},
// 500 failed to get policy
{
request: &testingRequest{
method: http.MethodDelete,
url: "/api/projects/1/webhook/policies/3",
credential: sysAdmin,
},
code: http.StatusInternalServerError,
},
// 200
{
request: &testingRequest{
method: http.MethodDelete,
url: "/api/projects/1/webhook/policies/1",
credential: sysAdmin,
},
code: http.StatusOK,
},
}
runCodeCheckingCases(t, cases...)
}

View File

@ -16,6 +16,7 @@ package jobs
import (
"encoding/json"
"github.com/goharbor/harbor/src/lib/orm"
"time"
"github.com/goharbor/harbor/src/common/job"
@ -24,6 +25,7 @@ import (
jjob "github.com/goharbor/harbor/src/jobservice/job"
"github.com/goharbor/harbor/src/lib/log"
"github.com/goharbor/harbor/src/pkg/notification"
"github.com/goharbor/harbor/src/pkg/notification/job/model"
)
var statusMap = map[string]string{
@ -87,7 +89,7 @@ func (h *Handler) Prepare() {
// HandleNotificationJob handles the hook of notification job
func (h *Handler) HandleNotificationJob() {
log.Debugf("received notification job status update event: job-%d, status-%s", h.id, h.status)
if err := notification.JobMgr.Update(&models.NotificationJob{
if err := notification.JobMgr.Update(orm.Context(), &model.Job{
ID: h.id,
Status: h.status,
UpdateTime: time.Now(),

View File

@ -1,6 +1,7 @@
package hook
import (
"context"
"encoding/json"
"fmt"
"time"
@ -12,13 +13,13 @@ import (
"github.com/goharbor/harbor/src/core/utils"
"github.com/goharbor/harbor/src/lib/log"
"github.com/goharbor/harbor/src/pkg/notification/job"
"github.com/goharbor/harbor/src/pkg/notification/job/manager"
job_model "github.com/goharbor/harbor/src/pkg/notification/job/model"
"github.com/goharbor/harbor/src/pkg/notifier/model"
)
// Manager send hook
type Manager interface {
StartHook(*model.HookEvent, *models.JobData) error
StartHook(context.Context, *model.HookEvent, *models.JobData) error
}
// DefaultManager ...
@ -30,20 +31,20 @@ type DefaultManager struct {
// NewHookManager ...
func NewHookManager() *DefaultManager {
return &DefaultManager{
jobMgr: manager.NewDefaultManager(),
jobMgr: job.NewManager(),
client: utils.GetJobServiceClient(),
}
}
// StartHook create a notification job record in database, and submit it to jobservice
func (hm *DefaultManager) StartHook(event *model.HookEvent, data *models.JobData) error {
func (hm *DefaultManager) StartHook(ctx context.Context, event *model.HookEvent, data *models.JobData) error {
payload, err := json.Marshal(event.Payload)
if err != nil {
return err
}
t := time.Now()
id, err := hm.jobMgr.Create(&cModels.NotificationJob{
id, err := hm.jobMgr.Create(ctx, &job_model.Job{
PolicyID: event.PolicyID,
EventType: event.EventType,
NotifyType: event.Target.Type,
@ -64,7 +65,7 @@ func (hm *DefaultManager) StartHook(event *model.HookEvent, data *models.JobData
jobUUID, err := hm.client.SubmitJob(data)
if err != nil {
log.Errorf("failed to submit job with notification event: %v", err)
e := hm.jobMgr.Update(&cModels.NotificationJob{
e := hm.jobMgr.Update(ctx, &job_model.Job{
ID: id,
Status: cModels.JobError,
}, "Status")
@ -74,7 +75,7 @@ func (hm *DefaultManager) StartHook(event *model.HookEvent, data *models.JobData
return err
}
if err = hm.jobMgr.Update(&cModels.NotificationJob{
if err = hm.jobMgr.Update(ctx, &job_model.Job{
ID: id,
UUID: jobUUID,
}, "UUID"); err != nil {

View File

@ -0,0 +1,185 @@
package dao
import (
"context"
"fmt"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/lib/log"
"github.com/goharbor/harbor/src/lib/orm"
"github.com/goharbor/harbor/src/lib/q"
"github.com/goharbor/harbor/src/pkg/notification/job/model"
)
// DAO defines the interface to access the robot data model
type DAO interface {
// Create ...
Create(ctx context.Context, n *model.Job) (int64, error)
// Update ...
Update(ctx context.Context, n *model.Job, props ...string) error
// Get ...
Get(ctx context.Context, id int64) (*model.Job, error)
// Count ...
Count(ctx context.Context, query *q.Query) (total int64, err error)
// List ...
List(ctx context.Context, query *q.Query) ([]*model.Job, error)
// Delete ...
Delete(ctx context.Context, id int64) error
// GetLastTriggerJobsGroupByEventType ...
GetLastTriggerJobsGroupByEventType(ctx context.Context, policyID int64) ([]*model.Job, error)
// DeleteByPolicyID
DeleteByPolicyID(ctx context.Context, policyID int64) error
}
// New creates a default implementation for Dao
func New() DAO {
return &dao{}
}
type dao struct{}
// UpdateNotificationJob update notification job
func (d *dao) Update(ctx context.Context, job *model.Job, props ...string) error {
if job == nil {
return errors.New("nil job")
}
if job.ID == 0 {
return fmt.Errorf("notification job ID is empty")
}
ormer, err := orm.FromContext(ctx)
if err != nil {
return err
}
n, err := ormer.Update(job, props...)
if n == 0 {
return errors.NotFoundError(nil).WithMessage("notification %d not found", job.ID)
}
if err != nil {
return err
}
return nil
}
// Create insert new notification job to DB
func (d *dao) Create(ctx context.Context, job *model.Job) (int64, error) {
if job == nil {
return 0, errors.New("nil job")
}
ormer, err := orm.FromContext(ctx)
if err != nil {
return 0, err
}
if len(job.Status) == 0 {
job.Status = models.JobPending
}
return ormer.Insert(job)
}
// Get ...
func (d *dao) Get(ctx context.Context, id int64) (*model.Job, error) {
ormer, err := orm.FromContext(ctx)
if err != nil {
return nil, err
}
j := &model.Job{
ID: id,
}
if err := ormer.Read(j); err != nil {
if e := orm.AsNotFoundError(err, "notificationJob %d not found", id); e != nil {
err = e
}
return nil, err
}
return j, nil
}
// Count ...
func (d *dao) Count(ctx context.Context, query *q.Query) (int64, error) {
qs, err := orm.QuerySetterForCount(ctx, &model.Job{}, query)
if err != nil {
return 0, err
}
return qs.Count()
}
// List ...
func (d *dao) List(ctx context.Context, query *q.Query) ([]*model.Job, error) {
jobs := []*model.Job{}
qs, err := orm.QuerySetter(ctx, &model.Job{}, query)
if err != nil {
return nil, err
}
if _, err = qs.All(&jobs); err != nil {
return nil, err
}
return jobs, nil
}
// GetLastTriggerJobsGroupByEventType get notification jobs info of policy, including event type and last trigger time
func (d *dao) GetLastTriggerJobsGroupByEventType(ctx context.Context, policyID int64) ([]*model.Job, error) {
ormer, err := orm.FromContext(ctx)
if err != nil {
return nil, err
}
// get jobs last triggered(created) group by event_type. postgres group by usage reference:
// https://stackoverflow.com/questions/13325583/postgresql-max-and-group-by
sql := `select distinct on (event_type) event_type, id, creation_time, status, notify_type, job_uuid, update_time,
creation_time, job_detail from notification_job where policy_id = ?
order by event_type, id desc, creation_time, status, notify_type, job_uuid, update_time, creation_time, job_detail`
jobs := []*model.Job{}
_, err = ormer.Raw(sql, policyID).QueryRows(&jobs)
if err != nil {
log.Errorf("query last trigger info group by event type failed: %v", err)
return nil, err
}
return jobs, nil
}
func (d *dao) Delete(ctx context.Context, id int64) error {
ormer, err := orm.FromContext(ctx)
if err != nil {
return err
}
n, err := ormer.Delete(&model.Job{
ID: id,
})
if err != nil {
return err
}
if n == 0 {
return errors.NotFoundError(nil).WithMessage("notificationJob %d not found", id)
}
return nil
}
// DeleteByPolicyID ...
func (d *dao) DeleteByPolicyID(ctx context.Context, policyID int64) error {
qs, err := orm.QuerySetter(ctx, &model.Job{}, &q.Query{
Keywords: map[string]interface{}{
"policy_id": policyID,
},
})
if err != nil {
return err
}
n, err := qs.Delete()
if err != nil {
return err
}
if n == 0 {
return errors.NotFoundError(nil).WithMessage("notificationJob %d not found", policyID)
}
return nil
}

View File

@ -0,0 +1,187 @@
package dao
import (
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/lib/orm"
"github.com/goharbor/harbor/src/lib/q"
"github.com/goharbor/harbor/src/pkg/notification/job/model"
htesting "github.com/goharbor/harbor/src/testing"
"github.com/stretchr/testify/suite"
"testing"
)
var (
testJob1 = &model.Job{
PolicyID: 1111,
EventType: "pushImage",
NotifyType: "http",
Status: "pending",
JobDetail: "{\"type\":\"pushImage\",\"occur_at\":1563536782,\"event_data\":{\"resources\":[{\"digest\":\"sha256:bf1684a6e3676389ec861c602e97f27b03f14178e5bc3f70dce198f9f160cce9\",\"tag\":\"v1.0\",\"resource_url\":\"10.194.32.23/myproj/alpine:v1.0\"}],\"repository\":{\"date_created\":1563505587,\"name\":\"alpine\",\"namespace\":\"myproj\",\"repo_full_name\":\"myproj/alpine\",\"repo_type\":\"private\"}},\"operator\":\"admin\"}",
UUID: "00000000",
}
testJob2 = &model.Job{
PolicyID: 111,
EventType: "pullImage",
NotifyType: "http",
Status: "",
JobDetail: "{\"type\":\"pushImage\",\"occur_at\":1563537782,\"event_data\":{\"resources\":[{\"digest\":\"sha256:bf1684a6e3676389ec861c602e97f27b03f14178e5bc3f70dce198f9f160cce9\",\"tag\":\"v1.0\",\"resource_url\":\"10.194.32.23/myproj/alpine:v1.0\"}],\"repository\":{\"date_created\":1563505587,\"name\":\"alpine\",\"namespace\":\"myproj\",\"repo_full_name\":\"myproj/alpine\",\"repo_type\":\"private\"}},\"operator\":\"admin\"}",
UUID: "00000000",
}
testJob3 = &model.Job{
PolicyID: 111,
EventType: "deleteImage",
NotifyType: "http",
Status: "pending",
JobDetail: "{\"type\":\"pushImage\",\"occur_at\":1563538782,\"event_data\":{\"resources\":[{\"digest\":\"sha256:bf1684a6e3676389ec861c602e97f27b03f14178e5bc3f70dce198f9f160cce9\",\"tag\":\"v1.0\",\"resource_url\":\"10.194.32.23/myproj/alpine:v1.0\"}],\"repository\":{\"date_created\":1563505587,\"name\":\"alpine\",\"namespace\":\"myproj\",\"repo_full_name\":\"myproj/alpine\",\"repo_type\":\"private\"}},\"operator\":\"admin\"}",
UUID: "00000000",
}
)
type DaoTestSuite struct {
htesting.Suite
dao DAO
jobID1 int64
jobID2 int64
jobID3 int64
}
func (suite *DaoTestSuite) SetupSuite() {
suite.Suite.SetupSuite()
suite.dao = New()
suite.Suite.ClearTables = []string{"notification_job"}
suite.jobs()
}
func (suite *DaoTestSuite) jobs() {
var err error
suite.jobID1, err = suite.dao.Create(orm.Context(), testJob1)
suite.Nil(err)
suite.jobID2, err = suite.dao.Create(orm.Context(), testJob2)
suite.Nil(err)
suite.jobID3, err = suite.dao.Create(orm.Context(), testJob3)
suite.Nil(err)
}
func (suite *DaoTestSuite) TestCreate() {
_, err := suite.dao.Create(orm.Context(), nil)
suite.NotNil(err)
}
func (suite *DaoTestSuite) TestDelete() {
err := suite.dao.Delete(orm.Context(), 1234)
suite.Require().NotNil(err)
suite.True(errors.IsErr(err, errors.NotFoundCode))
err = suite.dao.Delete(orm.Context(), suite.jobID2)
suite.Nil(err)
}
func (suite *DaoTestSuite) TestList() {
jobs, err := suite.dao.List(orm.Context(), &q.Query{
Keywords: map[string]interface{}{
"EventType": "pushImage",
},
})
suite.Require().Nil(err)
suite.Equal(len(jobs), 1)
suite.Equal(suite.jobID1, jobs[0].ID)
}
func (suite *DaoTestSuite) TestGet() {
_, err := suite.dao.Get(orm.Context(), 1234)
suite.Require().NotNil(err)
suite.True(errors.IsErr(err, errors.NotFoundCode))
id, err := suite.dao.Create(orm.Context(), &model.Job{
PolicyID: 2222,
EventType: "pushChart",
NotifyType: "http",
Status: "pending",
JobDetail: "{\"type\":\"pushImage\",\"occur_at\":1563536782,\"event_data\":{\"resources\":[{\"digest\":\"sha256:bf1684a6e3676389ec861c602e97f27b03f14178e5bc3f70dce198f9f160cce9\",\"tag\":\"v1.0\",\"resource_url\":\"10.194.32.23/myproj/alpine:v1.0\"}],\"repository\":{\"date_created\":1563505587,\"name\":\"alpine\",\"namespace\":\"myproj\",\"repo_full_name\":\"myproj/alpine\",\"repo_type\":\"private\"}},\"operator\":\"admin\"}",
UUID: "00000000",
})
suite.Nil(err)
r, err := suite.dao.Get(orm.Context(), id)
suite.Nil(err)
suite.Equal("pushChart", r.EventType)
}
func (suite *DaoTestSuite) TestUpdate() {
j := &model.Job{
ID: suite.jobID1,
Status: "success",
}
err := suite.dao.Update(orm.Context(), j)
suite.Nil(err)
r1, err := suite.dao.Get(orm.Context(), j.ID)
suite.Equal("success", r1.Status)
}
func (suite *DaoTestSuite) TestCount() {
// nil query
total, err := suite.dao.Count(orm.Context(), nil)
suite.Nil(err)
suite.True(total > 0)
// query by name
total, err = suite.dao.Count(orm.Context(), &q.Query{
Keywords: map[string]interface{}{
"EventType": "deleteImage",
},
})
suite.Nil(err)
suite.Equal(int64(1), total)
}
func (suite *DaoTestSuite) TestDeleteByPolicyID() {
jobs, err := suite.dao.List(orm.Context(), &q.Query{
Keywords: map[string]interface{}{
"PolicyID": 111,
},
})
suite.True(len(jobs) > 0)
err = suite.dao.DeleteByPolicyID(orm.Context(), 111)
suite.Nil(err)
jobs, err = suite.dao.List(orm.Context(), &q.Query{
Keywords: map[string]interface{}{
"PolicyID": 111,
},
})
suite.Equal(0, len(jobs))
}
func (suite *DaoTestSuite) TestGetLastTriggerJobsGroupByEventType() {
_, err := suite.dao.Create(orm.Context(), &model.Job{
PolicyID: 3333,
EventType: "pushChart",
NotifyType: "http",
Status: "pending",
JobDetail: "{\"type\":\"pushImage\",\"occur_at\":1563536782,\"event_data\":{\"resources\":[{\"digest\":\"sha256:bf1684a6e3676389ec861c602e97f27b03f14178e5bc3f70dce198f9f160cce9\",\"tag\":\"v1.0\",\"resource_url\":\"10.194.32.23/myproj/alpine:v1.0\"}],\"repository\":{\"date_created\":1563505587,\"name\":\"alpine\",\"namespace\":\"myproj\",\"repo_full_name\":\"myproj/alpine\",\"repo_type\":\"private\"}},\"operator\":\"admin\"}",
UUID: "00000000",
})
suite.Nil(err)
_, err = suite.dao.Create(orm.Context(), &model.Job{
PolicyID: 3333,
EventType: "pullChart",
NotifyType: "http",
Status: "pending",
JobDetail: "{\"type\":\"pushImage\",\"occur_at\":1563536782,\"event_data\":{\"resources\":[{\"digest\":\"sha256:bf1684a6e3676389ec861c602e97f27b03f14178e5bc3f70dce198f9f160cce9\",\"tag\":\"v1.0\",\"resource_url\":\"10.194.32.23/myproj/alpine:v1.0\"}],\"repository\":{\"date_created\":1563505587,\"name\":\"alpine\",\"namespace\":\"myproj\",\"repo_full_name\":\"myproj/alpine\",\"repo_type\":\"private\"}},\"operator\":\"admin\"}",
UUID: "00000000",
})
suite.Nil(err)
jobs, err := suite.dao.GetLastTriggerJobsGroupByEventType(orm.Context(), 3333)
suite.Nil(err)
suite.Equal(2, len(jobs))
}
func TestDaoTestSuite(t *testing.T) {
suite.Run(t, &DaoTestSuite{})
}

View File

@ -1,20 +1,69 @@
package job
import (
"github.com/goharbor/harbor/src/common/models"
"context"
"github.com/goharbor/harbor/src/lib/q"
"github.com/goharbor/harbor/src/pkg/notification/job/dao"
"github.com/goharbor/harbor/src/pkg/notification/job/model"
)
var (
// Mgr is a global variable for the default notification job
Mgr = NewManager()
)
// Manager manages notification jobs recorded in database
type Manager interface {
// Create create a notification job
Create(job *models.NotificationJob) (int64, error)
Create(ctx context.Context, job *model.Job) (int64, error)
// List list notification jobs
List(...*models.NotificationJobQuery) (int64, []*models.NotificationJob, error)
List(ctx context.Context, query *q.Query) ([]*model.Job, error)
// Update update notification job
Update(job *models.NotificationJob, props ...string) error
Update(ctx context.Context, job *model.Job, props ...string) error
// ListJobsGroupByEventType lists last triggered jobs group by event type
ListJobsGroupByEventType(policyID int64) ([]*models.NotificationJob, error)
ListJobsGroupByEventType(ctx context.Context, policyID int64) ([]*model.Job, error)
// Count ...
Count(ctx context.Context, query *q.Query) (total int64, err error)
}
var _ Manager = &manager{}
type manager struct {
dao dao.DAO
}
// NewManager ...
func NewManager() Manager {
return &manager{
dao: dao.New(),
}
}
// Create ...
func (d *manager) Create(ctx context.Context, job *model.Job) (int64, error) {
return d.dao.Create(ctx, job)
}
// Count ...
func (d *manager) Count(ctx context.Context, query *q.Query) (int64, error) {
return d.dao.Count(ctx, query)
}
// List ...
func (d *manager) List(ctx context.Context, query *q.Query) ([]*model.Job, error) {
return d.dao.List(ctx, query)
}
// Update ...
func (d *manager) Update(ctx context.Context, job *model.Job, props ...string) error {
return d.dao.Update(ctx, job, props...)
}
// ListJobsGroupByEventType lists last triggered jobs group by event type
func (d *manager) ListJobsGroupByEventType(ctx context.Context, policyID int64) ([]*model.Job, error) {
return d.dao.GetLastTriggerJobsGroupByEventType(ctx, policyID)
}

View File

@ -1,55 +0,0 @@
package manager
import (
"fmt"
"github.com/goharbor/harbor/src/common/dao/notification"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/pkg/notification/job"
)
// DefaultManager ..
type DefaultManager struct {
}
// NewDefaultManager ...
func NewDefaultManager() job.Manager {
return &DefaultManager{}
}
// Create ...
func (d *DefaultManager) Create(job *models.NotificationJob) (int64, error) {
return notification.AddNotificationJob(job)
}
// List ...
func (d *DefaultManager) List(query ...*models.NotificationJobQuery) (int64, []*models.NotificationJob, error) {
total, err := notification.GetTotalCountOfNotificationJobs(query...)
if err != nil {
return 0, nil, err
}
executions, err := notification.GetNotificationJobs(query...)
if err != nil {
return 0, nil, err
}
return total, executions, nil
}
// Update ...
func (d *DefaultManager) Update(job *models.NotificationJob, props ...string) error {
n, err := notification.UpdateNotificationJob(job, props...)
if err != nil {
return err
}
if n == 0 {
return fmt.Errorf("execution %d not found", job.ID)
}
return nil
}
// ListJobsGroupByEventType lists last triggered jobs group by event type
func (d *DefaultManager) ListJobsGroupByEventType(policyID int64) ([]*models.NotificationJob, error) {
return notification.GetLastTriggerJobsGroupByEventType(policyID)
}

View File

@ -1,22 +0,0 @@
package manager
import (
"reflect"
"testing"
)
func TestNewDefaultManger(t *testing.T) {
tests := []struct {
name string
want *DefaultManager
}{
{want: &DefaultManager{}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := NewDefaultManager(); !reflect.DeepEqual(got, tt.want) {
t.Errorf("NewDefaultManager() = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -0,0 +1,81 @@
package job
import (
"context"
"github.com/goharbor/harbor/src/pkg/notification/job/model"
"github.com/goharbor/harbor/src/testing/mock"
"github.com/goharbor/harbor/src/testing/pkg/notification/job/dao"
"github.com/stretchr/testify/suite"
"testing"
)
type managerTestSuite struct {
suite.Suite
mgr *manager
dao *dao.DAO
}
func (m *managerTestSuite) SetupTest() {
m.dao = &dao.DAO{}
m.mgr = &manager{
dao: m.dao,
}
}
func (m *managerTestSuite) TestCreate() {
m.dao.On("Create", mock.Anything, mock.Anything).Return(int64(1), nil)
_, err := m.mgr.Create(context.Background(), &model.Job{})
m.Nil(err)
m.dao.AssertExpectations(m.T())
}
func (m *managerTestSuite) TestUpdate() {
m.dao.On("Update", mock.Anything, mock.Anything).Return(nil)
err := m.mgr.Update(context.Background(), &model.Job{})
m.Nil(err)
m.dao.AssertExpectations(m.T())
}
func (m *managerTestSuite) TestCount() {
m.dao.On("Count", mock.Anything, mock.Anything).Return(int64(1), nil)
n, err := m.mgr.Count(context.Background(), nil)
m.Nil(err)
m.Equal(int64(1), n)
m.dao.AssertExpectations(m.T())
}
func (m *managerTestSuite) TestList() {
m.dao.On("List", mock.Anything, mock.Anything).Return([]*model.Job{
{
ID: 1,
EventType: "test_job",
},
}, nil)
rpers, err := m.mgr.List(context.Background(), nil)
m.Nil(err)
m.Equal(1, len(rpers))
m.dao.AssertExpectations(m.T())
}
func (m *managerTestSuite) TestListJobsGroupByEventType() {
m.dao.On("GetLastTriggerJobsGroupByEventType", mock.Anything, mock.Anything).Return([]*model.Job{
{
ID: 1,
EventType: "test_job",
PolicyID: 1,
},
{
ID: 2,
EventType: "test_job",
PolicyID: 1,
},
}, nil)
rpers, err := m.mgr.ListJobsGroupByEventType(context.Background(), 1)
m.Nil(err)
m.Equal(2, len(rpers))
m.dao.AssertExpectations(m.T())
}
func TestManager(t *testing.T) {
suite.Run(t, &managerTestSuite{})
}

View File

@ -0,0 +1,28 @@
package model
import (
"github.com/astaxie/beego/orm"
"time"
)
func init() {
orm.RegisterModel(&Job{})
}
// Job is the model for a notification job
type Job struct {
ID int64 `orm:"pk;auto;column(id)" json:"id"`
PolicyID int64 `orm:"column(policy_id)" json:"policy_id"`
EventType string `orm:"column(event_type)" json:"event_type"`
NotifyType string `orm:"column(notify_type)" json:"notify_type"`
Status string `orm:"column(status)" json:"status"`
JobDetail string `orm:"column(job_detail)" json:"job_detail"`
UUID string `orm:"column(job_uuid)" json:"-"`
CreationTime time.Time `orm:"column(creation_time);auto_now_add" json:"creation_time"`
UpdateTime time.Time `orm:"column(update_time);auto_now" json:"update_time" sort:"default:desc"`
}
// TableName set table name for ORM.
func (j *Job) TableName() string {
return "notification_job"
}

View File

@ -3,15 +3,14 @@ package notification
import (
"container/list"
"context"
"github.com/goharbor/harbor/src/controller/event"
notifier_model "github.com/goharbor/harbor/src/pkg/notifier/model"
"github.com/goharbor/harbor/src/lib/log"
"github.com/goharbor/harbor/src/pkg/notification/hook"
"github.com/goharbor/harbor/src/pkg/notification/job"
jobMgr "github.com/goharbor/harbor/src/pkg/notification/job/manager"
"github.com/goharbor/harbor/src/pkg/notification/policy"
"github.com/goharbor/harbor/src/pkg/notification/policy/manager"
n_event "github.com/goharbor/harbor/src/pkg/notifier/event"
"github.com/goharbor/harbor/src/pkg/notifier/model"
)
var (
@ -34,20 +33,40 @@ var (
// Init ...
func Init() {
// init notification policy manager
PolicyMgr = manager.NewDefaultManger()
PolicyMgr = policy.Mgr
// init hook manager
HookManager = hook.NewHookManager()
// init notification job manager
JobMgr = jobMgr.NewDefaultManager()
JobMgr = job.Mgr
SupportedNotifyTypes = make(map[string]struct{})
initSupportedNotifyType(model.NotifyTypeHTTP, model.NotifyTypeSlack)
initSupportedNotifyType()
log.Info("notification initialization completed")
}
func initSupportedNotifyType(notifyTypes ...string) {
func initSupportedNotifyType() {
SupportedEventTypes = make(map[string]struct{}, 0)
SupportedNotifyTypes = make(map[string]struct{}, 0)
eventTypes := []string{
event.TopicPushArtifact,
event.TopicPullArtifact,
event.TopicDeleteArtifact,
event.TopicUploadChart,
event.TopicDeleteChart,
event.TopicDownloadChart,
event.TopicQuotaExceed,
event.TopicQuotaWarning,
event.TopicScanningFailed,
event.TopicScanningCompleted,
event.TopicReplication,
event.TopicTagRetention,
}
for _, eventType := range eventTypes {
SupportedEventTypes[eventType] = struct{}{}
}
notifyTypes := []string{notifier_model.NotifyTypeHTTP, notifier_model.NotifyTypeSlack}
for _, notifyType := range notifyTypes {
SupportedNotifyTypes[notifyType] = struct{}{}
}

View File

@ -0,0 +1,141 @@
package dao
import (
"context"
"fmt"
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/lib/orm"
"github.com/goharbor/harbor/src/lib/q"
"github.com/goharbor/harbor/src/pkg/notification/policy/model"
)
// DAO defines the interface to access the notification policy data model
type DAO interface {
// Create ...
Create(ctx context.Context, n *model.Policy) (int64, error)
// Update ...
Update(ctx context.Context, n *model.Policy) error
// Get ...
Get(ctx context.Context, id int64) (*model.Policy, error)
// Count returns the total count of robots according to the query
Count(ctx context.Context, query *q.Query) (total int64, err error)
// List ...
List(ctx context.Context, query *q.Query) ([]*model.Policy, error)
// Delete ...
Delete(ctx context.Context, id int64) error
}
// New creates a default implementation for Dao
func New() DAO {
return &dao{}
}
type dao struct{}
// Get ...
func (d *dao) Get(ctx context.Context, id int64) (*model.Policy, error) {
ormer, err := orm.FromContext(ctx)
if err != nil {
return nil, err
}
j := &model.Policy{
ID: id,
}
if err := ormer.Read(j); err != nil {
if e := orm.AsNotFoundError(err, "notificationPolicy %d not found", id); e != nil {
err = e
}
return nil, err
}
return j, nil
}
// Create ...
func (d *dao) Create(ctx context.Context, policy *model.Policy) (int64, error) {
if policy == nil {
return 0, errors.New("nil policy")
}
ormer, err := orm.FromContext(ctx)
if err != nil {
return 0, err
}
id, err := ormer.Insert(policy)
if err != nil {
if e := orm.AsConflictError(err, "notification policy named %s already exists", policy.Name); e != nil {
err = e
return id, err
}
err = fmt.Errorf("failed to create the notification policy: %v", err)
return id, err
}
return id, err
}
// Update ...
func (d *dao) Update(ctx context.Context, policy *model.Policy) error {
if policy == nil {
return errors.New("nil policy")
}
ormer, err := orm.FromContext(ctx)
if err != nil {
return err
}
n, err := ormer.Update(policy)
if n == 0 {
if e := orm.AsConflictError(err, "notification policy named %s already exists", policy.Name); e != nil {
err = e
}
return err
}
if err != nil {
return err
}
return nil
}
// Count ...
func (d *dao) Count(ctx context.Context, query *q.Query) (int64, error) {
qs, err := orm.QuerySetterForCount(ctx, &model.Policy{}, query)
if err != nil {
return 0, err
}
return qs.Count()
}
// List ...
func (d *dao) List(ctx context.Context, query *q.Query) ([]*model.Policy, error) {
policies := []*model.Policy{}
qs, err := orm.QuerySetter(ctx, &model.Policy{}, query)
if err != nil {
return nil, err
}
if _, err = qs.All(&policies); err != nil {
return nil, err
}
return policies, nil
}
// Delete delete notification policy by id
func (d *dao) Delete(ctx context.Context, id int64) error {
ormer, err := orm.FromContext(ctx)
if err != nil {
return err
}
n, err := ormer.Delete(&model.Policy{
ID: id,
})
if err != nil {
return err
}
if n == 0 {
return errors.NotFoundError(nil).WithMessage("notificationPolicy %d not found", id)
}
return nil
}

View File

@ -0,0 +1,162 @@
package dao
import (
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/lib/orm"
"github.com/goharbor/harbor/src/lib/q"
"github.com/goharbor/harbor/src/pkg/notification/policy/model"
htesting "github.com/goharbor/harbor/src/testing"
"github.com/stretchr/testify/suite"
"testing"
"time"
)
var (
testPly1 = &model.Policy{
Name: "webhook test policy1",
Description: "webhook test policy1 description",
ProjectID: 111,
TargetsDB: "[{\"type\":\"http\",\"address\":\"http://10.173.32.58:9009\",\"token\":\"xxxxxxxxx\",\"skip_cert_verify\":true}]",
EventTypesDB: "[\"pushImage\",\"pullImage\",\"deleteImage\",\"uploadChart\",\"deleteChart\",\"downloadChart\",\"scanningFailed\",\"scanningCompleted\"]",
Creator: "no one",
CreationTime: time.Now(),
UpdateTime: time.Now(),
Enabled: true,
}
)
var (
testPly2 = &model.Policy{
Name: "webhook test policy2",
Description: "webhook test policy2 description",
ProjectID: 222,
TargetsDB: "[{\"type\":\"http\",\"address\":\"http://10.173.32.58:9009\",\"token\":\"xxxxxxxxx\",\"skip_cert_verify\":true}]",
EventTypesDB: "[\"pushImage\",\"pullImage\",\"deleteImage\",\"uploadChart\",\"deleteChart\",\"downloadChart\",\"scanningFailed\",\"scanningCompleted\"]",
Creator: "no one",
CreationTime: time.Now(),
UpdateTime: time.Now(),
Enabled: true,
}
)
var (
testPly3 = &model.Policy{
Name: "webhook test policy3",
Description: "webhook test policy3 description",
ProjectID: 333,
TargetsDB: "[{\"type\":\"http\",\"address\":\"http://10.173.32.58:9009\",\"token\":\"xxxxxxxxx\",\"skip_cert_verify\":true}]",
EventTypesDB: "[\"pushImage\",\"pullImage\",\"deleteImage\",\"uploadChart\",\"deleteChart\",\"downloadChart\",\"scanningFailed\",\"scanningCompleted\"]",
Creator: "no one",
CreationTime: time.Now(),
UpdateTime: time.Now(),
Enabled: true,
}
)
type DaoTestSuite struct {
htesting.Suite
dao DAO
jobID1 int64
jobID2 int64
jobID3 int64
}
func (suite *DaoTestSuite) SetupSuite() {
suite.Suite.SetupSuite()
suite.dao = New()
suite.Suite.ClearTables = []string{"notification_policy"}
suite.policies()
}
func (suite *DaoTestSuite) policies() {
var err error
suite.jobID1, err = suite.dao.Create(orm.Context(), testPly1)
suite.Nil(err)
suite.jobID2, err = suite.dao.Create(orm.Context(), testPly2)
suite.Nil(err)
suite.jobID3, err = suite.dao.Create(orm.Context(), testPly3)
suite.Nil(err)
}
func (suite *DaoTestSuite) TestCreate() {
_, err := suite.dao.Create(orm.Context(), nil)
suite.NotNil(err)
}
func (suite *DaoTestSuite) TestDelete() {
err := suite.dao.Delete(orm.Context(), 1234)
suite.Require().NotNil(err)
suite.True(errors.IsErr(err, errors.NotFoundCode))
err = suite.dao.Delete(orm.Context(), suite.jobID2)
suite.Nil(err)
}
func (suite *DaoTestSuite) TestList() {
jobs, err := suite.dao.List(orm.Context(), &q.Query{
Keywords: map[string]interface{}{
"ProjectID": 333,
},
})
suite.Require().Nil(err)
suite.Equal(len(jobs), 1)
suite.Equal(suite.jobID3, jobs[0].ID)
}
func (suite *DaoTestSuite) TestGet() {
_, err := suite.dao.Get(orm.Context(), 1234)
suite.Require().NotNil(err)
suite.True(errors.IsErr(err, errors.NotFoundCode))
id, err := suite.dao.Create(orm.Context(), &model.Policy{
Name: "webhook test policy4",
Description: "webhook test policy4 description",
ProjectID: 444,
TargetsDB: "[{\"type\":\"http\",\"address\":\"http://10.173.32.58:9009\",\"token\":\"xxxxxxxxx\",\"skip_cert_verify\":true}]",
EventTypesDB: "[\"pushImage\",\"pullImage\",\"deleteImage\",\"uploadChart\",\"deleteChart\",\"downloadChart\",\"scanningFailed\",\"scanningCompleted\"]",
Creator: "no one",
CreationTime: time.Now(),
UpdateTime: time.Now(),
Enabled: true,
})
suite.Nil(err)
r, err := suite.dao.Get(orm.Context(), id)
suite.Nil(err)
suite.Equal("webhook test policy4", r.Name)
}
func (suite *DaoTestSuite) TestUpdate() {
j := &model.Policy{
ID: suite.jobID1,
Enabled: false,
}
err := suite.dao.Update(orm.Context(), j)
suite.Nil(err)
r1, err := suite.dao.Get(orm.Context(), j.ID)
suite.False(r1.Enabled)
}
func (suite *DaoTestSuite) TestCount() {
// nil query
total, err := suite.dao.Count(orm.Context(), nil)
suite.Nil(err)
suite.True(total > 0)
total, err = suite.dao.Count(orm.Context(), &q.Query{
Keywords: map[string]interface{}{
"ProjectID": 111,
},
})
suite.Nil(err)
suite.Equal(int64(1), total)
}
func TestDaoTestSuite(t *testing.T) {
suite.Run(t, &DaoTestSuite{})
}

View File

@ -1,25 +1,197 @@
package policy
import (
"github.com/goharbor/harbor/src/common/models"
"context"
"fmt"
commonhttp "github.com/goharbor/harbor/src/common/http"
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/lib/log"
"github.com/goharbor/harbor/src/lib/q"
"github.com/goharbor/harbor/src/pkg/notification/policy/dao"
"github.com/goharbor/harbor/src/pkg/notification/policy/model"
notifier_model "github.com/goharbor/harbor/src/pkg/notifier/model"
"net/http"
"time"
)
var (
// Mgr is a global variable for the default notification policies
Mgr = NewManager()
)
// Manager manages the notification policies
type Manager interface {
// Create new policy
Create(*models.NotificationPolicy) (int64, error)
Create(ctx context.Context, policy *model.Policy) (int64, error)
// List the policies, returns the policy list and error
List(int64) ([]*models.NotificationPolicy, error)
List(ctx context.Context, query *q.Query) ([]*model.Policy, error)
// Count the policies, returns the policy count and error
Count(ctx context.Context, query *q.Query) (int64, error)
// Get policy with specified ID
Get(int64) (*models.NotificationPolicy, error)
Get(ctx context.Context, id int64) (*model.Policy, error)
// GetByNameAndProjectID get policy by the name and projectID
GetByNameAndProjectID(string, int64) (*models.NotificationPolicy, error)
GetByNameAndProjectID(ctx context.Context, name string, projectID int64) (*model.Policy, error)
// Update the specified policy
Update(*models.NotificationPolicy) error
Update(ctx context.Context, policy *model.Policy) error
// Delete the specified policy
Delete(int64) error
Delete(ctx context.Context, policyID int64) error
// Test the specified policy
Test(*models.NotificationPolicy) error
Test(policy *model.Policy) error
// GetRelatedPolices get event type related policies in project
GetRelatedPolices(int64, string) ([]*models.NotificationPolicy, error)
GetRelatedPolices(ctx context.Context, projectID int64, eventType string) ([]*model.Policy, error)
}
var _ Manager = &manager{}
type manager struct {
dao dao.DAO
}
// NewManager ...
func NewManager() Manager {
return &manager{
dao: dao.New(),
}
}
// Create notification policy
func (m *manager) Create(ctx context.Context, policy *model.Policy) (int64, error) {
t := time.Now()
policy.CreationTime = t
policy.UpdateTime = t
err := policy.ConvertToDBModel()
if err != nil {
return 0, err
}
return m.dao.Create(ctx, policy)
}
// List the notification policies, returns the policy list and error
func (m *manager) List(ctx context.Context, query *q.Query) ([]*model.Policy, error) {
policies := []*model.Policy{}
persisPolicies, err := m.dao.List(ctx, query)
if err != nil {
return nil, err
}
for _, policy := range persisPolicies {
err := policy.ConvertFromDBModel()
if err != nil {
return nil, err
}
policies = append(policies, policy)
}
return policies, nil
}
// Count the notification policies, returns the count and error
func (m *manager) Count(ctx context.Context, query *q.Query) (int64, error) {
return m.dao.Count(ctx, query)
}
// Get notification policy with specified ID
func (m *manager) Get(ctx context.Context, id int64) (*model.Policy, error) {
policy, err := m.dao.Get(ctx, id)
if err != nil {
return nil, err
}
if policy == nil {
return nil, nil
}
if err := policy.ConvertFromDBModel(); err != nil {
return nil, err
}
return policy, err
}
// GetByNameAndProjectID notification policy by the name and projectID
func (m *manager) GetByNameAndProjectID(ctx context.Context, name string, projectID int64) (*model.Policy, error) {
query := q.New(q.KeyWords{"name": name, "project_id": projectID})
policies, err := m.dao.List(ctx, query)
if err != nil {
return nil, err
}
if len(policies) == 0 {
return nil, errors.New(nil).WithCode(errors.NotFoundCode).WithMessage("no notification policy found")
}
policy := policies[0]
if err := policy.ConvertFromDBModel(); err != nil {
return nil, err
}
return policy, err
}
// Update the specified notification policy
func (m *manager) Update(ctx context.Context, policy *model.Policy) error {
policy.UpdateTime = time.Now()
err := policy.ConvertToDBModel()
if err != nil {
return err
}
return m.dao.Update(ctx, policy)
}
// Delete the specified notification policy
func (m *manager) Delete(ctx context.Context, policyID int64) error {
return m.dao.Delete(ctx, policyID)
}
// Test the specified notification policy, just test for network connection without request body
func (m *manager) Test(policy *model.Policy) error {
for _, target := range policy.Targets {
switch target.Type {
case notifier_model.NotifyTypeHTTP, notifier_model.NotifyTypeSlack:
return m.policyHTTPTest(target.Address, target.SkipCertVerify)
default:
return fmt.Errorf("invalid policy target type: %s", target.Type)
}
}
return nil
}
func (m *manager) policyHTTPTest(address string, skipCertVerify bool) error {
req, err := http.NewRequest(http.MethodPost, address, nil)
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
client := http.Client{
Transport: commonhttp.GetHTTPTransportByInsecure(skipCertVerify),
}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
log.Debugf("policy test success with address %s, skip cert verify :%v", address, skipCertVerify)
return nil
}
// GetRelatedPolices get policies including event type in project
func (m *manager) GetRelatedPolices(ctx context.Context, projectID int64, eventType string) ([]*model.Policy, error) {
policies, err := m.List(ctx, q.New(q.KeyWords{"project_id": projectID}))
if err != nil {
return nil, fmt.Errorf("failed to get notification policies with projectID %d: %v", projectID, err)
}
var result []*model.Policy
for _, ply := range policies {
if !ply.Enabled {
continue
}
for _, t := range ply.EventTypes {
if t != eventType {
continue
}
result = append(result, ply)
}
}
return result, nil
}

View File

@ -1,149 +0,0 @@
package manager
import (
"fmt"
"net/http"
"time"
"github.com/goharbor/harbor/src/common/dao/notification"
commonhttp "github.com/goharbor/harbor/src/common/http"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/lib/log"
"github.com/goharbor/harbor/src/pkg/notifier/model"
)
// DefaultManager ...
type DefaultManager struct {
}
// NewDefaultManger ...
func NewDefaultManger() *DefaultManager {
return &DefaultManager{}
}
// Create notification policy
func (m *DefaultManager) Create(policy *models.NotificationPolicy) (int64, error) {
t := time.Now()
policy.CreationTime = t
policy.UpdateTime = t
err := policy.ConvertToDBModel()
if err != nil {
return 0, err
}
return notification.AddNotificationPolicy(policy)
}
// List the notification policies, returns the policy list and error
func (m *DefaultManager) List(projectID int64) ([]*models.NotificationPolicy, error) {
policies := []*models.NotificationPolicy{}
persisPolicies, err := notification.GetNotificationPolicies(projectID)
if err != nil {
return nil, err
}
for _, policy := range persisPolicies {
err := policy.ConvertFromDBModel()
if err != nil {
return nil, err
}
policies = append(policies, policy)
}
return policies, nil
}
// Get notification policy with specified ID
func (m *DefaultManager) Get(id int64) (*models.NotificationPolicy, error) {
policy, err := notification.GetNotificationPolicy(id)
if err != nil {
return nil, err
}
if policy == nil {
return nil, nil
}
err = policy.ConvertFromDBModel()
return policy, err
}
// GetByNameAndProjectID notification policy by the name and projectID
func (m *DefaultManager) GetByNameAndProjectID(name string, projectID int64) (*models.NotificationPolicy, error) {
policy, err := notification.GetNotificationPolicyByName(name, projectID)
if err != nil {
return nil, err
}
err = policy.ConvertFromDBModel()
return policy, err
}
// Update the specified notification policy
func (m *DefaultManager) Update(policy *models.NotificationPolicy) error {
policy.UpdateTime = time.Now()
err := policy.ConvertToDBModel()
if err != nil {
return err
}
return notification.UpdateNotificationPolicy(policy)
}
// Delete the specified notification policy
func (m *DefaultManager) Delete(policyID int64) error {
return notification.DeleteNotificationPolicy(policyID)
}
// Test the specified notification policy, just test for network connection without request body
func (m *DefaultManager) Test(policy *models.NotificationPolicy) error {
for _, target := range policy.Targets {
switch target.Type {
case model.NotifyTypeHTTP, model.NotifyTypeSlack:
return m.policyHTTPTest(target.Address, target.SkipCertVerify)
default:
return fmt.Errorf("invalid policy target type: %s", target.Type)
}
}
return nil
}
func (m *DefaultManager) policyHTTPTest(address string, skipCertVerify bool) error {
req, err := http.NewRequest(http.MethodPost, address, nil)
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
client := http.Client{
Transport: commonhttp.GetHTTPTransportByInsecure(skipCertVerify),
}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
log.Debugf("policy test success with address %s, skip cert verify :%v", address, skipCertVerify)
return nil
}
// GetRelatedPolices get policies including event type in project
func (m *DefaultManager) GetRelatedPolices(projectID int64, eventType string) ([]*models.NotificationPolicy, error) {
policies, err := m.List(projectID)
if err != nil {
return nil, fmt.Errorf("failed to get notification policies with projectID %d: %v", projectID, err)
}
var result []*models.NotificationPolicy
for _, ply := range policies {
if !ply.Enabled {
continue
}
for _, t := range ply.EventTypes {
if t != eventType {
continue
}
result = append(result, ply)
}
}
return result, nil
}

View File

@ -1,22 +0,0 @@
package manager
import (
"reflect"
"testing"
)
func TestNewDefaultManger(t *testing.T) {
tests := []struct {
name string
want *DefaultManager
}{
{want: &DefaultManager{}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := NewDefaultManger(); !reflect.DeepEqual(got, tt.want) {
t.Errorf("NewDefaultManger() = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -0,0 +1,116 @@
package policy
import (
"context"
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/pkg/notification/policy/model"
"github.com/goharbor/harbor/src/testing/mock"
"github.com/goharbor/harbor/src/testing/pkg/notification/policy/dao"
"github.com/stretchr/testify/suite"
"testing"
)
type managerTestSuite struct {
suite.Suite
mgr *manager
dao *dao.DAO
}
func (m *managerTestSuite) SetupTest() {
m.dao = &dao.DAO{}
m.mgr = &manager{
dao: m.dao,
}
}
func (m *managerTestSuite) TestCreate() {
m.dao.On("Create", mock.Anything, mock.Anything).Return(int64(1), nil)
_, err := m.mgr.Create(context.Background(), &model.Policy{})
m.Nil(err)
m.dao.AssertExpectations(m.T())
}
func (m *managerTestSuite) TestDelete() {
m.dao.On("Delete", mock.Anything, mock.Anything).Return(nil)
err := m.mgr.Delete(context.Background(), 1)
m.Nil(err)
m.dao.AssertExpectations(m.T())
}
func (m *managerTestSuite) TestUpdate() {
m.dao.On("Update", mock.Anything, mock.Anything).Return(nil)
err := m.mgr.Update(context.Background(), &model.Policy{})
m.Nil(err)
m.dao.AssertExpectations(m.T())
}
func (m *managerTestSuite) TestGet() {
m.dao.On("Get", mock.Anything, mock.Anything).Return(&model.Policy{
Name: "test_policy",
}, nil)
policy, err := m.mgr.Get(context.Background(), 1)
m.Nil(err)
m.Equal("test_policy", policy.Name)
m.dao.AssertExpectations(m.T())
}
func (m *managerTestSuite) TestList() {
m.dao.On("List", mock.Anything, mock.Anything).Return([]*model.Policy{
{
ID: 1,
Name: "policy",
},
}, nil)
rpers, err := m.mgr.List(context.Background(), nil)
m.Nil(err)
m.Equal(1, len(rpers))
m.dao.AssertExpectations(m.T())
}
func (m *managerTestSuite) TestGetByNameAndProjectID404() {
m.dao.On("List", mock.Anything, mock.Anything).Return([]*model.Policy{}, nil)
_, err := m.mgr.GetByNameAndProjectID(context.Background(), "test_policy", 1)
m.NotNil(err)
m.True(errors.IsNotFoundErr(err))
}
func (m *managerTestSuite) TestGetByNameAndProjectID() {
m.dao.On("List", mock.Anything, mock.Anything).Return([]*model.Policy{
{
ID: 1,
Name: "test_policy",
ProjectID: 1,
},
}, nil)
policy, err := m.mgr.GetByNameAndProjectID(context.Background(), "test_policy", 1)
m.Nil(err)
m.Equal("test_policy", policy.Name)
m.dao.AssertExpectations(m.T())
}
func (m *managerTestSuite) TestGetRelatedPolices() {
m.dao.On("List", mock.Anything, mock.Anything).Return([]*model.Policy{
{
ID: 1,
Name: "policy",
ProjectID: 1,
Enabled: true,
EventTypesDB: "[\"PULL_IMAGE\",\"PUSH_CHART\"]",
},
{
ID: 2,
Name: "policy",
ProjectID: 1,
Enabled: true,
EventTypesDB: "[\"PULL_IMAGE\",\"PUSH_CHART\"]",
},
}, nil)
rpers, err := m.mgr.GetRelatedPolices(context.Background(), 1, "PULL_IMAGE")
m.Nil(err)
m.Equal(2, len(rpers))
m.dao.AssertExpectations(m.T())
}
func TestManager(t *testing.T) {
suite.Run(t, &managerTestSuite{})
}

View File

@ -1,19 +1,17 @@
package models
package model
import (
"encoding/json"
"github.com/astaxie/beego/orm"
"time"
)
const (
// NotificationPolicyTable is table name for notification policies
NotificationPolicyTable = "notification_policy"
// NotificationJobTable is table name for notification job
NotificationJobTable = "notification_job"
)
func init() {
orm.RegisterModel(&Policy{})
}
// NotificationPolicy is the model for a notification policy.
type NotificationPolicy struct {
// Policy ...
type Policy struct {
ID int64 `orm:"pk;auto;column(id)" json:"id"`
Name string `orm:"column(name)" json:"name"`
Description string `orm:"column(description)" json:"description"`
@ -23,18 +21,18 @@ type NotificationPolicy struct {
EventTypesDB string `orm:"column(event_types)" json:"-"`
EventTypes []string `orm:"-" json:"event_types"`
Creator string `orm:"column(creator)" json:"creator"`
CreationTime time.Time `orm:"column(creation_time);auto_now_add" json:"creation_time"`
CreationTime time.Time `orm:"column(creation_time);auto_now_add" json:"creation_time" sort:"default:desc"`
UpdateTime time.Time `orm:"column(update_time);auto_now_add" json:"update_time"`
Enabled bool `orm:"column(enabled)" json:"enabled"`
}
// TableName set table name for ORM.
func (w *NotificationPolicy) TableName() string {
return NotificationPolicyTable
func (w *Policy) TableName() string {
return "notification_policy"
}
// ConvertToDBModel convert struct data in notification policy to DB model data
func (w *NotificationPolicy) ConvertToDBModel() error {
func (w *Policy) ConvertToDBModel() error {
if len(w.Targets) != 0 {
targets, err := json.Marshal(w.Targets)
if err != nil {
@ -54,7 +52,7 @@ func (w *NotificationPolicy) ConvertToDBModel() error {
}
// ConvertFromDBModel convert from DB model data to struct data
func (w *NotificationPolicy) ConvertFromDBModel() error {
func (w *Policy) ConvertFromDBModel() error {
targets := []EventTarget{}
if len(w.TargetsDB) != 0 {
err := json.Unmarshal([]byte(w.TargetsDB), &targets)
@ -76,32 +74,6 @@ func (w *NotificationPolicy) ConvertFromDBModel() error {
return nil
}
// NotificationJob is the model for a notification job
type NotificationJob struct {
ID int64 `orm:"pk;auto;column(id)" json:"id"`
PolicyID int64 `orm:"column(policy_id)" json:"policy_id"`
EventType string `orm:"column(event_type)" json:"event_type"`
NotifyType string `orm:"column(notify_type)" json:"notify_type"`
Status string `orm:"column(status)" json:"status"`
JobDetail string `orm:"column(job_detail)" json:"job_detail"`
UUID string `orm:"column(job_uuid)" json:"-"`
CreationTime time.Time `orm:"column(creation_time);auto_now_add" json:"creation_time"`
UpdateTime time.Time `orm:"column(update_time);auto_now" json:"update_time"`
}
// TableName set table name for ORM.
func (w *NotificationJob) TableName() string {
return NotificationJobTable
}
// NotificationJobQuery holds query conditions for notification job
type NotificationJobQuery struct {
PolicyID int64
Statuses []string
EventTypes []string
Pagination
}
// EventTarget defines the structure of target a notification send to
type EventTarget struct {
Type string `json:"type"`

View File

@ -1,40 +1,39 @@
package models
package model
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"testing"
)
func TestNotificationPolicy_ConvertFromDBModel(t *testing.T) {
func TestPolicy_ConvertFromDBModel(t *testing.T) {
tests := []struct {
name string
policy *NotificationPolicy
want *NotificationPolicy
policy *Policy
want *Policy
wantErr bool
}{
{
name: "ConvertFromDBModel want error 1",
policy: &NotificationPolicy{
policy: &Policy{
TargetsDB: "[{{\"type\":\"http\",\"address\":\"http://10.173.32.58:9009\"}]",
},
wantErr: true,
},
{
name: "ConvertFromDBModel want error 2",
policy: &NotificationPolicy{
policy: &Policy{
EventTypesDB: "[{\"pushImage\",\"pullImage\",\"deleteImage\"]",
},
wantErr: true,
},
{
name: "ConvertFromDBModel 1",
policy: &NotificationPolicy{
policy: &Policy{
TargetsDB: "[{\"type\":\"http\",\"address\":\"http://10.173.32.58:9009\"}]",
EventTypesDB: "[\"pushImage\",\"pullImage\",\"deleteImage\"]",
},
want: &NotificationPolicy{
want: &Policy{
Targets: []EventTarget{
{
Type: "http",
@ -60,16 +59,16 @@ func TestNotificationPolicy_ConvertFromDBModel(t *testing.T) {
}
}
func TestNotificationPolicy_ConvertToDBModel(t *testing.T) {
func TestPolicy_ConvertToDBModel(t *testing.T) {
tests := []struct {
name string
policy *NotificationPolicy
want *NotificationPolicy
policy *Policy
want *Policy
wantErr bool
}{
{
name: "ConvertToDBModel 1",
policy: &NotificationPolicy{
policy: &Policy{
Targets: []EventTarget{
{
Type: "http",
@ -79,7 +78,7 @@ func TestNotificationPolicy_ConvertToDBModel(t *testing.T) {
},
EventTypes: []string{"pushImage", "pullImage", "deleteImage"},
},
want: &NotificationPolicy{
want: &Policy{
TargetsDB: "[{\"type\":\"http\",\"address\":\"http://127.0.0.1\",\"skip_cert_verify\":false}]",
EventTypesDB: "[\"pushImage\",\"pullImage\",\"deleteImage\"]",
},
@ -99,16 +98,3 @@ func TestNotificationPolicy_ConvertToDBModel(t *testing.T) {
})
}
}
func TestNotificationJob_TableName(t *testing.T) {
job := &NotificationJob{}
got := job.TableName()
assert.Equal(t, NotificationJobTable, got)
}
func TestNotificationPolicy_TableName(t *testing.T) {
policy := &NotificationPolicy{}
got := policy.TableName()
assert.Equal(t, NotificationPolicyTable, got)
}

View File

@ -1,9 +1,9 @@
package event
import (
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/lib/log"
policy_model "github.com/goharbor/harbor/src/pkg/notification/policy/model"
"github.com/goharbor/harbor/src/pkg/notifier"
"github.com/goharbor/harbor/src/pkg/notifier/model"
)
@ -40,7 +40,7 @@ type Metadata interface {
type HookMetaData struct {
PolicyID int64
EventType string
Target *models.EventTarget
Target *policy_model.EventTarget
Payload *model.Payload
}

View File

@ -1,7 +1,7 @@
package event
import (
"github.com/goharbor/harbor/src/common/models"
policy_model "github.com/goharbor/harbor/src/pkg/notification/policy/model"
notifierModel "github.com/goharbor/harbor/src/pkg/notifier/model"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -25,7 +25,7 @@ func TestHookEvent_Build(t *testing.T) {
hookMetadata: &HookMetaData{
PolicyID: 1,
EventType: "pushImage",
Target: &models.EventTarget{
Target: &policy_model.EventTarget{
Type: "http",
Address: "http://127.0.0.1",
},

View File

@ -1,10 +1,10 @@
package notification
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/goharbor/harbor/src/common/job/models"
"github.com/goharbor/harbor/src/jobservice/job"
"github.com/goharbor/harbor/src/pkg/notification"
@ -21,7 +21,7 @@ func (h *HTTPHandler) Name() string {
}
// Handle handles http event
func (h *HTTPHandler) Handle(value interface{}) error {
func (h *HTTPHandler) Handle(ctx context.Context, value interface{}) error {
if value == nil {
return errors.New("HTTPHandler cannot handle nil value")
}
@ -30,8 +30,7 @@ func (h *HTTPHandler) Handle(value interface{}) error {
if !ok || event == nil {
return errors.New("invalid notification http event")
}
return h.process(event)
return h.process(ctx, event)
}
// IsStateful ...
@ -39,7 +38,7 @@ func (h *HTTPHandler) IsStateful() bool {
return false
}
func (h *HTTPHandler) process(event *model.HookEvent) error {
func (h *HTTPHandler) process(ctx context.Context, event *model.HookEvent) error {
j := &models.JobData{
Metadata: &models.JobMetadata{
JobKind: job.KindGeneric,
@ -60,5 +59,5 @@ func (h *HTTPHandler) process(event *model.HookEvent) error {
"auth_header": event.Target.AuthHeader,
"skip_cert_verify": event.Target.SkipCertVerify,
}
return notification.HookManager.StartHook(event, j)
return notification.HookManager.StartHook(ctx, event, j)
}

View File

@ -1,14 +1,15 @@
package notification
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/goharbor/harbor/src/common/job/models"
cModels "github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/pkg/notification"
policy_model "github.com/goharbor/harbor/src/pkg/notification/policy/model"
"github.com/goharbor/harbor/src/pkg/notifier/event"
"github.com/goharbor/harbor/src/pkg/notifier/model"
"github.com/stretchr/testify/require"
@ -17,7 +18,7 @@ import (
type fakedHookManager struct {
}
func (f *fakedHookManager) StartHook(event *model.HookEvent, job *models.JobData) error {
func (f *fakedHookManager) StartHook(ctx context.Context, event *model.HookEvent, job *models.JobData) error {
return nil
}
@ -66,7 +67,7 @@ func TestHTTPHandler_Handle(t *testing.T) {
Data: &model.HookEvent{
PolicyID: 1,
EventType: "pushImage",
Target: &cModels.EventTarget{
Target: &policy_model.EventTarget{
Type: "http",
Address: "http://127.0.0.1:8080",
},
@ -82,7 +83,7 @@ func TestHTTPHandler_Handle(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := handler.Handle(tt.args.event.Data)
err := handler.Handle(context.TODO(), tt.args.event.Data)
if tt.wantErr {
require.NotNil(t, err, "Error: %s", err)
return

View File

@ -1,11 +1,11 @@
package notification
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"bytes"
"encoding/json"
"github.com/goharbor/harbor/src/common/job/models"
"github.com/goharbor/harbor/src/jobservice/job"
"github.com/goharbor/harbor/src/pkg/notification"
@ -70,7 +70,7 @@ func (s *SlackHandler) Name() string {
}
// Handle handles event to slack
func (s *SlackHandler) Handle(value interface{}) error {
func (s *SlackHandler) Handle(ctx context.Context, value interface{}) error {
if value == nil {
return errors.New("SlackHandler cannot handle nil value")
}
@ -80,7 +80,7 @@ func (s *SlackHandler) Handle(value interface{}) error {
return errors.New("invalid notification slack event")
}
return s.process(event)
return s.process(ctx, event)
}
// IsStateful ...
@ -88,7 +88,7 @@ func (s *SlackHandler) IsStateful() bool {
return false
}
func (s *SlackHandler) process(event *model.HookEvent) error {
func (s *SlackHandler) process(ctx context.Context, event *model.HookEvent) error {
j := &models.JobData{
Metadata: &models.JobMetadata{
JobKind: job.KindGeneric,
@ -108,7 +108,7 @@ func (s *SlackHandler) process(event *model.HookEvent) error {
"address": event.Target.Address,
"skip_cert_verify": event.Target.SkipCertVerify,
}
return notification.HookManager.StartHook(event, j)
return notification.HookManager.StartHook(ctx, event, j)
}
func (s *SlackHandler) convert(payLoad *model.Payload) (string, error) {

View File

@ -1,19 +1,21 @@
package notification
import (
"context"
"github.com/goharbor/harbor/src/common/dao"
"testing"
"time"
"github.com/stretchr/testify/assert"
cModels "github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/pkg/notification"
policy_model "github.com/goharbor/harbor/src/pkg/notification/policy/model"
"github.com/goharbor/harbor/src/pkg/notifier/event"
"github.com/goharbor/harbor/src/pkg/notifier/model"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestSlackHandler_Handle(t *testing.T) {
dao.PrepareTestForPostgresSQL()
hookMgr := notification.HookManager
defer func() {
notification.HookManager = hookMgr
@ -58,7 +60,7 @@ func TestSlackHandler_Handle(t *testing.T) {
Data: &model.HookEvent{
PolicyID: 1,
EventType: "pushImage",
Target: &cModels.EventTarget{
Target: &policy_model.EventTarget{
Type: "slack",
Address: "http://127.0.0.1:8080",
},
@ -86,7 +88,7 @@ func TestSlackHandler_Handle(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := handler.Handle(tt.args.event.Data)
err := handler.Handle(context.TODO(), tt.args.event.Data)
if tt.wantErr {
require.NotNil(t, err, "Error: %s", err)
return

View File

@ -1,15 +1,15 @@
package model
import (
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/controller/event/model"
policy_model "github.com/goharbor/harbor/src/pkg/notification/policy/model"
)
// HookEvent is hook related event data to publish
type HookEvent struct {
PolicyID int64
EventType string
Target *models.EventTarget
Target *policy_model.EventTarget
Payload *Payload
}

View File

@ -1,5 +1,7 @@
package notifier
import "context"
// NotificationHandler defines what operations a notification handler
// should have.
type NotificationHandler interface {
@ -8,7 +10,7 @@ type NotificationHandler interface {
// Handle the event when it coming.
// value might be optional, it depends on usages.
Handle(value interface{}) error
Handle(ctx context.Context, value interface{}) error
// IsStateful returns whether the handler is stateful or not.
// If handler is stateful, it will not be triggered in parallel.

View File

@ -3,6 +3,7 @@ package notifier
import (
"errors"
"fmt"
"github.com/goharbor/harbor/src/lib/orm"
"reflect"
"strings"
"sync"
@ -197,7 +198,7 @@ func (nw *NotificationWatcher) Notify(notification Notification) error {
<-ch
}
}()
if err := hd.Handle(notification.Value); err != nil {
if err := hd.Handle(orm.Context(), notification.Value); err != nil {
// Currently, we just log the error
log.Errorf("Error occurred when triggering handler %s of topic %s: %s\n", reflect.TypeOf(hd).String(), notification.Topic, err.Error())
} else {

View File

@ -1,6 +1,8 @@
package notifier
import (
"context"
"github.com/goharbor/harbor/src/common/dao"
"reflect"
"sync/atomic"
"testing"
@ -21,7 +23,7 @@ func (fsh *fakeStatefulHandler) IsStateful() bool {
return true
}
func (fsh *fakeStatefulHandler) Handle(v interface{}) error {
func (fsh *fakeStatefulHandler) Handle(ctx context.Context, v interface{}) error {
increment := 0
if v != nil && reflect.TypeOf(v).Kind() == reflect.Int {
increment = v.(int)
@ -40,7 +42,7 @@ func (fsh *fakeStatelessHandler) Name() string {
return "fakeStateless"
}
func (fsh *fakeStatelessHandler) Handle(v interface{}) error {
func (fsh *fakeStatelessHandler) Handle(ctx context.Context, v interface{}) error {
return nil
}
@ -116,6 +118,7 @@ func TestSubscribeAndUnSubscribe(t *testing.T) {
}
func TestPublish(t *testing.T) {
dao.PrepareTestForPostgresSQL()
count := len(notificationWatcher.handlers)
err := Subscribe("topic1", &fakeStatefulHandler{0})
if err != nil {
@ -157,6 +160,7 @@ func TestPublish(t *testing.T) {
}
func TestConcurrentPublish(t *testing.T) {
dao.PrepareTestForPostgresSQL()
count := len(notificationWatcher.handlers)
err := Subscribe("topic1", &fakeStatefulHandler{0})
if err != nil {

View File

@ -50,6 +50,8 @@ func New() http.Handler {
GCAPI: newGCAPI(),
QuotaAPI: newQuotaAPI(),
RetentionAPI: newRetentionAPI(),
WebhookAPI: newNotificationPolicyAPI(),
WebhookjobAPI: newNotificationJobAPI(),
ImmutableAPI: newImmutableAPI(),
OidcAPI: newOIDCAPI(),
SystemCVEAllowlistAPI: newSystemCVEAllowListAPI(),

View File

@ -0,0 +1,33 @@
package model
import (
"github.com/go-openapi/strfmt"
"github.com/goharbor/harbor/src/pkg/notification/job/model"
"github.com/goharbor/harbor/src/server/v2.0/models"
)
// NotificationJob ...
type NotificationJob struct {
*model.Job
}
// ToSwagger ...
func (n *NotificationJob) ToSwagger() *models.WebhookJob {
return &models.WebhookJob{
ID: n.ID,
EventType: n.EventType,
JobDetail: n.JobDetail,
NotifyType: n.NotifyType,
PolicyID: n.PolicyID,
Status: n.Status,
CreationTime: strfmt.DateTime(n.CreationTime),
UpdateTime: strfmt.DateTime(n.UpdateTime),
}
}
// NewNotificationJob ...
func NewNotificationJob(j *model.Job) *NotificationJob {
return &NotificationJob{
Job: j,
}
}

View File

@ -0,0 +1,49 @@
package model
import (
"github.com/go-openapi/strfmt"
"github.com/goharbor/harbor/src/pkg/notification/policy/model"
"github.com/goharbor/harbor/src/server/v2.0/models"
)
// NotifiactionPolicy ...
type NotifiactionPolicy struct {
*model.Policy
}
// ToSwagger ...
func (n *NotifiactionPolicy) ToSwagger() *models.WebhookPolicy {
return &models.WebhookPolicy{
ID: n.ID,
CreationTime: strfmt.DateTime(n.CreationTime),
UpdateTime: strfmt.DateTime(n.UpdateTime),
Creator: n.Creator,
Description: n.Description,
Enabled: n.Enabled,
EventTypes: n.EventTypes,
Name: n.Name,
ProjectID: n.ProjectID,
Targets: n.ToTargets(),
}
}
// ToTargets ...
func (n *NotifiactionPolicy) ToTargets() []*models.WebhookTargetObject {
var results []*models.WebhookTargetObject
for _, t := range n.Targets {
results = append(results, &models.WebhookTargetObject{
Type: t.Type,
Address: t.Address,
AuthHeader: t.AuthHeader,
SkipCertVerify: t.SkipCertVerify,
})
}
return results
}
// NewNotifiactionPolicy ...
func NewNotifiactionPolicy(p *model.Policy) *NotifiactionPolicy {
return &NotifiactionPolicy{
Policy: p,
}
}

View File

@ -0,0 +1,67 @@
package handler
import (
"context"
"github.com/go-openapi/runtime/middleware"
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/pkg/notification/job"
"github.com/goharbor/harbor/src/pkg/notification/policy"
"github.com/goharbor/harbor/src/server/v2.0/handler/model"
"github.com/goharbor/harbor/src/server/v2.0/models"
"github.com/goharbor/harbor/src/server/v2.0/restapi/operations/webhookjob"
operation "github.com/goharbor/harbor/src/server/v2.0/restapi/operations/webhookjob"
)
func newNotificationJobAPI() *notificationJobAPI {
return &notificationJobAPI{
webhookjobMgr: job.Mgr,
webhookPolicyMgr: policy.Mgr,
}
}
type notificationJobAPI struct {
BaseAPI
webhookjobMgr job.Manager
webhookPolicyMgr policy.Manager
}
func (n *notificationJobAPI) ListWebhookJobs(ctx context.Context, params webhookjob.ListWebhookJobsParams) middleware.Responder {
projectNameOrID := parseProjectNameOrID(params.ProjectNameOrID, params.XIsResourceName)
if err := n.RequireProjectAccess(ctx, projectNameOrID, rbac.ActionList, rbac.ResourceNotificationPolicy); err != nil {
return n.SendError(ctx, err)
}
policy, err := n.webhookPolicyMgr.Get(ctx, params.PolicyID)
if err != nil {
return n.SendError(ctx, err)
}
query, err := n.BuildQuery(ctx, params.Q, params.Sort, params.Page, params.PageSize)
if err != nil {
return n.SendError(ctx, err)
}
query.Keywords["PolicyID"] = policy.ID
if len(params.Status) != 0 {
query.Keywords["Status"] = params.Status
}
total, err := n.webhookjobMgr.Count(ctx, query)
if err != nil {
return n.SendError(ctx, err)
}
jobs, err := n.webhookjobMgr.List(ctx, query)
if err != nil {
return n.SendError(ctx, err)
}
var results []*models.WebhookJob
for _, j := range jobs {
results = append(results, model.NewNotificationJob(j).ToSwagger())
}
return operation.NewListWebhookJobsOK().
WithXTotalCount(total).
WithLink(n.Links(ctx, params.HTTPRequest.URL, total, query.PageNumber, query.PageSize).String()).
WithPayload(results)
}

View File

@ -0,0 +1,285 @@
package handler
import (
"context"
"fmt"
"github.com/go-openapi/runtime/middleware"
"github.com/go-openapi/strfmt"
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/common/utils"
"github.com/goharbor/harbor/src/lib"
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/lib/q"
"github.com/goharbor/harbor/src/pkg/notification"
"github.com/goharbor/harbor/src/pkg/notification/job"
"github.com/goharbor/harbor/src/pkg/notification/policy"
policy_model "github.com/goharbor/harbor/src/pkg/notification/policy/model"
"github.com/goharbor/harbor/src/server/v2.0/handler/model"
"github.com/goharbor/harbor/src/server/v2.0/models"
"github.com/goharbor/harbor/src/server/v2.0/restapi/operations/webhook"
operation "github.com/goharbor/harbor/src/server/v2.0/restapi/operations/webhook"
"strings"
"time"
)
func newNotificationPolicyAPI() *notificationPolicyAPI {
return &notificationPolicyAPI{
webhookjobMgr: job.Mgr,
webhookPolicyMgr: policy.Mgr,
}
}
type notificationPolicyAPI struct {
BaseAPI
webhookjobMgr job.Manager
webhookPolicyMgr policy.Manager
}
func (n *notificationPolicyAPI) Prepare(ctx context.Context, operation string, params interface{}) middleware.Responder {
return nil
}
func (n *notificationPolicyAPI) ListWebhookPoliciesOfProject(ctx context.Context, params webhook.ListWebhookPoliciesOfProjectParams) middleware.Responder {
projectNameOrID := parseProjectNameOrID(params.ProjectNameOrID, params.XIsResourceName)
if err := n.RequireProjectAccess(ctx, projectNameOrID, rbac.ActionList, rbac.ResourceNotificationPolicy); err != nil {
return n.SendError(ctx, err)
}
projectID, err := getProjectID(ctx, projectNameOrID)
if err != nil {
return n.SendError(ctx, err)
}
query, err := n.BuildQuery(ctx, params.Q, params.Sort, params.Page, params.PageSize)
if err != nil {
return n.SendError(ctx, err)
}
query.Keywords["ProjectID"] = projectID
total, err := n.webhookPolicyMgr.Count(ctx, query)
if err != nil {
return n.SendError(ctx, err)
}
policies, err := n.webhookPolicyMgr.List(ctx, query)
if err != nil {
return n.SendError(ctx, err)
}
var results []*models.WebhookPolicy
for _, p := range policies {
results = append(results, model.NewNotifiactionPolicy(p).ToSwagger())
}
return operation.NewListWebhookPoliciesOfProjectOK().
WithXTotalCount(total).
WithLink(n.Links(ctx, params.HTTPRequest.URL, total, query.PageNumber, query.PageSize).String()).
WithPayload(results)
}
func (n *notificationPolicyAPI) CreateWebhookPolicyOfProject(ctx context.Context, params webhook.CreateWebhookPolicyOfProjectParams) middleware.Responder {
projectNameOrID := parseProjectNameOrID(params.ProjectNameOrID, params.XIsResourceName)
if err := n.RequireProjectAccess(ctx, projectNameOrID, rbac.ActionCreate, rbac.ResourceNotificationPolicy); err != nil {
return n.SendError(ctx, err)
}
policy := &policy_model.Policy{}
lib.JSONCopy(policy, params.Policy)
if ok, err := n.validateEventTypes(policy); !ok {
return n.SendError(ctx, err)
}
if ok, err := n.validateTargets(policy); !ok {
return n.SendError(ctx, err)
}
projectID, err := getProjectID(ctx, projectNameOrID)
if err != nil {
return n.SendError(ctx, err)
}
policy.ProjectID = projectID
id, err := n.webhookPolicyMgr.Create(ctx, policy)
if err != nil {
return n.SendError(ctx, err)
}
location := fmt.Sprintf("%s/%d", strings.TrimSuffix(params.HTTPRequest.URL.Path, "/"), id)
return operation.NewCreateWebhookPolicyOfProjectCreated().WithLocation(location)
}
func (n *notificationPolicyAPI) UpdateWebhookPolicyOfProject(ctx context.Context, params webhook.UpdateWebhookPolicyOfProjectParams) middleware.Responder {
projectNameOrID := parseProjectNameOrID(params.ProjectNameOrID, params.XIsResourceName)
if err := n.RequireProjectAccess(ctx, projectNameOrID, rbac.ActionUpdate, rbac.ResourceNotificationPolicy); err != nil {
return n.SendError(ctx, err)
}
policy := &policy_model.Policy{}
lib.JSONCopy(policy, params.Policy)
if ok, err := n.validateEventTypes(policy); !ok {
return n.SendError(ctx, err)
}
if ok, err := n.validateTargets(policy); !ok {
return n.SendError(ctx, err)
}
projectID, err := getProjectID(ctx, projectNameOrID)
if err != nil {
return n.SendError(ctx, err)
}
policy.ProjectID = projectID
if err := n.webhookPolicyMgr.Update(ctx, policy); err != nil {
return n.SendError(ctx, err)
}
return operation.NewUpdateWebhookPolicyOfProjectOK()
}
func (n *notificationPolicyAPI) DeleteWebhookPolicyOfProject(ctx context.Context, params webhook.DeleteWebhookPolicyOfProjectParams) middleware.Responder {
projectNameOrID := parseProjectNameOrID(params.ProjectNameOrID, params.XIsResourceName)
if err := n.RequireProjectAccess(ctx, projectNameOrID, rbac.ActionDelete, rbac.ResourceNotificationPolicy); err != nil {
return n.SendError(ctx, err)
}
if err := n.webhookPolicyMgr.Delete(ctx, params.WebhookPolicyID); err != nil {
return n.SendError(ctx, err)
}
return operation.NewDeleteWebhookPolicyOfProjectOK()
}
func (n *notificationPolicyAPI) GetWebhookPolicyOfProject(ctx context.Context, params webhook.GetWebhookPolicyOfProjectParams) middleware.Responder {
projectNameOrID := parseProjectNameOrID(params.ProjectNameOrID, params.XIsResourceName)
if err := n.RequireProjectAccess(ctx, projectNameOrID, rbac.ActionRead, rbac.ResourceNotificationPolicy); err != nil {
return n.SendError(ctx, err)
}
policy, err := n.webhookPolicyMgr.Get(ctx, params.WebhookPolicyID)
if err != nil {
return n.SendError(ctx, err)
}
return operation.NewGetWebhookPolicyOfProjectOK().WithPayload(model.NewNotifiactionPolicy(policy).ToSwagger())
}
func (n *notificationPolicyAPI) LastTrigger(ctx context.Context, params webhook.LastTriggerParams) middleware.Responder {
projectNameOrID := parseProjectNameOrID(params.ProjectNameOrID, params.XIsResourceName)
if err := n.RequireProjectAccess(ctx, projectNameOrID, rbac.ActionRead, rbac.ResourceNotificationPolicy); err != nil {
return n.SendError(ctx, err)
}
projectID, err := getProjectID(ctx, projectNameOrID)
if err != nil {
return n.SendError(ctx, err)
}
query := &q.Query{
Keywords: q.KeyWords{
"ProjectID": projectID,
},
}
policies, err := n.webhookPolicyMgr.List(ctx, query)
if err != nil {
return n.SendError(ctx, err)
}
triggers, err := n.constructPolicyWithTriggerTime(ctx, policies)
if err != nil {
return n.SendError(ctx, err)
}
return operation.NewLastTriggerOK().WithPayload(triggers)
}
func (n *notificationPolicyAPI) GetSupportedEventTypes(ctx context.Context, params webhook.GetSupportedEventTypesParams) middleware.Responder {
projectNameOrID := parseProjectNameOrID(params.ProjectNameOrID, params.XIsResourceName)
if err := n.RequireProjectAccess(ctx, projectNameOrID, rbac.ActionRead, rbac.ResourceNotificationPolicy); err != nil {
return n.SendError(ctx, err)
}
var notificationTypes = &models.SupportedWebhookEventTypes{}
for key := range notification.SupportedNotifyTypes {
notificationTypes.NotifyType = append(notificationTypes.NotifyType, models.NotifyType(key))
}
for key := range notification.SupportedEventTypes {
notificationTypes.EventType = append(notificationTypes.EventType, models.EventType(key))
}
return operation.NewGetSupportedEventTypesOK().WithPayload(notificationTypes)
}
func (n *notificationPolicyAPI) getLastTriggerTimeGroupByEventType(ctx context.Context, eventType string, policyID int64) (time.Time, error) {
jobs, err := n.webhookjobMgr.ListJobsGroupByEventType(ctx, policyID)
if err != nil {
return time.Time{}, err
}
for _, job := range jobs {
if eventType == job.EventType {
return job.CreationTime, nil
}
}
return time.Time{}, nil
}
func (n *notificationPolicyAPI) validateTargets(policy *policy_model.Policy) (bool, error) {
if len(policy.Targets) == 0 {
return false, errors.New(nil).WithMessage("empty notification target with policy %s", policy.Name).WithCode(errors.BadRequestCode)
}
for _, target := range policy.Targets {
url, err := utils.ParseEndpoint(target.Address)
if err != nil {
return false, errors.New(err).WithCode(errors.BadRequestCode)
}
// Prevent SSRF security issue #3755
target.Address = url.Scheme + "://" + url.Host + url.Path
_, ok := notification.SupportedNotifyTypes[target.Type]
if !ok {
return false, errors.New(nil).WithMessage("unsupported target type %s with policy %s", target.Type, policy.Name).WithCode(errors.BadRequestCode)
}
}
return true, nil
}
func (n *notificationPolicyAPI) validateEventTypes(policy *policy_model.Policy) (bool, error) {
if len(policy.EventTypes) == 0 {
return false, errors.New(nil).WithMessage("empty event type").WithCode(errors.BadRequestCode)
}
for _, eventType := range policy.EventTypes {
_, ok := notification.SupportedEventTypes[eventType]
if !ok {
return false, errors.New(nil).WithMessage("unsupported event type %s", eventType).WithCode(errors.BadRequestCode)
}
}
return true, nil
}
// constructPolicyWithTriggerTime construct notification policy information displayed in UI
// including event type, enabled, creation time, last trigger time
func (n *notificationPolicyAPI) constructPolicyWithTriggerTime(ctx context.Context, policies []*policy_model.Policy) ([]*models.WebhookLastTrigger, error) {
res := []*models.WebhookLastTrigger{}
if policies != nil {
for _, policy := range policies {
for _, t := range policy.EventTypes {
ply := &models.WebhookLastTrigger{
PolicyName: policy.Name,
EventType: t,
Enabled: policy.Enabled,
CreationTime: strfmt.DateTime(policy.CreationTime),
}
if !policy.CreationTime.IsZero() {
ply.CreationTime = strfmt.DateTime(policy.CreationTime)
}
ltTime, err := n.getLastTriggerTimeGroupByEventType(ctx, t, policy.ID)
if err != nil {
return nil, err
}
if !ltTime.IsZero() {
ply.LastTriggerTime = strfmt.DateTime(ltTime)
}
res = append(res, ply)
}
}
}
return res, nil
}

View File

@ -15,13 +15,16 @@
package handler
import (
"context"
"fmt"
"github.com/goharbor/harbor/src/controller/project"
"net/url"
"reflect"
"strconv"
"strings"
"github.com/goharbor/harbor/src/lib"
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/lib/log"
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
)
@ -113,3 +116,19 @@ func parseProjectNameOrID(str string, isResourceName *bool) interface{} {
return v // projectID
}
func getProjectID(ctx context.Context, projectNameOrID interface{}) (int64, error) {
projectName, ok := projectNameOrID.(string)
if ok {
p, err := project.Ctl.Get(ctx, projectName, project.Metadata(false))
if err != nil {
return 0, err
}
return p.ProjectID, nil
}
projectID, ok := projectNameOrID.(int64)
if ok {
return projectID, nil
}
return 0, errors.New("unknown project identifier type")
}

View File

@ -40,12 +40,6 @@ func registerLegacyRoutes() {
beego.Router("/api/"+version+"/replication/adapters", &api.ReplicationAdapterAPI{}, "get:List")
beego.Router("/api/"+version+"/replication/adapterinfos", &api.ReplicationAdapterAPI{}, "get:ListAdapterInfos")
beego.Router("/api/"+version+"/projects/:pid([0-9]+)/webhook/policies", &api.NotificationPolicyAPI{}, "get:List;post:Post")
beego.Router("/api/"+version+"/projects/:pid([0-9]+)/webhook/policies/:id([0-9]+)", &api.NotificationPolicyAPI{})
beego.Router("/api/"+version+"/projects/:pid([0-9]+)/webhook/lasttrigger", &api.NotificationPolicyAPI{}, "get:ListGroupByEventType")
beego.Router("/api/"+version+"/projects/:pid([0-9]+)/webhook/events", &api.NotificationPolicyAPI{}, "get:GetSupportedEventTypes")
beego.Router("/api/"+version+"/projects/:pid([0-9]+)/webhook/jobs/", &api.NotificationJobAPI{}, "get:List")
beego.Router("/api/"+version+"/configurations", &api.ConfigAPI{}, "get:Get;put:Put")
beego.Router("/api/"+version+"/statistics", &api.StatisticAPI{})
beego.Router("/api/"+version+"/labels", &api.LabelAPI{}, "post:Post;get:List")

View File

@ -0,0 +1,178 @@
// Code generated by mockery v2.1.0. DO NOT EDIT.
package dao
import (
context "context"
mock "github.com/stretchr/testify/mock"
model "github.com/goharbor/harbor/src/pkg/notification/job/model"
q "github.com/goharbor/harbor/src/lib/q"
)
// DAO is an autogenerated mock type for the DAO type
type DAO struct {
mock.Mock
}
// Count provides a mock function with given fields: ctx, query
func (_m *DAO) Count(ctx context.Context, query *q.Query) (int64, error) {
ret := _m.Called(ctx, query)
var r0 int64
if rf, ok := ret.Get(0).(func(context.Context, *q.Query) int64); ok {
r0 = rf(ctx, query)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *q.Query) error); ok {
r1 = rf(ctx, query)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Create provides a mock function with given fields: ctx, n
func (_m *DAO) Create(ctx context.Context, n *model.Job) (int64, error) {
ret := _m.Called(ctx, n)
var r0 int64
if rf, ok := ret.Get(0).(func(context.Context, *model.Job) int64); ok {
r0 = rf(ctx, n)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *model.Job) error); ok {
r1 = rf(ctx, n)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Delete provides a mock function with given fields: ctx, id
func (_m *DAO) Delete(ctx context.Context, id int64) error {
ret := _m.Called(ctx, id)
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok {
r0 = rf(ctx, id)
} else {
r0 = ret.Error(0)
}
return r0
}
// DeleteByPolicyID provides a mock function with given fields: ctx, policyID
func (_m *DAO) DeleteByPolicyID(ctx context.Context, policyID int64) error {
ret := _m.Called(ctx, policyID)
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok {
r0 = rf(ctx, policyID)
} else {
r0 = ret.Error(0)
}
return r0
}
// Get provides a mock function with given fields: ctx, id
func (_m *DAO) Get(ctx context.Context, id int64) (*model.Job, error) {
ret := _m.Called(ctx, id)
var r0 *model.Job
if rf, ok := ret.Get(0).(func(context.Context, int64) *model.Job); ok {
r0 = rf(ctx, id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Job)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok {
r1 = rf(ctx, id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetLastTriggerJobsGroupByEventType provides a mock function with given fields: ctx, policyID
func (_m *DAO) GetLastTriggerJobsGroupByEventType(ctx context.Context, policyID int64) ([]*model.Job, error) {
ret := _m.Called(ctx, policyID)
var r0 []*model.Job
if rf, ok := ret.Get(0).(func(context.Context, int64) []*model.Job); ok {
r0 = rf(ctx, policyID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Job)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok {
r1 = rf(ctx, policyID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// List provides a mock function with given fields: ctx, query
func (_m *DAO) List(ctx context.Context, query *q.Query) ([]*model.Job, error) {
ret := _m.Called(ctx, query)
var r0 []*model.Job
if rf, ok := ret.Get(0).(func(context.Context, *q.Query) []*model.Job); ok {
r0 = rf(ctx, query)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Job)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *q.Query) error); ok {
r1 = rf(ctx, query)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Update provides a mock function with given fields: ctx, n, props
func (_m *DAO) Update(ctx context.Context, n *model.Job, props ...string) error {
_va := make([]interface{}, len(props))
for _i := range props {
_va[_i] = props[_i]
}
var _ca []interface{}
_ca = append(_ca, ctx, n)
_ca = append(_ca, _va...)
ret := _m.Called(_ca...)
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, *model.Job, ...string) error); ok {
r0 = rf(ctx, n, props...)
} else {
r0 = ret.Error(0)
}
return r0
}

View File

@ -1,62 +0,0 @@
package notification
import (
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/controller/event"
)
type FakedPolicyMgr struct {
}
func (f *FakedPolicyMgr) Create(*models.NotificationPolicy) (int64, error) {
return 0, nil
}
func (f *FakedPolicyMgr) List(id int64) ([]*models.NotificationPolicy, error) {
return nil, nil
}
func (f *FakedPolicyMgr) Get(id int64) (*models.NotificationPolicy, error) {
return nil, nil
}
func (f *FakedPolicyMgr) GetByNameAndProjectID(string, int64) (*models.NotificationPolicy, error) {
return nil, nil
}
func (f *FakedPolicyMgr) Update(*models.NotificationPolicy) error {
return nil
}
func (f *FakedPolicyMgr) Delete(int64) error {
return nil
}
func (f *FakedPolicyMgr) Test(*models.NotificationPolicy) error {
return nil
}
func (f *FakedPolicyMgr) GetRelatedPolices(id int64, eventType string) ([]*models.NotificationPolicy, error) {
return []*models.NotificationPolicy{
{
ID: 1,
EventTypes: []string{
event.TopicUploadChart,
event.TopicDownloadChart,
event.TopicDeleteChart,
event.TopicPushArtifact,
event.TopicPullArtifact,
event.TopicDeleteArtifact,
event.TopicScanningFailed,
event.TopicScanningCompleted,
event.TopicQuotaExceed,
},
Targets: []models.EventTarget{
{
Type: "http",
Address: "http://127.0.0.1:8080",
},
},
},
}, nil
}

View File

@ -0,0 +1,134 @@
// Code generated by mockery v2.1.0. DO NOT EDIT.
package dao
import (
context "context"
mock "github.com/stretchr/testify/mock"
model "github.com/goharbor/harbor/src/pkg/notification/policy/model"
q "github.com/goharbor/harbor/src/lib/q"
)
// DAO is an autogenerated mock type for the DAO type
type DAO struct {
mock.Mock
}
// Count provides a mock function with given fields: ctx, query
func (_m *DAO) Count(ctx context.Context, query *q.Query) (int64, error) {
ret := _m.Called(ctx, query)
var r0 int64
if rf, ok := ret.Get(0).(func(context.Context, *q.Query) int64); ok {
r0 = rf(ctx, query)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *q.Query) error); ok {
r1 = rf(ctx, query)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Create provides a mock function with given fields: ctx, n
func (_m *DAO) Create(ctx context.Context, n *model.Policy) (int64, error) {
ret := _m.Called(ctx, n)
var r0 int64
if rf, ok := ret.Get(0).(func(context.Context, *model.Policy) int64); ok {
r0 = rf(ctx, n)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *model.Policy) error); ok {
r1 = rf(ctx, n)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Delete provides a mock function with given fields: ctx, id
func (_m *DAO) Delete(ctx context.Context, id int64) error {
ret := _m.Called(ctx, id)
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok {
r0 = rf(ctx, id)
} else {
r0 = ret.Error(0)
}
return r0
}
// Get provides a mock function with given fields: ctx, id
func (_m *DAO) Get(ctx context.Context, id int64) (*model.Policy, error) {
ret := _m.Called(ctx, id)
var r0 *model.Policy
if rf, ok := ret.Get(0).(func(context.Context, int64) *model.Policy); ok {
r0 = rf(ctx, id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Policy)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok {
r1 = rf(ctx, id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// List provides a mock function with given fields: ctx, query
func (_m *DAO) List(ctx context.Context, query *q.Query) ([]*model.Policy, error) {
ret := _m.Called(ctx, query)
var r0 []*model.Policy
if rf, ok := ret.Get(0).(func(context.Context, *q.Query) []*model.Policy); ok {
r0 = rf(ctx, query)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Policy)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *q.Query) error); ok {
r1 = rf(ctx, query)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Update provides a mock function with given fields: ctx, n
func (_m *DAO) Update(ctx context.Context, n *model.Policy) error {
ret := _m.Called(ctx, n)
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, *model.Policy) error); ok {
r0 = rf(ctx, n)
} else {
r0 = ret.Error(0)
}
return r0
}

View File

@ -0,0 +1,193 @@
// Code generated by mockery v2.1.0. DO NOT EDIT.
package notification
import (
context "context"
model "github.com/goharbor/harbor/src/pkg/notification/policy/model"
mock "github.com/stretchr/testify/mock"
q "github.com/goharbor/harbor/src/lib/q"
)
// Manager is an autogenerated mock type for the Manager type
type Manager struct {
mock.Mock
}
// Count provides a mock function with given fields: ctx, query
func (_m *Manager) Count(ctx context.Context, query *q.Query) (int64, error) {
ret := _m.Called(ctx, query)
var r0 int64
if rf, ok := ret.Get(0).(func(context.Context, *q.Query) int64); ok {
r0 = rf(ctx, query)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *q.Query) error); ok {
r1 = rf(ctx, query)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Create provides a mock function with given fields: ctx, _a1
func (_m *Manager) Create(ctx context.Context, _a1 *model.Policy) (int64, error) {
ret := _m.Called(ctx, _a1)
var r0 int64
if rf, ok := ret.Get(0).(func(context.Context, *model.Policy) int64); ok {
r0 = rf(ctx, _a1)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *model.Policy) error); ok {
r1 = rf(ctx, _a1)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Delete provides a mock function with given fields: ctx, policyID
func (_m *Manager) Delete(ctx context.Context, policyID int64) error {
ret := _m.Called(ctx, policyID)
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok {
r0 = rf(ctx, policyID)
} else {
r0 = ret.Error(0)
}
return r0
}
// Get provides a mock function with given fields: ctx, id
func (_m *Manager) Get(ctx context.Context, id int64) (*model.Policy, error) {
ret := _m.Called(ctx, id)
var r0 *model.Policy
if rf, ok := ret.Get(0).(func(context.Context, int64) *model.Policy); ok {
r0 = rf(ctx, id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Policy)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok {
r1 = rf(ctx, id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetByNameAndProjectID provides a mock function with given fields: ctx, name, projectID
func (_m *Manager) GetByNameAndProjectID(ctx context.Context, name string, projectID int64) (*model.Policy, error) {
ret := _m.Called(ctx, name, projectID)
var r0 *model.Policy
if rf, ok := ret.Get(0).(func(context.Context, string, int64) *model.Policy); ok {
r0 = rf(ctx, name, projectID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Policy)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, string, int64) error); ok {
r1 = rf(ctx, name, projectID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetRelatedPolices provides a mock function with given fields: ctx, projectID, eventType
func (_m *Manager) GetRelatedPolices(ctx context.Context, projectID int64, eventType string) ([]*model.Policy, error) {
ret := _m.Called(ctx, projectID, eventType)
var r0 []*model.Policy
if rf, ok := ret.Get(0).(func(context.Context, int64, string) []*model.Policy); ok {
r0 = rf(ctx, projectID, eventType)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Policy)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, int64, string) error); ok {
r1 = rf(ctx, projectID, eventType)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// List provides a mock function with given fields: ctx, query
func (_m *Manager) List(ctx context.Context, query *q.Query) ([]*model.Policy, error) {
ret := _m.Called(ctx, query)
var r0 []*model.Policy
if rf, ok := ret.Get(0).(func(context.Context, *q.Query) []*model.Policy); ok {
r0 = rf(ctx, query)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Policy)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *q.Query) error); ok {
r1 = rf(ctx, query)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Test provides a mock function with given fields: _a0
func (_m *Manager) Test(_a0 *model.Policy) error {
ret := _m.Called(_a0)
var r0 error
if rf, ok := ret.Get(0).(func(*model.Policy) error); ok {
r0 = rf(_a0)
} else {
r0 = ret.Error(0)
}
return r0
}
// Update provides a mock function with given fields: ctx, _a1
func (_m *Manager) Update(ctx context.Context, _a1 *model.Policy) error {
ret := _m.Called(ctx, _a1)
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, *model.Policy) error); ok {
r0 = rf(ctx, _a1)
} else {
r0 = ret.Error(0)
}
return r0
}

View File

@ -33,6 +33,9 @@ package pkg
//go:generate mockery --case snake --dir ../../pkg/robot --name Manager --output ./robot --outpkg robot
//go:generate mockery --case snake --dir ../../pkg/robot/dao --name DAO --output ./robot/dao --outpkg dao
//go:generate mockery --case snake --dir ../../pkg/repository/dao --name DAO --output ./repository/dao --outpkg dao
//go:generate mockery --case snake --dir ../../pkg/notification/job/dao --name DAO --output ./notification/job/dao --outpkg dao
//go:generate mockery --case snake --dir ../../pkg/notification/policy/dao --name DAO --output ./notification/policy/dao --outpkg dao
//go:generate mockery --case snake --dir ../../pkg/notification/policy --name Manager --output ./notification/policy --outpkg notification
//go:generate mockery --case snake --dir ../../pkg/immutable/dao --name DAO --output ./immutable/dao --outpkg dao
//go:generate mockery --case snake --dir ../../pkg/ldap --name Manager --output ./ldap --outpkg ldap
//go:generate mockery --case snake --dir ../../pkg/allowlist --name Manager --output ./allowlist --outpkg robot