Add immutable tag API

Signed-off-by: stonezdj <stonezdj@gmail.com>
This commit is contained in:
stonezdj 2019-09-23 18:04:36 +08:00
parent ec559b0585
commit cc22a175b9
11 changed files with 646 additions and 4 deletions

View File

@ -3989,7 +3989,124 @@ paths:
description: User have no permission to list webhook jobs of the project.
'500':
description: Unexpected internal errors.
'/projects/{project_id}/immutabletagrules':
get:
summary: List all immutable tag rules of current project
description: |
This endpoint returns the immutable tag rules 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 immutable tag rules successfully.
schema:
type: array
items:
$ref: '#/definitions/ImmutableTagRule'
'400':
description: Illegal format of provided ID value.
'401':
description: User need to log in first.
'403':
description: User have no permission to list immutable tag rules of the project.
'500':
description: Unexpected internal errors.
post:
summary: Add an immutable tag rule to current project
description: |
This endpoint add an immutable tag rule to the project
parameters:
- name: project_id
in: path
type: integer
format: int64
required: true
description: Relevant project ID.
- name: immutabletagrule
in: body
schema:
$ref: '#/definitions/ImmutableTagRule'
tags:
- Products
responses:
'200':
description: Add the immutable tag rule 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 immutable tag rule of the project.
'500':
description: Internal server errors.
'/projects/{project_id}/immutabletagrules/{id}':
put:
summary: Update the immutable tag rule or enable or disable the rule
parameters:
- name: project_id
in: path
type: integer
format: int64
required: true
description: Relevant project ID.
- name: id
in: path
type: integer
format: int64
required: true
description: Immutable tag rule ID.
- name: immutabletagrule
in: body
schema:
$ref: '#/definitions/ImmutableTagRule'
tags:
- Products
responses:
'200':
description: Update the immutable tag rule 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 the immutable tag rule of the project.
'500':
description: Internal server errors.
delete:
summary: Delete the immutable tag rule.
parameters:
- name: project_id
in: path
type: integer
format: int64
required: true
description: Relevant project ID.
- name: id
in: path
type: integer
format: int64
required: true
description: Immutable tag rule ID.
tags:
- Products
responses:
'200':
description: Delete the immutable tag rule 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 immutable tags of the project.
'500':
description: Internal server errors.
'/retentions/metadatas':
get:
summary: Get Retention Metadatas
@ -6269,10 +6386,22 @@ definitions:
type: integer
retained:
type: integer
QuotaSwitcher:
type: object
properties:
enabled:
type: boolean
description: The quota is enable or disable
ImmutableTagRule:
type: object
properties:
id:
type: integer
format: int64
project_id:
type: integer
format: int64
tag_filter:
type: string
enabled:
type: boolean

View File

@ -15,7 +15,8 @@ func CreateImmutableRule(ir *models.ImmutableRule) (int64, error) {
}
// UpdateImmutableRule update the immutable rules
func UpdateImmutableRule(projectID int, ir *models.ImmutableRule) (int64, error) {
func UpdateImmutableRule(projectID int64, ir *models.ImmutableRule) (int64, error) {
ir.ProjectID = projectID
o := GetOrmer()
return o.Update(ir, "TagFilter")
}

View File

@ -49,6 +49,7 @@ const (
ResourceReplicationTask = Resource("replication-task")
ResourceRepository = Resource("repository")
ResourceTagRetention = Resource("tag-retention")
ResourceImmutableTag = Resource("immutable-tag")
ResourceRepositoryLabel = Resource("repository-label")
ResourceRepositoryTag = Resource("repository-tag")
ResourceRepositoryTagLabel = Resource("repository-tag-label")

View File

@ -95,6 +95,11 @@ var (
{Resource: rbac.ResourceTagRetention, Action: rbac.ActionList},
{Resource: rbac.ResourceTagRetention, Action: rbac.ActionOperate},
{Resource: rbac.ResourceImmutableTag, Action: rbac.ActionCreate},
{Resource: rbac.ResourceImmutableTag, Action: rbac.ActionUpdate},
{Resource: rbac.ResourceImmutableTag, Action: rbac.ActionDelete},
{Resource: rbac.ResourceImmutableTag, Action: rbac.ActionList},
{Resource: rbac.ResourceLabel, Action: rbac.ActionCreate},
{Resource: rbac.ResourceLabel, Action: rbac.ActionRead},
{Resource: rbac.ResourceLabel, Action: rbac.ActionUpdate},

View File

@ -68,6 +68,11 @@ var (
{Resource: rbac.ResourceTagRetention, Action: rbac.ActionList},
{Resource: rbac.ResourceTagRetention, Action: rbac.ActionOperate},
{Resource: rbac.ResourceImmutableTag, Action: rbac.ActionCreate},
{Resource: rbac.ResourceImmutableTag, Action: rbac.ActionUpdate},
{Resource: rbac.ResourceImmutableTag, Action: rbac.ActionDelete},
{Resource: rbac.ResourceImmutableTag, Action: rbac.ActionList},
{Resource: rbac.ResourceRepositoryLabel, Action: rbac.ActionCreate},
{Resource: rbac.ResourceRepositoryLabel, Action: rbac.ActionDelete},
{Resource: rbac.ResourceRepositoryLabel, Action: rbac.ActionList},
@ -153,6 +158,11 @@ var (
{Resource: rbac.ResourceTagRetention, Action: rbac.ActionList},
{Resource: rbac.ResourceTagRetention, Action: rbac.ActionOperate},
{Resource: rbac.ResourceImmutableTag, Action: rbac.ActionCreate},
{Resource: rbac.ResourceImmutableTag, Action: rbac.ActionUpdate},
{Resource: rbac.ResourceImmutableTag, Action: rbac.ActionDelete},
{Resource: rbac.ResourceImmutableTag, Action: rbac.ActionList},
{Resource: rbac.ResourceRepositoryLabel, Action: rbac.ActionCreate},
{Resource: rbac.ResourceRepositoryLabel, Action: rbac.ActionDelete},
{Resource: rbac.ResourceRepositoryLabel, Action: rbac.ActionList},

View File

@ -180,6 +180,7 @@ func runCodeCheckingCases(t *testing.T, cases ...*codeCheckingCase) {
if c.postFunc != nil {
if err := c.postFunc(resp); err != nil {
t.Logf("error in running post function: %v", err)
t.Error(err)
}
}
}

View File

@ -177,7 +177,8 @@ func init() {
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")
beego.Router("/api/projects/:pid([0-9]+)/immutabletagrules", &ImmutableTagRuleAPI{}, "get:List;post:Post")
beego.Router("/api/projects/:pid([0-9]+)/immutabletagrules/:id([0-9]+)", &ImmutableTagRuleAPI{})
// Charts are controlled under projects
chartRepositoryAPIType := &ChartRepositoryAPI{}
beego.Router("/api/chartrepo/health", chartRepositoryAPIType, "get:GetHealthStatus")

View File

@ -0,0 +1,166 @@
package api
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"strconv"
"strings"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/pkg/immutabletag"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
)
// ImmutableTagRuleAPI ...
type ImmutableTagRuleAPI struct {
BaseController
manager immutabletag.RuleManager
projectID int64
ID int64
}
// Prepare validates the user and projectID
func (itr *ImmutableTagRuleAPI) Prepare() {
itr.BaseController.Prepare()
if !itr.SecurityCtx.IsAuthenticated() {
itr.SendUnAuthorizedError(errors.New("Unauthorized"))
return
}
pid, err := itr.GetInt64FromPath(":pid")
if err != nil || pid <= 0 {
text := "invalid project ID: "
if err != nil {
text += err.Error()
} else {
text += fmt.Sprintf("%d", pid)
}
itr.SendBadRequestError(errors.New(text))
return
}
itr.projectID = pid
ruleID, err := itr.GetInt64FromPath(":id")
if err == nil || ruleID > 0 {
itr.ID = ruleID
}
itr.manager = immutabletag.NewDefaultRuleManager()
if strings.EqualFold(itr.Ctx.Request.Method, "get") {
if !itr.requireAccess(rbac.ActionList) {
return
}
} else if strings.EqualFold(itr.Ctx.Request.Method, "put") {
if !itr.requireAccess(rbac.ActionUpdate) {
return
}
} else if strings.EqualFold(itr.Ctx.Request.Method, "post") {
if !itr.requireAccess(rbac.ActionCreate) {
return
}
} else if strings.EqualFold(itr.Ctx.Request.Method, "delete") {
if !itr.requireAccess(rbac.ActionDelete) {
return
}
}
}
func (itr *ImmutableTagRuleAPI) requireAccess(action rbac.Action) bool {
return itr.RequireProjectAccess(itr.projectID, action, rbac.ResourceImmutableTag)
}
// List list all immutable tag rules of current project
func (itr *ImmutableTagRuleAPI) List() {
rules, err := itr.manager.QueryImmutableRuleByProjectID(itr.projectID)
if err != nil {
itr.SendInternalServerError(err)
return
}
itr.WriteJSONData(rules)
}
// Post create immutable tag rule
func (itr *ImmutableTagRuleAPI) Post() {
ir := &models.ImmutableRule{}
if err := itr.DecodeJSONReq(ir); err != nil {
itr.SendBadRequestError(fmt.Errorf("the filter must be a valid json, failed to parse json, error %+v", err))
return
}
if !isValidSelectorJSON(ir.TagFilter) {
itr.SendBadRequestError(fmt.Errorf("the filter should be a valid json"))
return
}
ir.ProjectID = itr.projectID
id, err := itr.manager.CreateImmutableRule(ir)
if err != nil {
itr.SendInternalServerError(err)
return
}
itr.Redirect(http.StatusCreated, strconv.FormatInt(id, 10))
}
// Delete delete immutable tag rule
func (itr *ImmutableTagRuleAPI) Delete() {
if itr.ID <= 0 {
itr.SendBadRequestError(fmt.Errorf("invalid immutable rule id %d", itr.ID))
return
}
_, err := itr.manager.DeleteImmutableRule(itr.ID)
if err != nil {
itr.SendInternalServerError(err)
return
}
}
// Put update an immutable tag rule
func (itr *ImmutableTagRuleAPI) Put() {
ir := &models.ImmutableRule{}
if err := itr.DecodeJSONReq(ir); err != nil {
itr.SendInternalServerError(err)
return
}
ir.ID = itr.ID
ir.ProjectID = itr.projectID
if itr.ID <= 0 {
itr.SendBadRequestError(fmt.Errorf("invalid immutable rule id %d", itr.ID))
return
}
if len(ir.TagFilter) == 0 {
if _, err := itr.manager.EnableImmutableRule(itr.ID, ir.Enabled); err != nil {
itr.SendInternalServerError(err)
return
}
} else {
if !isValidSelectorJSON(ir.TagFilter) {
itr.SendBadRequestError(fmt.Errorf("the filter should be a valid json"))
return
}
if _, err := itr.manager.UpdateImmutableRule(itr.ID, ir); err != nil {
itr.SendInternalServerError(err)
return
}
}
}
func isValidSelectorJSON(filter string) bool {
tagSector := &rule.Metadata{}
err := json.Unmarshal([]byte(filter), tagSector)
if err != nil {
log.Errorf("The json is %v", filter)
return false
}
return true
}

View File

@ -0,0 +1,264 @@
package api
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/pkg/immutabletag"
)
func TestImmutableTagRuleAPI_List(t *testing.T) {
tagFilter := `{
"id":0,
"priority":0,
"disabled":false,
"action":"immutable",
"template":"immutable_template",
"tag_selectors":[{"kind":"doublestar","decoration":"matches","pattern":"**"}],
"scope_selectors":{"repository":[{"kind":"doublestar","decoration":"repoMatches","pattern":"**"}]}
}`
mgr := immutabletag.NewDefaultRuleManager()
id, err := mgr.CreateImmutableRule(&models.ImmutableRule{ProjectID: 1, TagFilter: tagFilter})
if err != nil {
t.Error(err)
}
defer mgr.DeleteImmutableRule(id)
cases := []*codeCheckingCase{
// 401
{
request: &testingRequest{
method: http.MethodGet,
url: "/api/projects/1/immutabletagrules",
},
code: http.StatusUnauthorized,
},
// 200
{
request: &testingRequest{
method: http.MethodGet,
url: "/api/projects/1/immutabletagrules",
credential: admin,
},
postFunc: func(responseRecorder *httptest.ResponseRecorder) error {
var rules []models.ImmutableRule
err := json.Unmarshal([]byte(responseRecorder.Body.String()), &rules)
if err != nil {
return err
}
if len(rules) <= 0 {
return fmt.Errorf("no rules found")
}
if rules[0].TagFilter != tagFilter {
return fmt.Errorf("rule is not expected. actual: %v", responseRecorder.Body.String())
}
return nil
},
code: http.StatusOK,
},
// 200
{
request: &testingRequest{
method: http.MethodGet,
url: "/api/projects/1/immutabletagrules",
credential: projAdmin,
},
code: http.StatusOK,
},
// 403
{
request: &testingRequest{
method: http.MethodGet,
url: "/api/projects/1/immutabletagrules",
credential: projGuest,
},
code: http.StatusForbidden,
},
}
runCodeCheckingCases(t, cases...)
}
func TestImmutableTagRuleAPI_Post(t *testing.T) {
tagFilter := `{
"id":0,
"priority":0,
"disabled":false,
"action":"immutable",
"template":"immutable_template",
"tag_selectors":[{"kind":"doublestar","decoration":"matches","pattern":"**"}],
"scope_selectors":{"repository":[{"kind":"doublestar","decoration":"repoMatches","pattern":"**"}]}
}`
body := &models.ImmutableRule{ProjectID: 1, TagFilter: tagFilter}
cases := []*codeCheckingCase{
// 401
{
request: &testingRequest{
method: http.MethodPost,
url: "/api/projects/1/immutabletagrules",
bodyJSON: body,
},
code: http.StatusUnauthorized,
},
// 200
{
request: &testingRequest{
method: http.MethodPost,
url: "/api/projects/1/immutabletagrules",
credential: admin,
bodyJSON: body,
},
code: http.StatusCreated,
},
// 200
{
request: &testingRequest{
method: http.MethodPost,
url: "/api/projects/1/immutabletagrules",
credential: projAdmin,
bodyJSON: body,
},
code: http.StatusCreated,
},
// 403
{
request: &testingRequest{
method: http.MethodPost,
url: "/api/projects/1/immutabletagrules",
credential: projGuest,
bodyJSON: body,
},
code: http.StatusForbidden,
},
}
runCodeCheckingCases(t, cases...)
}
func TestImmutableTagRuleAPI_Put(t *testing.T) {
tagFilter := `{
"id":0,
"priority":0,
"disabled":false,
"action":"immutable",
"template":"immutable_template",
"tag_selectors":[{"kind":"doublestar","decoration":"matches","pattern":"**"}],
"scope_selectors":{"repository":[{"kind":"doublestar","decoration":"repoMatches","pattern":"**"}]}
}`
tagFilter2 := `{
"id":0,
"priority":0,
"disabled":false,
"action":"immutable",
"template":"immutable_template",
"tag_selectors":[{"kind":"doublestar","decoration":"matches","pattern":"release-1.6.0"}],
"scope_selectors":{"repository":[{"kind":"doublestar","decoration":"repoMatches","pattern":"regids"}]}
}`
mgr := immutabletag.NewDefaultRuleManager()
id, err := mgr.CreateImmutableRule(&models.ImmutableRule{ProjectID: 1, TagFilter: tagFilter})
if err != nil {
t.Error(err)
}
defer mgr.DeleteImmutableRule(id)
url := fmt.Sprintf("/api/projects/1/immutabletagrules/%d", id)
body := &models.ImmutableRule{ID: id, ProjectID: 1, TagFilter: tagFilter2}
cases := []*codeCheckingCase{
// 401
{
request: &testingRequest{
method: http.MethodPut,
url: url,
bodyJSON: body,
},
code: http.StatusUnauthorized,
},
// 200
{
request: &testingRequest{
method: http.MethodPut,
url: url,
credential: admin,
bodyJSON: body,
},
code: http.StatusOK,
},
// 200
{
request: &testingRequest{
method: http.MethodPut,
url: url,
credential: projAdmin,
bodyJSON: body,
},
code: http.StatusOK,
},
// 403
{
request: &testingRequest{
method: http.MethodPut,
url: url,
credential: projGuest,
bodyJSON: body,
},
code: http.StatusForbidden,
},
}
runCodeCheckingCases(t, cases...)
}
func TestImmutableTagRuleAPI_Delete(t *testing.T) {
tagFilter := `{
"id":0,
"priority":0,
"disabled":false,
"action":"immutable",
"template":"immutable_template",
"tag_selectors":[{"kind":"doublestar","decoration":"matches","pattern":"**"}],
"scope_selectors":{"repository":[{"kind":"doublestar","decoration":"repoMatches","pattern":"**"}]}
}`
mgr := immutabletag.NewDefaultRuleManager()
id, err := mgr.CreateImmutableRule(&models.ImmutableRule{ProjectID: 1, TagFilter: tagFilter})
if err != nil {
t.Error(err)
}
defer mgr.DeleteImmutableRule(id)
url := fmt.Sprintf("/api/projects/1/immutabletagrules/%d", id)
cases := []*codeCheckingCase{
// 401
{
request: &testingRequest{
method: http.MethodDelete,
url: url,
},
code: http.StatusUnauthorized,
},
// 403
{
request: &testingRequest{
method: http.MethodDelete,
url: url,
credential: projGuest,
},
code: http.StatusForbidden,
},
// 200
{
request: &testingRequest{
method: http.MethodDelete,
url: url,
credential: projAdmin,
},
code: http.StatusOK,
},
}
runCodeCheckingCases(t, cases...)
}

View File

@ -121,6 +121,9 @@ func initRouters() {
beego.Router("/api/projects/:pid([0-9]+)/webhook/jobs/", &api.NotificationJobAPI{}, "get:List")
beego.Router("/api/projects/:pid([0-9]+)/immutabletagrules", &api.ImmutableTagRuleAPI{}, "get:List;post:Post")
beego.Router("/api/projects/:pid([0-9]+)/immutabletagrules/:id([0-9]+)", &api.ImmutableTagRuleAPI{})
beego.Router("/api/internal/configurations", &api.ConfigAPI{}, "get:GetInternalConfig;put:Put")
beego.Router("/api/configurations", &api.ConfigAPI{}, "get:Get;put:Put")
beego.Router("/api/statistics", &api.StatisticAPI{})
@ -164,6 +167,8 @@ func initRouters() {
beego.Router("/api/retentions/:id/executions", &api.RetentionAPI{}, "get:ListRetentionExecs")
beego.Router("/api/retentions/:id/executions/:eid/tasks", &api.RetentionAPI{}, "get:ListRetentionExecTasks")
beego.Router("/api/retentions/:id/executions/:eid/tasks/:tid", &api.RetentionAPI{}, "get:GetRetentionExecTaskLog")
beego.Router("/api/projects/:pid([0-9]+)/immutabletagrules", &api.ImmutableTagRuleAPI{}, "get:List;post:Post")
beego.Router("/api/projects/:pid([0-9]+)/immutabletagrules/:id([0-9]+)", &api.ImmutableTagRuleAPI{})
beego.Router("/v2/*", &controllers.RegistryProxy{}, "*:Handle")

View File

@ -0,0 +1,59 @@
package immutabletag
import (
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/models"
)
// RuleManager ...
type RuleManager interface {
// CreateImmutableRule creates the Immutable Rule
CreateImmutableRule(ir *models.ImmutableRule) (int64, error)
// UpdateImmutableRule update the immutable rules
UpdateImmutableRule(projectID int64, ir *models.ImmutableRule) (int64, error)
// EnableImmutableRule enable/disable immutable rules
EnableImmutableRule(id int64, enabled bool) (int64, error)
// GetImmutableRule get immutable rule
GetImmutableRule(id int64) (*models.ImmutableRule, error)
// QueryImmutableRuleByProjectID get all immutable rule by project
QueryImmutableRuleByProjectID(projectID int64) ([]models.ImmutableRule, error)
// QueryEnabledImmutableRuleByProjectID get all enabled immutable rule by project
QueryEnabledImmutableRuleByProjectID(projectID int64) ([]models.ImmutableRule, error)
// DeleteImmutableRule delete the immutable rule
DeleteImmutableRule(id int64) (int64, error)
}
type defaultRuleManager struct{}
func (drm *defaultRuleManager) CreateImmutableRule(ir *models.ImmutableRule) (int64, error) {
return dao.CreateImmutableRule(ir)
}
func (drm *defaultRuleManager) UpdateImmutableRule(projectID int64, ir *models.ImmutableRule) (int64, error) {
return dao.UpdateImmutableRule(projectID, ir)
}
func (drm *defaultRuleManager) EnableImmutableRule(id int64, enabled bool) (int64, error) {
return dao.ToggleImmutableRule(id, enabled)
}
func (drm *defaultRuleManager) GetImmutableRule(id int64) (*models.ImmutableRule, error) {
return dao.GetImmutableRule(id)
}
func (drm *defaultRuleManager) QueryImmutableRuleByProjectID(projectID int64) ([]models.ImmutableRule, error) {
return dao.QueryImmutableRuleByProjectID(projectID)
}
func (drm *defaultRuleManager) QueryEnabledImmutableRuleByProjectID(projectID int64) ([]models.ImmutableRule, error) {
return dao.QueryEnabledImmutableRuleByProjectID(projectID)
}
func (drm *defaultRuleManager) DeleteImmutableRule(id int64) (int64, error) {
return dao.DeleteImmutableRule(id)
}
// NewDefaultRuleManager return a new instance of defaultRuleManager
func NewDefaultRuleManager() RuleManager {
return &defaultRuleManager{}
}