Fix conflicts of list-repository.component.spec.ts

This commit is contained in:
Steven Zou 2017-06-06 13:36:44 +08:00
commit 3e02f756a3
54 changed files with 949 additions and 683 deletions

View File

@ -861,24 +861,18 @@ paths:
description: |
This endpoint let user see the recent operation logs of the projects which he is member of
parameters:
- name: lines
- name: page
in: query
type: integer
format: int32
required: false
description: The number of logs to be shown, default is 10 if lines, start_time, end_time are not provided.
- name: start_time
description: The page nubmer, default is 1.
- name: page_size
in: query
type: integer
format: int64
format: int32
required: false
description: The start time of logs to be shown in unix timestap
- name: end_time
in: query
type: integer
format: int64
required: false
description: The end time of logs to be shown in unix timestap
description: The size of per page, default is 10, maximum is 100.
tags:
- Products
responses:
@ -889,7 +883,7 @@ paths:
items:
$ref: '#/definitions/AccessLog'
400:
description: Bad request because of invalid parameter of lines or start_time or end_time.
description: Bad request because of invalid parameters.
401:
description: User need to login first.
500:

View File

@ -15,8 +15,7 @@
package dao
import (
"strings"
"github.com/astaxie/beego/orm"
"github.com/vmware/harbor/src/common/models"
"github.com/vmware/harbor/src/common/utils/log"
)
@ -29,155 +28,61 @@ func AddAccessLog(accessLog models.AccessLog) error {
}
// GetTotalOfAccessLogs ...
func GetTotalOfAccessLogs(query models.AccessLog) (int64, error) {
o := GetOrmer()
queryParam := []interface{}{}
sql := `select count(*) from access_log al
where al.project_id = ?`
queryParam = append(queryParam, query.ProjectID)
sql += genFilterClauses(query, &queryParam)
var total int64
if err := o.Raw(sql, queryParam).QueryRow(&total); err != nil {
return 0, err
}
return total, nil
func GetTotalOfAccessLogs(query *models.LogQueryParam) (int64, error) {
return logQueryConditions(query).Count()
}
//GetAccessLogs gets access logs according to different conditions
func GetAccessLogs(query models.AccessLog, limit, offset int64) ([]models.AccessLog, error) {
o := GetOrmer()
func GetAccessLogs(query *models.LogQueryParam) ([]models.AccessLog, error) {
qs := logQueryConditions(query).OrderBy("-op_time")
queryParam := []interface{}{}
sql := `select al.log_id, al.username, al.repo_name,
al.repo_tag, al.operation, al.op_time
from access_log al
where al.project_id = ? `
queryParam = append(queryParam, query.ProjectID)
if query != nil && query.Pagination != nil {
size := query.Pagination.Size
if size > 0 {
qs = qs.Limit(size)
sql += genFilterClauses(query, &queryParam)
sql += ` order by al.op_time desc `
sql = paginateForRawSQL(sql, limit, offset)
logs := []models.AccessLog{}
_, err := o.Raw(sql, queryParam).QueryRows(&logs)
if err != nil {
return logs, err
}
return logs, nil
}
func genFilterClauses(query models.AccessLog, queryParam *[]interface{}) string {
sql := ""
if query.Username != "" {
sql += ` and al.username like ? `
*queryParam = append(*queryParam, "%"+escape(query.Username)+"%")
}
if query.Operation != "" {
sql += ` and al.operation = ? `
*queryParam = append(*queryParam, query.Operation)
}
if query.RepoName != "" {
sql += ` and al.repo_name = ? `
*queryParam = append(*queryParam, query.RepoName)
}
if query.RepoTag != "" {
sql += ` and al.repo_tag = ? `
*queryParam = append(*queryParam, query.RepoTag)
}
if query.Keywords != "" {
sql += ` and al.operation in ( `
keywordList := strings.Split(query.Keywords, "/")
num := len(keywordList)
for i := 0; i < num; i++ {
if keywordList[i] != "" {
if i == num-1 {
sql += `?)`
} else {
sql += `?,`
}
*queryParam = append(*queryParam, keywordList[i])
page := query.Pagination.Page
if page > 0 {
qs = qs.Offset((page - 1) * size)
}
}
}
if query.BeginTimestamp > 0 {
sql += ` and al.op_time >= ? `
*queryParam = append(*queryParam, query.BeginTime)
}
if query.EndTimestamp > 0 {
sql += ` and al.op_time <= ? `
*queryParam = append(*queryParam, query.EndTime)
}
return sql
logs := []models.AccessLog{}
_, err := qs.All(&logs)
return logs, err
}
//GetRecentLogs returns recent logs according to parameters
func GetRecentLogs(username string, linesNum int, startTime, endTime string) ([]models.AccessLog, error) {
logs := []models.AccessLog{}
func logQueryConditions(query *models.LogQueryParam) orm.QuerySeter {
qs := GetOrmer().QueryTable(&models.AccessLog{})
isAdmin, err := IsAdminRole(username)
if err != nil {
return logs, err
if query == nil {
return qs
}
queryParam := []interface{}{}
sql := `select log_id, username, project_id, repo_name, repo_tag, GUID, operation, op_time
from access_log `
hasWhere := false
if !isAdmin {
sql += ` where project_id in
(select distinct project_id
from project_member pm
join user u
on pm.user_id = u.user_id
where u.username = ?) `
queryParam = append(queryParam, username)
hasWhere = true
if len(query.ProjectIDs) > 0 {
qs = qs.Filter("project_id__in", query.ProjectIDs)
}
if len(query.Username) != 0 {
qs = qs.Filter("username__contains", query.Username)
}
if len(query.Repository) != 0 {
qs = qs.Filter("repo_name", query.Repository)
}
if len(query.Tag) != 0 {
qs = qs.Filter("repo_tag", query.Tag)
}
if len(query.Operations) > 0 {
qs = qs.Filter("operation__in", query.Operations)
}
if query.BeginTime != nil {
qs = qs.Filter("op_time__gte", query.BeginTime)
}
if query.EndTime != nil {
qs = qs.Filter("op_time__lte", query.EndTime)
}
if startTime != "" {
if hasWhere {
sql += " and op_time >= ?"
} else {
sql += " where op_time >= ?"
hasWhere = true
}
queryParam = append(queryParam, startTime)
}
if endTime != "" {
if hasWhere {
sql += " and op_time <= ?"
} else {
sql += " where op_time <= ?"
hasWhere = true
}
queryParam = append(queryParam, endTime)
}
sql += " order by op_time desc"
if linesNum != 0 {
sql += " limit ?"
queryParam = append(queryParam, linesNum)
}
_, err = GetOrmer().Raw(sql, queryParam).QueryRows(&logs)
if err != nil {
return logs, err
}
return logs, nil
return qs
}
// CountPull ...

View File

@ -504,7 +504,7 @@ func TestChangeUserPasswordWithIncorrectOldPassword(t *testing.T) {
}
func TestQueryRelevantProjectsWhenNoProjectAdded(t *testing.T) {
projects, err := SearchProjects(currentUser.UserID)
projects, err := GetHasReadPermProjects(currentUser.Username)
if err != nil {
t.Errorf("Error occurred in QueryRelevantProjects: %v", err)
}
@ -570,11 +570,11 @@ func TestGetAccessLog(t *testing.T) {
t.Errorf("failed to add access log: %v", err)
}
queryAccessLog := models.AccessLog{
Username: currentUser.Username,
ProjectID: currentProject.ProjectID,
query := &models.LogQueryParam{
Username: currentUser.Username,
ProjectIDs: []int64{currentProject.ProjectID},
}
accessLogs, err := GetAccessLogs(queryAccessLog, 1000, 0)
accessLogs, err := GetAccessLogs(query)
if err != nil {
t.Errorf("Error occurred in GetAccessLog: %v", err)
}
@ -587,11 +587,11 @@ func TestGetAccessLog(t *testing.T) {
}
func TestGetTotalOfAccessLogs(t *testing.T) {
queryAccessLog := models.AccessLog{
Username: currentUser.Username,
ProjectID: currentProject.ProjectID,
query := &models.LogQueryParam{
Username: currentUser.Username,
ProjectIDs: []int64{currentProject.ProjectID},
}
total, err := GetTotalOfAccessLogs(queryAccessLog)
total, err := GetTotalOfAccessLogs(query)
if err != nil {
t.Fatalf("failed to get total of access log: %v", err)
}
@ -617,7 +617,15 @@ func TestAddAccessLog(t *testing.T) {
if err != nil {
t.Errorf("Error occurred in AddAccessLog: %v", err)
}
accessLogList, err = GetAccessLogs(accessLog, 1000, 0)
query := &models.LogQueryParam{
Username: accessLog.Username,
ProjectIDs: []int64{accessLog.ProjectID},
Repository: accessLog.RepoName,
Tag: accessLog.RepoTag,
Operations: []string{accessLog.Operation},
}
accessLogList, err = GetAccessLogs(query)
if err != nil {
t.Errorf("Error occurred in GetAccessLog: %v", err)
}
@ -819,7 +827,7 @@ func TestGetProjects(t *testing.T) {
func TestGetPublicProjects(t *testing.T) {
value := true
projects, err := GetProjects(&models.QueryParam{
projects, err := GetProjects(&models.ProjectQueryParam{
Public: &value,
})
if err != nil {
@ -953,16 +961,6 @@ func TestChangeUserProfile(t *testing.T) {
}
}
func TestGetRecentLogs(t *testing.T) {
logs, err := GetRecentLogs(currentUser.Username, 10, "2016-05-13 00:00:00", time.Now().String())
if err != nil {
t.Errorf("error occured in getting recent logs, error: %v", err)
}
if len(logs) <= 0 {
t.Errorf("get logs error, expected: %d, actual: %d", 1, len(logs))
}
}
var targetID, policyID, policyID2, policyID3, jobID, jobID2, jobID3 int64
func TestAddRepTarget(t *testing.T) {

View File

@ -156,11 +156,18 @@ func ToggleProjectPublicity(projectID int64, publicity int) error {
return err
}
// SearchProjects returns a project list,
// GetHasReadPermProjects returns a project list,
// which satisfies the following conditions:
// 1. the project is not deleted
// 2. the prject is public or the user is a member of the project
func SearchProjects(userID int) ([]*models.Project, error) {
func GetHasReadPermProjects(username string) ([]*models.Project, error) {
user, err := GetUser(models.User{
Username: username,
})
if err != nil {
return nil, err
}
o := GetOrmer()
sql :=
@ -174,7 +181,7 @@ func SearchProjects(userID int) ([]*models.Project, error) {
var projects []*models.Project
if _, err := o.Raw(sql, userID).QueryRows(&projects); err != nil {
if _, err := o.Raw(sql, user.UserID).QueryRows(&projects); err != nil {
return nil, err
}
@ -183,7 +190,7 @@ func SearchProjects(userID int) ([]*models.Project, error) {
// GetTotalOfProjects returns the total count of projects
// according to the query conditions
func GetTotalOfProjects(query *models.QueryParam) (int64, error) {
func GetTotalOfProjects(query *models.ProjectQueryParam) (int64, error) {
var (
owner string
@ -203,7 +210,7 @@ func GetTotalOfProjects(query *models.QueryParam) (int64, error) {
}
}
sql, params := queryConditions(owner, name, public, member, role)
sql, params := projectQueryConditions(owner, name, public, member, role)
sql = `select count(*) ` + sql
@ -213,7 +220,7 @@ func GetTotalOfProjects(query *models.QueryParam) (int64, error) {
}
// GetProjects returns a project list according to the query conditions
func GetProjects(query *models.QueryParam) ([]*models.Project, error) {
func GetProjects(query *models.ProjectQueryParam) ([]*models.Project, error) {
var (
owner string
@ -239,7 +246,7 @@ func GetProjects(query *models.QueryParam) ([]*models.Project, error) {
}
}
sql, params := queryConditions(owner, name, public, member, role)
sql, params := projectQueryConditions(owner, name, public, member, role)
sql = `select distinct p.project_id, p.name, p.public, p.owner_id,
p.creation_time, p.update_time ` + sql
@ -258,7 +265,7 @@ func GetProjects(query *models.QueryParam) ([]*models.Project, error) {
return projects, err
}
func queryConditions(owner, name string, public *bool, member string,
func projectQueryConditions(owner, name string, public *bool, member string,
role int) (string, []interface{}) {
params := []interface{}{}

View File

@ -152,12 +152,11 @@ func FilterRepPolicies(name string, projectID int64) ([]*models.RepPolicy, error
var args []interface{}
sql := `select rp.id, rp.project_id, p.name as project_name, rp.target_id,
sql := `select rp.id, rp.project_id, rp.target_id,
rt.name as target_name, rp.name, rp.enabled, rp.description,
rp.cron_str, rp.start_time, rp.creation_time, rp.update_time,
count(rj.status) as error_job_count
from replication_policy rp
left join project p on rp.project_id=p.project_id
left join replication_target rt on rp.target_id=rt.id
left join replication_job rj on rp.id=rj.policy_id and (rj.status="error"
or rj.status="retrying")

View File

@ -125,50 +125,10 @@ func GetTotalOfRepositories(name string) (int64, error) {
return qs.Count()
}
// GetTotalOfPublicRepositories ...
func GetTotalOfPublicRepositories(name string) (int64, error) {
params := []interface{}{}
sql := `select count(*) from repository r
join project p
on r.project_id = p.project_id and p.public = 1 `
if len(name) != 0 {
sql += ` where r.name like ?`
params = append(params, "%"+escape(name)+"%")
}
var total int64
err := GetOrmer().Raw(sql, params).QueryRow(&total)
return total, err
}
// GetTotalOfUserRelevantRepositories ...
func GetTotalOfUserRelevantRepositories(userID int, name string) (int64, error) {
params := []interface{}{}
sql := `select count(*)
from repository r
join (
select p.project_id, p.public
from project p
join project_member pm
on p.project_id = pm.project_id
where pm.user_id = ?
) as pp
on r.project_id = pp.project_id `
params = append(params, userID)
if len(name) != 0 {
sql += ` where r.name like ?`
params = append(params, "%"+escape(name)+"%")
}
var total int64
err := GetOrmer().Raw(sql, params).QueryRow(&total)
return total, err
}
// GetTotalOfRepositoriesByProject ...
func GetTotalOfRepositoriesByProject(projectID int64, name string) (int64, error) {
func GetTotalOfRepositoriesByProject(projectIDs []int64, name string) (int64, error) {
qs := GetOrmer().QueryTable(&models.RepoRecord{}).
Filter("ProjectID", projectID)
Filter("project_id__in", projectIDs)
if len(name) != 0 {
qs = qs.Filter("Name__contains", name)

View File

@ -88,78 +88,6 @@ func TestGetTotalOfRepositories(t *testing.T) {
}
}
func TestGetTotalOfPublicRepositories(t *testing.T) {
total, err := GetTotalOfPublicRepositories("")
if err != nil {
t.Fatalf("failed to get total of public repositoreis: %v", err)
}
if err := addRepository(repository); err != nil {
t.Fatalf("failed to add repository %s: %v", name, err)
}
defer func() {
if err := deleteRepository(name); err != nil {
t.Fatalf("failed to delete repository %s: %v", name, err)
}
}()
n, err := GetTotalOfPublicRepositories("")
if err != nil {
t.Fatalf("failed to get total of public repositoreis: %v", err)
}
if n != total+1 {
t.Errorf("unexpected total: %d != %d", n, total+1)
}
}
func TestGetTotalOfUserRelevantRepositories(t *testing.T) {
total, err := GetTotalOfUserRelevantRepositories(1, "")
if err != nil {
t.Fatalf("failed to get total of repositoreis for user %d: %v", 1, err)
}
if err := addRepository(repository); err != nil {
t.Fatalf("failed to add repository %s: %v", name, err)
}
defer func() {
if err := deleteRepository(name); err != nil {
t.Fatalf("failed to delete repository %s: %v", name, err)
}
}()
users, err := GetUserByProject(1, models.User{})
if err != nil {
t.Fatalf("failed to list members of project %d: %v", 1, err)
}
exist := false
for _, user := range users {
if user.UserID == 1 {
exist = true
break
}
}
if !exist {
if err = AddProjectMember(1, 1, models.DEVELOPER); err != nil {
t.Fatalf("failed to add user %d to be member of project %d: %v", 1, 1, err)
}
defer func() {
if err = DeleteProjectMember(1, 1); err != nil {
t.Fatalf("failed to delete user %d from member of project %d: %v", 1, 1, err)
}
}()
}
n, err := GetTotalOfUserRelevantRepositories(1, "")
if err != nil {
t.Fatalf("failed to get total of public repositoreis for user %d: %v", 1, err)
}
if n != total+1 {
t.Errorf("unexpected total: %d != %d", n, total+1)
}
}
func TestGetTopRepos(t *testing.T) {
var err error
require := require.New(t)
@ -226,7 +154,7 @@ func TestGetTotalOfRepositoriesByProject(t *testing.T) {
var projectID int64 = 1
repoName := "library/total_count"
total, err := GetTotalOfRepositoriesByProject(projectID, repoName)
total, err := GetTotalOfRepositoriesByProject([]int64{projectID}, repoName)
if err != nil {
t.Errorf("failed to get total of repositoreis of project %d: %v", projectID, err)
return
@ -246,7 +174,7 @@ func TestGetTotalOfRepositoriesByProject(t *testing.T) {
}
}()
n, err := GetTotalOfRepositoriesByProject(projectID, repoName)
n, err := GetTotalOfRepositoriesByProject([]int64{projectID}, repoName)
if err != nil {
t.Errorf("failed to get total of repositoreis of project %d: %v", projectID, err)
return

View File

@ -30,8 +30,19 @@ type AccessLog struct {
Operation string `orm:"column(operation)" json:"operation"`
OpTime time.Time `orm:"column(op_time)" json:"op_time"`
Keywords string `orm:"-" json:"keywords"`
BeginTime time.Time `orm:"-"`
BeginTimestamp int64 `orm:"-" json:"begin_timestamp"`
EndTime time.Time `orm:"-"`
EndTimestamp int64 `orm:"-" json:"end_timestamp"`
}
// LogQueryParam is used to set query conditions when listing
// access logs.
type LogQueryParam struct {
ProjectIDs []int64 // the IDs of projects to which the operation is done
Username string // the operator's username of the log
Repository string // repository name
Tag string // tag name
Operations []string // operations
BeginTime *time.Time // the time after which the operation is done
EndTime *time.Time // the time before which the operation is doen
Pagination *Pagination // pagination information
}

View File

@ -58,7 +58,7 @@ func (ps *ProjectSorter) Swap(i, j int) {
ps.Projects[i], ps.Projects[j] = ps.Projects[j], ps.Projects[i]
}
// QueryParam can be used to set query parameters when listing projects.
// ProjectQueryParam can be used to set query parameters when listing projects.
// The query condition will be set in the query if its corresponding field
// is not nil. Leave it empty if you don't want to apply this condition.
//
@ -69,7 +69,7 @@ func (ps *ProjectSorter) Swap(i, j int) {
// List all public projects the owner of which is user1: query := &QueryParam{Owner:"user1",Public:true}
// List projects which user1 is member of: query := &QueryParam{Member:&Member{Name:"user1"}}
// List projects which user1 is the project admin : query := &QueryParam{Memeber:&Member{Name:"user1",Role:1}}
type QueryParam struct {
type ProjectQueryParam struct {
Name string // the name of project
Owner string // the username of project owner
Public *bool // the project is public or not, can be ture, false and nil

View File

@ -106,12 +106,17 @@ func (f *fakePM) Update(projectIDOrName interface{}, project *models.Project) er
}
// nil implement
func (f *fakePM) GetAll(*models.QueryParam) ([]*models.Project, error) {
func (f *fakePM) GetAll(*models.ProjectQueryParam) ([]*models.Project, error) {
return []*models.Project{}, nil
}
// nil implement
func (f *fakePM) GetTotal(*models.QueryParam) (int64, error) {
func (f *fakePM) GetHasReadPerm(username ...string) ([]*models.Project, error) {
return []*models.Project{}, nil
}
// nil implement
func (f *fakePM) GetTotal(*models.ProjectQueryParam) (int64, error) {
return 0, nil
}

View File

@ -54,7 +54,7 @@ func (sm *SM) EnterState(s string) (string, error) {
return "", err
}
} else {
log.Debugf("Job: %d, no handler found for state:%s, skip", sm.CurrentJob, sm.CurrentState)
log.Debugf("Job: %v, no exit handler found for state:%s, skip", sm.CurrentJob, sm.CurrentState)
}
enterHandler, ok := sm.Handlers[s]
var next = models.JobContinue
@ -101,7 +101,7 @@ func (sm *SM) Start(s string) {
log.Debugf("Job: %v, next state from handler: %s", sm.CurrentJob, n)
}
if err != nil {
log.Warningf("Job: %v, the statemachin will enter error state due to error: %v", sm.CurrentJob, err)
log.Warningf("Job: %v, the statemachine will enter error state due to error: %v", sm.CurrentJob, err)
sm.EnterState(models.JobError)
}
}
@ -187,6 +187,7 @@ func (sm *SM) Reset(j Job) error {
sm.AddTransition(models.JobRetrying, models.JobRunning, StatusUpdater{sm.CurrentJob, models.JobRunning})
sm.Handlers[models.JobError] = StatusUpdater{sm.CurrentJob, models.JobError}
sm.Handlers[models.JobStopped] = StatusUpdater{sm.CurrentJob, models.JobStopped}
sm.Handlers[models.JobCanceled] = StatusUpdater{sm.CurrentJob, models.JobCanceled}
sm.Handlers[models.JobRetrying] = Retry{sm.CurrentJob}
if err := sm.CurrentJob.Init(); err != nil {
return err
@ -201,12 +202,14 @@ func (sm *SM) kickOff() error {
if repJob, ok := sm.CurrentJob.(*RepJob); ok {
if repJob.parm.Enabled == 0 {
log.Debugf("The policy of job:%v is disabled, will cancel the job", repJob)
if err := repJob.UpdateStatus(models.JobCanceled); err != nil {
log.Warningf("Failed to update status of job: %v to 'canceled', error: %v", repJob, err)
_, err := sm.EnterState(models.JobCanceled)
if err != nil {
log.Warningf("For job: %v, failed to update state to 'canceled', error: %v", repJob, err)
}
return err
}
}
log.Debugf("In kickOff: will start job: %v", sm.CurrentJob)
sm.Start(models.JobRunning)
return nil
}

View File

@ -103,9 +103,10 @@ func (w *Worker) handle(job Job) {
}
// NewWorker returns a pointer to new instance of worker
func NewWorker(id int, wp *workerPool) *Worker {
func NewWorker(id int, t Type, wp *workerPool) *Worker {
w := &Worker{
ID: id,
Type: t,
Jobs: make(chan Job),
quit: make(chan bool),
queue: wp.workerChan,
@ -125,19 +126,19 @@ func InitWorkerPools() error {
return err
}
WorkerPools = make(map[Type]*workerPool)
WorkerPools[ReplicationType] = createWorkerPool(maxRepWorker)
WorkerPools[ScanType] = createWorkerPool(maxScanWorker)
WorkerPools[ReplicationType] = createWorkerPool(maxRepWorker, ReplicationType)
WorkerPools[ScanType] = createWorkerPool(maxScanWorker, ScanType)
return nil
}
//createWorkerPool create workers according to parm
func createWorkerPool(n int) *workerPool {
func createWorkerPool(n int, t Type) *workerPool {
wp := &workerPool{
workerChan: make(chan *Worker, n),
workerList: make([]*Worker, 0, n),
}
for i := 0; i < n; i++ {
worker := NewWorker(i, wp)
worker := NewWorker(i, t, wp)
wp.workerList = append(wp.workerList, worker)
worker.Start()
log.Debugf("worker %v started", worker)

View File

@ -19,8 +19,6 @@ import (
"net/http"
"strconv"
"github.com/vmware/harbor/src/common/api"
"github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/common/utils/email"
"github.com/vmware/harbor/src/common/utils/log"
"github.com/vmware/harbor/src/ui/config"
@ -32,20 +30,20 @@ const (
// EmailAPI ...
type EmailAPI struct {
api.BaseAPI
BaseController
}
// Prepare ...
func (e *EmailAPI) Prepare() {
userID := e.ValidateUser()
isSysAdmin, err := dao.IsAdminRole(userID)
if err != nil {
log.Errorf("failed to check the role of user: %v", err)
e.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
e.BaseController.Prepare()
if !e.SecurityCtx.IsAuthenticated() {
e.HandleUnauthorized()
return
}
if !isSysAdmin {
e.CustomAbort(http.StatusForbidden, http.StatusText(http.StatusForbidden))
if !e.SecurityCtx.IsSysAdmin() {
e.HandleForbidden(e.SecurityCtx.GetUsername())
return
}
}

View File

@ -128,7 +128,7 @@ func init() {
_ = updateInitPassword(1, "Harbor12345")
//syncRegistry
if err := SyncRegistry(); err != nil {
if err := SyncRegistry(config.GlobalProjectMgr); err != nil {
log.Fatalf("failed to sync repositories from registry: %v", err)
}
@ -226,20 +226,14 @@ func (a testapi) StatisticGet(user usrInfo) (int, apilib.StatisticMap, error) {
return httpStatusCode, successPayload, err
}
func (a testapi) LogGet(user usrInfo, startTime, endTime, lines string) (int, []apilib.AccessLog, error) {
func (a testapi) LogGet(user usrInfo) (int, []apilib.AccessLog, error) {
_sling := sling.New().Get(a.basePath)
// create path and map variables
path := "/api/logs/"
fmt.Printf("logs path: %s\n", path)
_sling = _sling.Path(path)
type QueryParams struct {
StartTime string `url:"start_time,omitempty"`
EndTime string `url:"end_time,omitempty"`
Lines string `url:"lines,omitempty"`
}
_sling = _sling.QueryStruct(&QueryParams{StartTime: startTime, EndTime: endTime, Lines: lines})
var successPayload []apilib.AccessLog
code, body, err := request(_sling, jsonAcceptHeader, user)
if 200 == code && nil == err {

View File

@ -16,35 +16,29 @@ package api
import (
"net/http"
"github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/common/utils/log"
"github.com/vmware/harbor/src/common/api"
)
// InternalAPI handles request of harbor admin...
type InternalAPI struct {
api.BaseAPI
BaseController
}
// Prepare validates the URL and parms
func (ia *InternalAPI) Prepare() {
var currentUserID int
currentUserID = ia.ValidateUser()
isAdmin, err := dao.IsAdminRole(currentUserID)
if err != nil {
log.Errorf("Error occurred in IsAdminRole:%v", err)
ia.CustomAbort(http.StatusInternalServerError, "Internal error.")
ia.BaseController.Prepare()
if !ia.SecurityCtx.IsAuthenticated() {
ia.HandleUnauthorized()
return
}
if !isAdmin {
log.Error("Guests doesn't have the permisson to request harbor internal API.")
ia.CustomAbort(http.StatusForbidden, "Guests doesn't have the permisson to request harbor internal API.")
if !ia.SecurityCtx.IsSysAdmin() {
ia.HandleForbidden(ia.SecurityCtx.GetUsername())
return
}
}
// SyncRegistry ...
func (ia *InternalAPI) SyncRegistry() {
err := SyncRegistry()
err := SyncRegistry(ia.ProjectMgr)
if err != nil {
ia.CustomAbort(http.StatusInternalServerError, "internal error")
}

View File

@ -15,18 +15,17 @@
package api
import (
"net/http"
"strconv"
"time"
"fmt"
"github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/common/models"
"github.com/vmware/harbor/src/common/utils/log"
)
//LogAPI handles request api/logs
type LogAPI struct {
BaseController
username string
isSysAdmin bool
}
//Prepare validates the URL and the user
@ -36,53 +35,58 @@ func (l *LogAPI) Prepare() {
l.HandleUnauthorized()
return
}
l.username = l.SecurityCtx.GetUsername()
l.isSysAdmin = l.SecurityCtx.IsSysAdmin()
}
//Get returns the recent logs according to parameters
func (l *LogAPI) Get() {
var err error
startTime := l.GetString("start_time")
if len(startTime) != 0 {
i, err := strconv.ParseInt(startTime, 10, 64)
if err != nil {
log.Errorf("Parse startTime to int error, err: %v", err)
l.CustomAbort(http.StatusBadRequest, "startTime is not a valid integer")
}
startTime = time.Unix(i, 0).String()
page, size := l.GetPaginationParams()
query := &models.LogQueryParam{
Pagination: &models.Pagination{
Page: page,
Size: size,
},
}
endTime := l.GetString("end_time")
if len(endTime) != 0 {
j, err := strconv.ParseInt(endTime, 10, 64)
if !l.isSysAdmin {
projects, err := l.ProjectMgr.GetByMember(l.username)
if err != nil {
log.Errorf("Parse endTime to int error, err: %v", err)
l.CustomAbort(http.StatusBadRequest, "endTime is not a valid integer")
l.HandleInternalServerError(fmt.Sprintf(
"failed to get projects of user %s: %v", l.username, err))
return
}
endTime = time.Unix(j, 0).String()
if len(projects) == 0 {
l.SetPaginationHeader(0, page, size)
l.Data["json"] = nil
l.ServeJSON()
return
}
ids := []int64{}
for _, project := range projects {
ids = append(ids, project.ProjectID)
}
query.ProjectIDs = ids
}
var linesNum int
lines := l.GetString("lines")
if len(lines) != 0 {
linesNum, err = strconv.Atoi(lines)
if err != nil {
log.Errorf("Get parameters error--lines, err: %v", err)
l.CustomAbort(http.StatusBadRequest, "bad request of lines")
}
if linesNum <= 0 {
log.Warning("lines must be a positive integer")
l.CustomAbort(http.StatusBadRequest, "lines is 0 or negative")
}
} else if len(startTime) == 0 && len(endTime) == 0 {
linesNum = 10
}
var logList []models.AccessLog
logList, err = dao.GetRecentLogs(l.SecurityCtx.GetUsername(), linesNum, startTime, endTime)
total, err := dao.GetTotalOfAccessLogs(query)
if err != nil {
log.Errorf("Get recent logs error, err: %v", err)
l.CustomAbort(http.StatusInternalServerError, "Internal error")
l.HandleInternalServerError(fmt.Sprintf(
"failed to get total of access logs: %v", err))
return
}
l.Data["json"] = logList
logs, err := dao.GetAccessLogs(query)
if err != nil {
l.HandleInternalServerError(fmt.Sprintf(
"failed to get access logs: %v", err))
return
}
l.SetPaginationHeader(total, page, size)
l.Data["json"] = logs
l.ServeJSON()
}

View File

@ -15,11 +15,11 @@ package api
import (
"fmt"
"github.com/stretchr/testify/assert"
"github.com/vmware/harbor/tests/apitests/apilib"
"strconv"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/vmware/harbor/tests/apitests/apilib"
)
func TestLogGet(t *testing.T) {
@ -29,12 +29,11 @@ func TestLogGet(t *testing.T) {
apiTest := newHarborAPI()
//prepare for test
CommonAddUser()
var project apilib.ProjectReq
project.ProjectName = "my_project"
project.Public = 1
now := fmt.Sprintf("%v", time.Now().Unix())
statusCode, result, err := apiTest.LogGet(*admin, "0", now, "1000")
statusCode, result, err := apiTest.LogGet(*testUser)
if err != nil {
t.Error("Error while get log information", err.Error())
t.Log(err)
@ -46,7 +45,7 @@ func TestLogGet(t *testing.T) {
fmt.Println("result", result)
//add the project first.
fmt.Println("add the project first.")
reply, err := apiTest.ProjectsPost(*admin, project)
reply, err := apiTest.ProjectsPost(*testUser, project)
if err != nil {
t.Error("Error while creat project", err.Error())
t.Log(err)
@ -54,8 +53,7 @@ func TestLogGet(t *testing.T) {
assert.Equal(int(201), reply, "Case 2: Project creation status should be 201")
}
//case 1: right parameters, expect the right output
now = fmt.Sprintf("%v", time.Now().Unix())
statusCode, result, err = apiTest.LogGet(*admin, "0", now, "1000")
statusCode, result, err = apiTest.LogGet(*testUser)
if err != nil {
t.Error("Error while get log information", err.Error())
t.Log(err)
@ -71,64 +69,6 @@ func TestLogGet(t *testing.T) {
}
}
fmt.Println("log ", result)
//case 2: wrong format of start_time parameter, expect the wrong output
statusCode, result, err = apiTest.LogGet(*admin, "ss", now, "3")
if err != nil {
t.Error("Error occured while get log information since the format of start_time parameter is not right.", err.Error())
t.Log(err)
} else {
assert.Equal(int(400), statusCode, "Http status code should be 400")
}
//case 3: wrong format of end_time parameter, expect the wrong output
statusCode, result, err = apiTest.LogGet(*admin, "0", "cc", "3")
if err != nil {
t.Error("Error occured while get log information since the format of end_time parameter is not right.", err.Error())
t.Log(err)
} else {
assert.Equal(int(400), statusCode, "Http status code should be 400")
}
//case 4: wrong format of lines parameter, expect the wrong output
statusCode, result, err = apiTest.LogGet(*admin, "0", now, "s")
if err != nil {
t.Error("Error occured while get log information since the format of lines parameter is not right.", err.Error())
t.Log(err)
} else {
assert.Equal(int(400), statusCode, "Http status code should be 400")
}
//case 5: wrong format of lines parameter, expect the wrong output
statusCode, result, err = apiTest.LogGet(*admin, "0", now, "-5")
if err != nil {
t.Error("Error occured while get log information since the format of lines parameter is not right.", err.Error())
t.Log(err)
} else {
assert.Equal(int(400), statusCode, "Http status code should be 400")
}
//case 6: all parameters are null, expect the right output
statusCode, result, err = apiTest.LogGet(*admin, "", "", "")
if err != nil {
t.Error("Error while get log information", err.Error())
t.Log(err)
} else {
//default get 10 logs
if logNum+1 >= 10 {
logNum = 10
} else {
logNum++
}
assert.Equal(logNum, len(result), "lines of logs should be equal")
num, index := getLog(result)
if num != 1 {
assert.Equal(1, num, "add my_project log number should be 1")
} else {
assert.Equal("my_project/", result[index].RepoName, "RepoName should be equal")
assert.Equal("N/A", result[index].RepoTag, "RepoTag should be equal")
assert.Equal("create", result[index].Operation, "Operation should be equal")
}
}
//get the project
var projects []apilib.Project
@ -144,7 +84,7 @@ func TestLogGet(t *testing.T) {
//delete the project
projectID := strconv.Itoa(int(addProjectID))
httpStatusCode, err = apiTest.ProjectsDelete(*admin, projectID)
httpStatusCode, err = apiTest.ProjectsDelete(*testUser, projectID)
if err != nil {
t.Error("Error while delete project", err.Error())
t.Log(err)
@ -152,7 +92,7 @@ func TestLogGet(t *testing.T) {
assert.Equal(int(200), httpStatusCode, "Case 1: Project creation status should be 200")
//t.Log(result)
}
CommonDelUser()
fmt.Printf("\n")
}

View File

@ -18,6 +18,7 @@ import (
"fmt"
"net/http"
"regexp"
"strings"
"github.com/vmware/harbor/src/common"
"github.com/vmware/harbor/src/common/dao"
@ -258,7 +259,7 @@ func projectContainsPolicy(id int64) (bool, error) {
// TODO refacter pattern to:
// /api/repositories?owner=xxx&name=xxx&public=true&member=xxx&role=1&page=1&size=3
func (p *ProjectAPI) List() {
query := &models.QueryParam{}
query := &models.ProjectQueryParam{}
query.Name = p.GetString("project_name")
public := p.GetString("is_public")
@ -382,28 +383,49 @@ func (p *ProjectAPI) FilterAccessLog() {
var query models.AccessLog
p.DecodeJSONReq(&query)
query.ProjectID = p.project.ProjectID
query.BeginTime = time.Unix(query.BeginTimestamp, 0)
query.EndTime = time.Unix(query.EndTimestamp, 0)
page, pageSize := p.GetPaginationParams()
total, err := dao.GetTotalOfAccessLogs(query)
if err != nil {
log.Errorf("failed to get total of access log: %v", err)
p.CustomAbort(http.StatusInternalServerError, "")
queryParm := &models.LogQueryParam{
ProjectIDs: []int64{p.project.ProjectID},
Username: query.Username,
Repository: query.RepoName,
Tag: query.RepoTag,
}
logs, err := dao.GetAccessLogs(query, pageSize, pageSize*(page-1))
if len(query.Keywords) > 0 {
queryParm.Operations = strings.Split(query.Keywords, "/")
}
if query.BeginTimestamp > 0 {
beginTime := time.Unix(query.BeginTimestamp, 0)
queryParm.BeginTime = &beginTime
}
if query.EndTimestamp > 0 {
endTime := time.Unix(query.EndTimestamp, 0)
queryParm.EndTime = &endTime
}
page, pageSize := p.GetPaginationParams()
queryParm.Pagination = &models.Pagination{
Page: page,
Size: pageSize,
}
total, err := dao.GetTotalOfAccessLogs(queryParm)
if err != nil {
log.Errorf("failed to get access log: %v", err)
p.CustomAbort(http.StatusInternalServerError, "")
p.HandleInternalServerError(fmt.Sprintf(
"failed to get total of access log: %v", err))
return
}
logs, err := dao.GetAccessLogs(queryParm)
if err != nil {
p.HandleInternalServerError(fmt.Sprintf(
"failed to get access log: %v", err))
return
}
p.SetPaginationHeader(total, page, pageSize)
p.Data["json"] = logs
p.ServeJSON()
}

View File

@ -25,29 +25,29 @@ import (
"github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/common/models"
"github.com/vmware/harbor/src/common/utils/log"
"github.com/vmware/harbor/src/common/api"
)
// RepJobAPI handles request to /api/replicationJobs /api/replicationJobs/:id/log
type RepJobAPI struct {
api.BaseAPI
BaseController
jobID int64
}
// Prepare validates that whether user has system admin role
func (ra *RepJobAPI) Prepare() {
uid := ra.ValidateUser()
isAdmin, err := dao.IsAdminRole(uid)
if err != nil {
log.Errorf("Failed to Check if the user is admin, error: %v, uid: %d", err, uid)
}
if !isAdmin {
ra.CustomAbort(http.StatusForbidden, "")
ra.BaseController.Prepare()
if !ra.SecurityCtx.IsAuthenticated() {
ra.HandleUnauthorized()
return
}
idStr := ra.Ctx.Input.Param(":id")
if len(idStr) != 0 {
id, err := strconv.ParseInt(idStr, 10, 64)
if !ra.SecurityCtx.IsSysAdmin() {
ra.HandleForbidden(ra.SecurityCtx.GetUsername())
return
}
if len(ra.GetStringFromPath(":id")) != 0 {
id, err := ra.GetInt64FromPath(":id")
if err != nil {
ra.CustomAbort(http.StatusBadRequest, "ID is invalid")
}

View File

@ -23,24 +23,24 @@ import (
"github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/common/models"
"github.com/vmware/harbor/src/common/utils/log"
"github.com/vmware/harbor/src/common/api"
)
// RepPolicyAPI handles /api/replicationPolicies /api/replicationPolicies/:id/enablement
type RepPolicyAPI struct {
api.BaseAPI
BaseController
}
// Prepare validates whether the user has system admin role
func (pa *RepPolicyAPI) Prepare() {
uid := pa.ValidateUser()
var err error
isAdmin, err := dao.IsAdminRole(uid)
if err != nil {
log.Errorf("Failed to Check if the user is admin, error: %v, uid: %d", err, uid)
pa.BaseController.Prepare()
if !pa.SecurityCtx.IsAuthenticated() {
pa.HandleUnauthorized()
return
}
if !isAdmin {
pa.CustomAbort(http.StatusForbidden, "")
if !pa.SecurityCtx.IsSysAdmin() {
pa.HandleForbidden(pa.SecurityCtx.GetUsername())
return
}
}
@ -82,6 +82,19 @@ func (pa *RepPolicyAPI) List() {
log.Errorf("failed to filter policies %s project ID %d: %v", name, projectID, err)
pa.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
for _, policy := range policies {
project, err := pa.ProjectMgr.Get(policy.ProjectID)
if err != nil {
pa.HandleInternalServerError(fmt.Sprintf(
"failed to get project %d: %v", policy.ProjectID, err))
return
}
if project != nil {
policy.ProjectName = project.Name
}
}
pa.Data["json"] = policies
pa.ServeJSON()
}
@ -103,7 +116,7 @@ func (pa *RepPolicyAPI) Post() {
}
*/
project, err := dao.GetProjectByID(policy.ProjectID)
project, err := pa.ProjectMgr.Get(policy.ProjectID)
if err != nil {
log.Errorf("failed to get project %d: %v", policy.ProjectID, err)
pa.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))

View File

@ -103,7 +103,8 @@ func (ra *RepositoryAPI) Get() {
keyword := ra.GetString("q")
total, err := dao.GetTotalOfRepositoriesByProject(projectID, keyword)
total, err := dao.GetTotalOfRepositoriesByProject(
[]int64{projectID}, keyword)
if err != nil {
ra.HandleInternalServerError(fmt.Sprintf("failed to get total of repositories of project %d: %v",
projectID, err))

View File

@ -15,11 +15,12 @@
package api
import (
"fmt"
"net/http"
"sort"
"strings"
"github.com/vmware/harbor/src/common/api"
"github.com/vmware/harbor/src/common"
"github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/common/models"
"github.com/vmware/harbor/src/common/utils"
@ -29,7 +30,7 @@ import (
// SearchAPI handles requesst to /api/search
type SearchAPI struct {
api.BaseAPI
BaseController
}
type searchResult struct {
@ -39,33 +40,25 @@ type searchResult struct {
// Get ...
func (s *SearchAPI) Get() {
userID, _, ok := s.GetUserIDForRequest()
if !ok {
userID = dao.NonExistUserID
}
keyword := s.GetString("q")
isSysAdmin, err := dao.IsAdminRole(userID)
if err != nil {
log.Errorf("failed to check whether the user %d is system admin: %v", userID, err)
s.CustomAbort(http.StatusInternalServerError, "internal error")
}
isAuthenticated := s.SecurityCtx.IsAuthenticated()
username := s.SecurityCtx.GetUsername()
isSysAdmin := s.SecurityCtx.IsSysAdmin()
var projects []*models.Project
var err error
if isSysAdmin {
projects, err = dao.GetProjects(nil)
if err != nil {
log.Errorf("failed to get all projects: %v", err)
s.CustomAbort(http.StatusInternalServerError, "internal error")
}
if !isAuthenticated {
projects, err = s.ProjectMgr.GetPublic()
} else if isSysAdmin {
projects, err = s.ProjectMgr.GetAll(nil)
} else {
projects, err = dao.SearchProjects(userID)
if err != nil {
log.Errorf("failed to get user %d 's relevant projects: %v", userID, err)
s.CustomAbort(http.StatusInternalServerError, "internal error")
}
projects, err = s.ProjectMgr.GetHasReadPerm(username)
}
if err != nil {
s.HandleInternalServerError(fmt.Sprintf(
"failed to get projects: %v", err))
return
}
projectSorter := &models.ProjectSorter{Projects: projects}
@ -76,17 +69,19 @@ func (s *SearchAPI) Get() {
continue
}
if userID != dao.NonExistUserID {
roles, err := dao.GetUserProjectRoles(userID, p.ProjectID)
if isAuthenticated {
roles, err := s.ProjectMgr.GetRoles(username, p.ProjectID)
if err != nil {
log.Errorf("failed to get user's project role: %v", err)
s.CustomAbort(http.StatusInternalServerError, "")
}
if len(roles) != 0 {
p.Role = roles[0].RoleID
s.HandleInternalServerError(fmt.Sprintf("failed to get roles of user %s to project %d: %v",
username, p.ProjectID, err))
return
}
if p.Role == models.PROJECTADMIN || isSysAdmin {
if len(roles) != 0 {
p.Role = roles[0]
}
if p.Role == common.RoleProjectAdmin || isSysAdmin {
p.Togglable = true
}
}

View File

@ -15,9 +15,9 @@
package api
import (
"fmt"
"net/http"
"github.com/vmware/harbor/src/common/api"
"github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/common/models"
"github.com/vmware/harbor/src/common/utils/log"
@ -40,42 +40,44 @@ const (
// StatisticAPI handles request to /api/statistics/
type StatisticAPI struct {
api.BaseAPI
userID int
BaseController
username string
}
//Prepare validates the URL and the user
func (s *StatisticAPI) Prepare() {
s.userID = s.ValidateUser()
s.BaseController.Prepare()
if !s.SecurityCtx.IsAuthenticated() {
s.HandleUnauthorized()
return
}
s.username = s.SecurityCtx.GetUsername()
}
// Get total projects and repos of the user
func (s *StatisticAPI) Get() {
statistic := map[string]int64{}
t := true
n, err := dao.GetTotalOfProjects(&models.QueryParam{
Public: &t,
})
projects, err := s.ProjectMgr.GetPublic()
if err != nil {
log.Errorf("failed to get total of public projects: %v", err)
s.CustomAbort(http.StatusInternalServerError, "")
s.HandleInternalServerError(fmt.Sprintf(
"failed to get public projects: %v", err))
return
}
statistic[PPC] = n
n, err = dao.GetTotalOfPublicRepositories("")
statistic[PPC] = (int64)(len(projects))
ids := []int64{}
for _, p := range projects {
ids = append(ids, p.ProjectID)
}
n, err := dao.GetTotalOfRepositoriesByProject(ids, "")
if err != nil {
log.Errorf("failed to get total of public repositories: %v", err)
s.CustomAbort(http.StatusInternalServerError, "")
}
statistic[PRC] = n
isAdmin, err := dao.IsAdminRole(s.userID)
if err != nil {
log.Errorf("Error occured in check admin, error: %v", err)
s.CustomAbort(http.StatusInternalServerError, "Internal error.")
}
if isAdmin {
if s.SecurityCtx.IsSysAdmin() {
n, err := dao.GetTotalOfProjects(nil)
if err != nil {
log.Errorf("failed to get total of projects: %v", err)
@ -92,28 +94,29 @@ func (s *StatisticAPI) Get() {
statistic[MRC] = n
statistic[TRC] = n
} else {
user, err := dao.GetUser(models.User{
UserID: s.userID,
})
if err != nil {
log.Errorf("failed to get user %d: %v", s.userID, err)
s.CustomAbort(http.StatusInternalServerError, "")
}
n, err := dao.GetTotalOfProjects(&models.QueryParam{
projects, err := s.ProjectMgr.GetAll(&models.ProjectQueryParam{
Member: &models.Member{
Name: user.Username,
Name: s.username,
},
})
if err != nil {
log.Errorf("failed to get total of projects for user %d: %v", s.userID, err)
s.CustomAbort(http.StatusInternalServerError, "")
s.HandleInternalServerError(fmt.Sprintf(
"failed to get projects of user %s: %v", s.username, err))
return
}
statistic[MPC] = n
statistic[MPC] = (int64)(len(projects))
n, err = dao.GetTotalOfUserRelevantRepositories(s.userID, "")
ids := []int64{}
for _, p := range projects {
ids = append(ids, p.ProjectID)
}
n, err = dao.GetTotalOfRepositoriesByProject(ids, "")
if err != nil {
log.Errorf("failed to get total of repositories for user %d: %v", s.userID, err)
s.CustomAbort(http.StatusInternalServerError, "")
s.HandleInternalServerError(fmt.Sprintf(
"failed to get total of repositories for user %s: %v",
s.username, err))
return
}
statistic[MRC] = n
}

View File

@ -21,7 +21,6 @@ import (
"net/url"
"strconv"
"github.com/vmware/harbor/src/common/api"
"github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/common/models"
"github.com/vmware/harbor/src/common/utils"
@ -34,29 +33,29 @@ import (
// TargetAPI handles request to /api/targets/ping /api/targets/{}
type TargetAPI struct {
api.BaseAPI
BaseController
secretKey string
}
// Prepare validates the user
func (t *TargetAPI) Prepare() {
t.BaseController.Prepare()
if !t.SecurityCtx.IsAuthenticated() {
t.HandleUnauthorized()
return
}
if !t.SecurityCtx.IsSysAdmin() {
t.HandleForbidden(t.SecurityCtx.GetUsername())
return
}
var err error
t.secretKey, err = config.SecretKey()
if err != nil {
log.Errorf("failed to get secret key: %v", err)
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
userID := t.ValidateUser()
isSysAdmin, err := dao.IsAdminRole(userID)
if err != nil {
log.Errorf("error occurred in IsAdminRole: %v", err)
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
if !isSysAdmin {
t.CustomAbort(http.StatusForbidden, http.StatusText(http.StatusForbidden))
}
}
func (t *TargetAPI) ping(endpoint, username, password string) {

View File

@ -31,6 +31,7 @@ import (
"github.com/vmware/harbor/src/common/utils/registry/auth"
registry_error "github.com/vmware/harbor/src/common/utils/registry/error"
"github.com/vmware/harbor/src/ui/config"
"github.com/vmware/harbor/src/ui/projectmanager"
)
//sysadmin has all privileges to all projects
@ -212,7 +213,7 @@ func addAuthentication(req *http.Request) {
}
// SyncRegistry syncs the repositories of registry with database.
func SyncRegistry() error {
func SyncRegistry(pm projectmanager.ProjectManager) error {
log.Infof("Start syncing repositories from registry to DB... ")
@ -236,7 +237,7 @@ func SyncRegistry() error {
var reposToAdd []string
var reposToDel []string
reposToAdd, reposToDel, err = diffRepos(reposInRegistry, reposInDB)
reposToAdd, reposToDel, err = diffRepos(reposInRegistry, reposInDB, pm)
if err != nil {
return err
}
@ -249,7 +250,7 @@ func SyncRegistry() error {
if err != nil {
log.Errorf("Error happens when counting pull count from access log: %v", err)
}
pro, err := dao.GetProjectByName(project)
pro, err := pm.Get(project)
if err != nil {
log.Errorf("failed to get project %s: %v", project, err)
continue
@ -299,7 +300,8 @@ func catalog() ([]string, error) {
return repositories, nil
}
func diffRepos(reposInRegistry []string, reposInDB []string) ([]string, []string, error) {
func diffRepos(reposInRegistry []string, reposInDB []string,
pm projectmanager.ProjectManager) ([]string, []string, error) {
var needsAdd []string
var needsDel []string
@ -314,7 +316,7 @@ func diffRepos(reposInRegistry []string, reposInDB []string) ([]string, []string
d := strings.Compare(repoInR, repoInD)
if d < 0 {
i++
exist, err := projectExists(repoInR)
exist, err := projectExists(pm, repoInR)
if err != nil {
log.Errorf("failed to check the existence of project %s: %v", repoInR, err)
continue
@ -377,7 +379,7 @@ func diffRepos(reposInRegistry []string, reposInDB []string) ([]string, []string
for i < len(reposInRegistry) {
repoInR = reposInRegistry[i]
i++
exist, err := projectExists(repoInR)
exist, err := projectExists(pm, repoInR)
if err != nil {
log.Errorf("failed to check whether project of %s exists: %v", repoInR, err)
continue
@ -397,9 +399,9 @@ func diffRepos(reposInRegistry []string, reposInDB []string) ([]string, []string
return needsAdd, needsDel, nil
}
func projectExists(repository string) (bool, error) {
func projectExists(pm projectmanager.ProjectManager, repository string) (bool, error) {
project, _ := utils.ParseRepository(repository)
return dao.ProjectExists(project)
return pm.Exist(project)
}
// TODO need a registry client which accept a raw token as param

View File

@ -40,9 +40,8 @@ var (
SecretStore *secret.Store
// AdminserverClient is a client for adminserver
AdminserverClient client.Client
// DBProjectManager is the project manager based on database,
// it is initialized only the deploy mode is standalone
DBProjectManager projectmanager.ProjectManager
// GlobalProjectMgr is initialized based on the deploy mode
GlobalProjectMgr projectmanager.ProjectManager
mg *comcfg.Manager
keyProvider comcfg.KeyProvider
)
@ -73,8 +72,8 @@ func Init() error {
// init secret store
initSecretStore()
// init project manager based on database
initDBProjectManager()
// init project manager based on deploy mode
initProjectManager()
return nil
}
@ -95,12 +94,13 @@ func initSecretStore() {
SecretStore = secret.NewStore(m)
}
func initDBProjectManager() {
func initProjectManager() {
if len(DeployMode()) == 0 ||
DeployMode() == common.DeployModeStandAlone {
log.Info("initializing the project manager based on database...")
DBProjectManager = &db.ProjectManager{}
GlobalProjectMgr = &db.ProjectManager{}
}
// TODO create project manager based on pms
}
// Load configurations

View File

@ -136,7 +136,7 @@ func getProjectManager(ctx *beegoctx.Context) projectmanager.ProjectManager {
if len(config.DeployMode()) == 0 ||
config.DeployMode() == common.DeployModeStandAlone {
log.Info("filling a project manager based on database...")
return config.DBProjectManager
return config.GlobalProjectMgr
}
// TODO create project manager based on pms

View File

@ -101,7 +101,7 @@ func main() {
beego.InsertFilter("/*", beego.BeforeRouter, filter.SecurityFilter)
initRouters()
if err := api.SyncRegistry(); err != nil {
if err := api.SyncRegistry(config.GlobalProjectMgr); err != nil {
log.Error(err)
}
log.Info("Init proxy")

View File

@ -107,7 +107,7 @@ func (p *ProjectManager) GetRoles(username string, projectIDOrName interface{})
// GetPublic returns all public projects
func (p *ProjectManager) GetPublic() ([]*models.Project, error) {
t := true
return p.GetAll(&models.QueryParam{
return p.GetAll(&models.ProjectQueryParam{
Public: &t,
})
}
@ -115,7 +115,7 @@ func (p *ProjectManager) GetPublic() ([]*models.Project, error) {
// GetByMember returns all projects which the user is a member of
func (p *ProjectManager) GetByMember(username string) (
[]*models.Project, error) {
return p.GetAll(&models.QueryParam{
return p.GetAll(&models.ProjectQueryParam{
Member: &models.Member{
Name: username,
},
@ -190,13 +190,23 @@ func (p *ProjectManager) Update(projectIDOrName interface{},
}
// GetAll returns a project list according to the query parameters
func (p *ProjectManager) GetAll(query *models.QueryParam) (
func (p *ProjectManager) GetAll(query *models.ProjectQueryParam) (
[]*models.Project, error) {
return dao.GetProjects(query)
}
// GetTotal returns the total count according to the query parameters
func (p *ProjectManager) GetTotal(query *models.QueryParam) (
func (p *ProjectManager) GetTotal(query *models.ProjectQueryParam) (
int64, error) {
return dao.GetTotalOfProjects(query)
}
// GetHasReadPerm returns projects which are public or the user is a member of
func (p *ProjectManager) GetHasReadPerm(username ...string) (
[]*models.Project, error) {
if len(username) == 0 || len(username[0]) == 0 {
return p.GetPublic()
}
return dao.GetHasReadPermProjects(username[0])
}

View File

@ -229,14 +229,14 @@ func TestGetTotal(t *testing.T) {
defer pm.Delete(id)
// get by name
total, err := pm.GetTotal(&models.QueryParam{
total, err := pm.GetTotal(&models.ProjectQueryParam{
Name: "get_total_test",
})
assert.Nil(t, err)
assert.Equal(t, int64(1), total)
// get by owner
total, err = pm.GetTotal(&models.QueryParam{
total, err = pm.GetTotal(&models.ProjectQueryParam{
Owner: "admin",
})
assert.Nil(t, err)
@ -244,7 +244,7 @@ func TestGetTotal(t *testing.T) {
// get by public
value := true
total, err = pm.GetTotal(&models.QueryParam{
total, err = pm.GetTotal(&models.ProjectQueryParam{
Public: &value,
})
assert.Nil(t, err)
@ -263,14 +263,14 @@ func TestGetAll(t *testing.T) {
defer pm.Delete(id)
// get by name
projects, err := pm.GetAll(&models.QueryParam{
projects, err := pm.GetAll(&models.ProjectQueryParam{
Name: "get_all_test",
})
assert.Nil(t, err)
assert.Equal(t, id, projects[0].ProjectID)
// get by owner
projects, err = pm.GetAll(&models.QueryParam{
projects, err = pm.GetAll(&models.ProjectQueryParam{
Owner: "admin",
})
assert.Nil(t, err)
@ -285,7 +285,7 @@ func TestGetAll(t *testing.T) {
// get by public
value := true
projects, err = pm.GetAll(&models.QueryParam{
projects, err = pm.GetAll(&models.ProjectQueryParam{
Public: &value,
})
assert.Nil(t, err)
@ -298,3 +298,58 @@ func TestGetAll(t *testing.T) {
}
assert.True(t, exist)
}
func TestGetHasReadPerm(t *testing.T) {
pm := &ProjectManager{}
// do not pass username
projects, err := pm.GetHasReadPerm()
assert.Nil(t, err)
assert.NotEqual(t, 0, len(projects))
exist := false
for _, project := range projects {
if project.ProjectID == 1 {
exist = true
break
}
}
assert.True(t, exist)
// username is nil
projects, err = pm.GetHasReadPerm("")
assert.Nil(t, err)
assert.NotEqual(t, 0, len(projects))
exist = false
for _, project := range projects {
if project.ProjectID == 1 {
exist = true
break
}
}
assert.True(t, exist)
// valid username
id, err := pm.Create(&models.Project{
Name: "get_has_read_perm_test",
OwnerID: 1,
Public: 0,
})
assert.Nil(t, err)
defer pm.Delete(id)
projects, err = pm.GetHasReadPerm("admin")
assert.Nil(t, err)
assert.NotEqual(t, 0, len(projects))
exist1 := false
exist2 := false
for _, project := range projects {
if project.ProjectID == 1 {
exist1 = true
}
if project.ProjectID == id {
exist2 = true
}
}
assert.True(t, exist1)
assert.True(t, exist2)
}

View File

@ -33,7 +33,11 @@ type ProjectManager interface {
Delete(projectIDOrName interface{}) error
Update(projectIDOrName interface{}, project *models.Project) error
// GetAll returns a project list according to the query parameters
GetAll(query *models.QueryParam) ([]*models.Project, error)
GetAll(query *models.ProjectQueryParam) ([]*models.Project, error)
// GetTotal returns the total count according to the query parameters
GetTotal(query *models.QueryParam) (int64, error)
GetTotal(query *models.ProjectQueryParam) (int64, error)
// GetHasReadPerm returns a project list which the user has read
// permission of. The list should contains all public projects and
// projects which the user is a member of if the username is not nil
GetHasReadPerm(username ...string) ([]*models.Project, error)
}

View File

@ -4,6 +4,8 @@ import { LOG_DIRECTIVES } from './log/index';
import { FILTER_DIRECTIVES } from './filter/index';
import { ENDPOINT_DIRECTIVES } from './endpoint/index';
import { REPOSITORY_DIRECTIVES } from './repository/index';
import { REPOSITORY_STACKVIEW_DIRECTIVES } from './repository-stackview/index';
import { LIST_REPOSITORY_DIRECTIVES } from './list-repository/index';
import { TAG_DIRECTIVES } from './tag/index';
@ -125,6 +127,7 @@ export function initConfig(translateInitializer: TranslateServiceInitializer, co
FILTER_DIRECTIVES,
ENDPOINT_DIRECTIVES,
REPOSITORY_DIRECTIVES,
REPOSITORY_STACKVIEW_DIRECTIVES,
LIST_REPOSITORY_DIRECTIVES,
TAG_DIRECTIVES,
CREATE_EDIT_ENDPOINT_DIRECTIVES,
@ -141,6 +144,7 @@ export function initConfig(translateInitializer: TranslateServiceInitializer, co
FILTER_DIRECTIVES,
ENDPOINT_DIRECTIVES,
REPOSITORY_DIRECTIVES,
REPOSITORY_STACKVIEW_DIRECTIVES,
LIST_REPOSITORY_DIRECTIVES,
TAG_DIRECTIVES,
CREATE_EDIT_ENDPOINT_DIRECTIVES,

View File

@ -7,7 +7,9 @@ export * from './log/index';
export * from './filter/index';
export * from './endpoint/index';
export * from './repository/index';
export * from './repository-stackview/index';
export * from './tag/index';
export * from './list-replication-rule/index';
export * from './replication/index';
export * from './vulnerability-scanning/index';
export * from './i18n/index';

View File

@ -3,26 +3,26 @@ export const LIST_REPLICATION_RULE_TEMPLATE: string = `
<confirmation-dialog #deletionConfirmDialog (confirmAction)="deletionConfirm($event)"></confirmation-dialog>
<clr-datagrid [clrDgLoading]="loading">
<clr-dg-column [clrDgField]="'name'">{{'REPLICATION.NAME' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'project_name'" *ngIf="projectless">{{'REPLICATION.PROJECT' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'project_name'" *ngIf="projectScope">{{'REPLICATION.PROJECT' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'description'">{{'REPLICATION.DESCRIPTION' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'target_name'">{{'REPLICATION.DESTINATION_NAME' | translate}}</clr-dg-column>
<clr-dg-column [clrDgSortBy]="startTimeComparator">{{'REPLICATION.LAST_START_TIME' | translate}}</clr-dg-column>
<clr-dg-column [clrDgSortBy]="enabledComparator">{{'REPLICATION.ACTIVATION' | translate}}</clr-dg-column>
<clr-dg-row *clrDgItems="let p of rules" [clrDgItem]="p" (click)="selectRule(p)" [style.backgroundColor]="(!projectless && selectedId === p.id) ? '#eee' : ''">
<clr-dg-row *clrDgItems="let p of changedRules" [clrDgItem]="p" (click)="selectRule(p)" [style.backgroundColor]="(!projectScope && withReplicationJob && selectedId === p.id) ? '#eee' : ''">
<clr-dg-action-overflow>
<button class="action-item" (click)="editRule(p)">{{'REPLICATION.EDIT_POLICY' | translate}}</button>
<button class="action-item" (click)="toggleRule(p)">{{ (p.enabled === 0 ? 'REPLICATION.ENABLE' : 'REPLICATION.DISABLE') | translate}}</button>
<button class="action-item" (click)="deleteRule(p)">{{'REPLICATION.DELETE_POLICY' | translate}}</button>
</clr-dg-action-overflow>
<clr-dg-cell>
<ng-template [ngIf]="projectless">
<a href="javascript:void(0)">{{p.name}}</a>
<ng-template [ngIf]="projectScope">
<a href="javascript:void(0)" (click)="redirectTo(p)">{{p.name}}</a>
</ng-template>
<ng-template [ngIf]="!projectless">
<ng-template [ngIf]="!projectScope">
{{p.name}}
</ng-template>
</clr-dg-cell>
<clr-dg-cell *ngIf="projectless">{{p.project_name}}</clr-dg-cell>
<clr-dg-cell *ngIf="projectScope">{{p.project_name}}</clr-dg-cell>
<clr-dg-cell>{{p.description ? p.description : '-'}}</clr-dg-cell>
<clr-dg-cell>{{p.target_name}}</clr-dg-cell>
<clr-dg-cell>

View File

@ -11,7 +11,7 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, Input, Output, EventEmitter, ViewChild, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
import { Component, Input, Output, OnInit, EventEmitter, ViewChild, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
import { ReplicationService } from '../service/replication.service';
import { ReplicationRule } from '../service/interface';
@ -32,17 +32,17 @@ import { State, Comparator } from 'clarity-angular';
import { LIST_REPLICATION_RULE_TEMPLATE } from './list-replication-rule.component.html';
@Component({
selector: 'list-replication-rule',
selector: 'hbr-list-replication-rule',
template: LIST_REPLICATION_RULE_TEMPLATE,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ListReplicationRuleComponent {
export class ListReplicationRuleComponent implements OnInit {
nullTime: string = '0001-01-01T00:00:00Z';
@Input() rules: ReplicationRule[];
@Input() projectless: boolean;
@Input() projectId: number;
@Input() selectedId: number | string;
@Input() withReplicationJob: boolean;
@Input() loading: boolean = false;
@ -50,6 +50,13 @@ export class ListReplicationRuleComponent {
@Output() selectOne = new EventEmitter<ReplicationRule>();
@Output() editOne = new EventEmitter<ReplicationRule>();
@Output() toggleOne = new EventEmitter<ReplicationRule>();
@Output() redirect = new EventEmitter<ReplicationRule>();
projectScope: boolean;
rules: ReplicationRule[];
changedRules: ReplicationRule[];
ruleName: string;
@ViewChild('toggleConfirmDialog')
toggleConfirmDialog: ConfirmationDialogComponent;
@ -68,6 +75,38 @@ export class ListReplicationRuleComponent {
setInterval(()=>ref.markForCheck(), 500);
}
ngOnInit(): void {
this.projectScope = (!this.projectId);
this.retrieveRules();
}
retrieveRules(ruleName: string = ''): void {
this.loading = true;
toPromise<ReplicationRule[]>(this.replicationService
.getReplicationRules(this.projectId, ruleName))
.then(rules=>{
this.rules = rules || [];
if(this.rules && this.rules.length > 0) {
this.selectedId = this.rules[0].id || '';
this.selectOne.emit(this.rules[0]);
}
this.changedRules = this.rules;
this.loading = false;
}
).catch(error=>{
this.errorHandler.error(error);
this.loading = false;
});
}
filterRuleStatus(status: string) {
if(status === 'all') {
this.changedRules = this.rules;
} else {
this.changedRules = this.rules.filter(policy=>policy.enabled === +status);
}
}
toggleConfirm(message: ConfirmationAcknowledgement) {
if(message &&
message.source === ConfirmationTargets.TOGGLE_CONFIRM &&
@ -112,6 +151,10 @@ export class ListReplicationRuleComponent {
this.selectOne.emit(rule);
}
redirectTo(rule: ReplicationRule): void {
this.redirect.emit(rule);
}
editRule(rule: ReplicationRule) {
this.editOne.emit(rule);
}

View File

@ -7,7 +7,7 @@ export const LIST_REPOSITORY_TEMPLATE = `
<clr-dg-action-overflow [hidden]="!hasProjectAdminRole">
<button class="action-item" (click)="deleteRepo(r.name)">{{'REPOSITORY.DELETE' | translate}}</button>
</clr-dg-action-overflow>
<clr-dg-cell>{{r.name}}</clr-dg-cell>
<clr-dg-cell><a href="javascript:void(0)" (click)="gotoLink(projectId || r.project_id, r.name || r.repository_name)">{{r.name}}</a></clr-dg-cell>
<clr-dg-cell>{{r.tags_count}}</clr-dg-cell>
<clr-dg-cell>{{r.pull_count}}</clr-dg-cell>
</clr-dg-row>

View File

@ -2,6 +2,7 @@
import { ComponentFixture, TestBed, async } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { Router } from '@angular/router';
import { SharedModule } from '../shared/shared.module';
import { ConfirmationDialogComponent } from '../confirmation-dialog/confirmation-dialog.component';
@ -10,8 +11,11 @@ import { Repository } from '../service/interface';
import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
describe('ListRepositoryComponent (inline template)', () => {
class RouterStub {
navigateByUrl(url: string) { return url; }
}
describe('ListRepositoryComponent (inline template)', () => {
let comp: ListRepositoryComponent;
let fixture: ComponentFixture<ListRepositoryComponent>;
@ -45,7 +49,10 @@ describe('ListRepositoryComponent (inline template)', () => {
ListRepositoryComponent,
ConfirmationDialogComponent
],
providers: [{ provide: SERVICE_CONFIG, useValue: {} }]
providers: [
{ provide: Router, useClass: RouterStub },
{ provide: SERVICE_CONFIG, useValue: {} }
]
});
}));

View File

@ -1,4 +1,5 @@
import { Component, Input, Output, EventEmitter, ChangeDetectorRef, ChangeDetectionStrategy } from '@angular/core';
import { Router } from '@angular/router';
import { State, Comparator } from 'clarity-angular';
@ -13,6 +14,7 @@ import { CustomComparator } from '../utils';
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ListRepositoryComponent {
@Input() urlPrefix: string;
@Input() projectId: number;
@Input() repositories: Repository[];
@ -28,6 +30,7 @@ export class ListRepositoryComponent {
tagsCountComparator: Comparator<Repository> = new CustomComparator<Repository>('tags_count', 'number');
constructor(
private router: Router,
private ref: ChangeDetectorRef) {
let hnd = setInterval(()=>ref.markForCheck(), 100);
setTimeout(()=>clearInterval(hnd), 1000);
@ -44,4 +47,9 @@ export class ListRepositoryComponent {
this.paginate.emit(state);
}
}
public gotoLink(projectId: number, repoName: string): void {
let linkUrl = [this.urlPrefix, 'tags', projectId, repoName];
this.router.navigate(linkUrl);
}
}

View File

@ -3,7 +3,7 @@ export const REPLICATION_TEMPLATE: string = `
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<div class="row flex-items-xs-between">
<div class="flex-xs-middle option-left">
<button class="btn btn-link" (click)="openModal()"><clr-icon shape="add"></clr-icon> {{'REPLICATION.REPLICATION_RULE' | translate}}</button>
<button *ngIf="withReplicationJob" class="btn btn-link" (click)="openModal()"><clr-icon shape="add"></clr-icon> {{'REPLICATION.REPLICATION_RULE' | translate}}</button>
<create-edit-rule [projectId]="projectId" (reload)="reloadRules($event)"></create-edit-rule>
</div>
<div class="flex-xs-middle option-right">
@ -20,9 +20,9 @@ export const REPLICATION_TEMPLATE: string = `
</div>
</div>
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<list-replication-rule [rules]="changedRules" [projectless]="false" [selectedId]="initSelectedId" (selectOne)="selectOneRule($event)" (editOne)="openEditRule($event)" (reload)="reloadRules($event)" [loading]="loading"></list-replication-rule>
<hbr-list-replication-rule #listReplicationRule [projectId]="projectId" (selectOne)="selectOneRule($event)" (editOne)="openEditRule($event)" (reload)="reloadRules($event)" [loading]="loading" [withReplicationJob]="withReplicationJob" (redirect)="customRedirect($event)"></hbr-list-replication-rule>
</div>
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<div *ngIf="withReplicationJob" class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<div class="row flex-items-xs-between">
<h5 class="flex-items-xs-bottom option-left-down" style="margin-left: 14px;">{{'REPLICATION.REPLICATION_JOBS' | translate}}</h5>
<div class="flex-items-xs-bottom option-right-down">
@ -45,7 +45,7 @@ export const REPLICATION_TEMPLATE: string = `
</div>
</div>
</div>
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<div *ngIf="withReplicationJob" class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<clr-datagrid [clrDgLoading]="loading">
<clr-dg-column [clrDgField]="'repository'">{{'REPLICATION.NAME' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'status'">{{'REPLICATION.STATUS' | translate}}</clr-dg-column>

View File

@ -11,12 +11,13 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit, ViewChild, Input } from '@angular/core';
import { Component, OnInit, ViewChild, Input, Output, EventEmitter } from '@angular/core';
import { ResponseOptions, RequestOptions } from '@angular/http';
import { NgModel } from '@angular/forms';
import { TranslateService } from '@ngx-translate/core';
import { ListReplicationRuleComponent} from '../list-replication-rule/list-replication-rule.component';
import { CreateEditRuleComponent } from '../create-edit-rule/create-edit-rule.component';
import { ErrorHandler } from '../error-handler/error-handler';
@ -71,6 +72,9 @@ export class SearchOption {
export class ReplicationComponent implements OnInit {
@Input() projectId: number | string;
@Input() withReplicationJob: boolean;
@Output() redirect = new EventEmitter<ReplicationRule>();
search: SearchOption = new SearchOption();
@ -94,6 +98,9 @@ export class ReplicationComponent implements OnInit {
toggleJobSearchOption = optionalSearch;
currentJobSearchOption: number;
@ViewChild(ListReplicationRuleComponent)
listReplicationRule: ListReplicationRuleComponent;
@ViewChild(CreateEditRuleComponent)
createEditPolicyComponent: CreateEditRuleComponent;
@ -113,32 +120,8 @@ export class ReplicationComponent implements OnInit {
this.currentRuleStatus = this.ruleStatus[0];
this.currentJobStatus = this.jobStatus[0];
this.currentJobSearchOption = 0;
this.retrieveRules();
}
retrieveRules(): void {
this.loading = true;
toPromise<ReplicationRule[]>(this.replicationService
.getReplicationRules(this.projectId, this.search.ruleName))
.then(response=>{
this.changedRules = response || [];
if(this.changedRules && this.changedRules.length > 0) {
this.initSelectedId = this.changedRules[0].id || '';
}
this.rules = this.changedRules;
if(this.changedRules && this.changedRules.length > 0) {
this.search.ruleId = this.changedRules[0].id || '';
this.fetchReplicationJobs();
}
this.loading = false;
}
).catch(error=>{
this.errorHandler.error(error);
this.loading = false;
});
}
openModal(): void {
this.createEditPolicyComponent.openCreateEditRule(true);
}
@ -177,37 +160,39 @@ export class ReplicationComponent implements OnInit {
this.search.repoName = '';
this.search.status = '';
this.currentJobSearchOption = 0;
this.currentJobStatus = { 'key': 'all', 'description': 'REPLICATION.ALL' };
this.currentJobStatus = { 'key': 'all', 'description': 'REPLICATION.ALL' };
this.fetchReplicationJobs();
}
}
customRedirect(rule: ReplicationRule) {
this.redirect.emit(rule);
}
doSearchRules(ruleName: string) {
this.search.ruleName = ruleName;
this.retrieveRules();
this.listReplicationRule.retrieveRules(ruleName);
}
doFilterRuleStatus($event: any) {
if ($event && $event.target && $event.target["value"]) {
let status = $event.target["value"];
this.currentRuleStatus = this.ruleStatus.find((r: any)=>r.key === status);
if(this.currentRuleStatus.key === 'all') {
this.changedRules = this.rules;
} else {
this.changedRules = this.rules.filter(policy=>policy.enabled === +this.currentRuleStatus.key);
}
this.listReplicationRule.filterRuleStatus(this.currentRuleStatus.key);
}
}
doFilterJobStatus($event: any) {
if ($event && $event.target && $event.target["value"]) {
let status = $event.target["value"];
this.currentJobStatus = this.jobStatus.find((r: any)=>r.key === status);
if(this.currentJobStatus.key === 'all') {
status = '';
}
this.search.status = status;
this.doSearchJobs(this.search.repoName);
}
}
@ -219,12 +204,12 @@ export class ReplicationComponent implements OnInit {
reloadRules(isReady: boolean) {
if(isReady) {
this.search.ruleName = '';
this.retrieveRules();
this.listReplicationRule.retrieveRules(this.search.ruleName);
}
}
refreshRules() {
this.retrieveRules();
this.listReplicationRule.retrieveRules();
}
refreshJobs() {

View File

@ -0,0 +1,6 @@
import { Type } from '@angular/core';
import { RepositoryStackviewComponent } from './repository-stackview.component';
export const REPOSITORY_STACKVIEW_DIRECTIVES: Type<any>[] = [
RepositoryStackviewComponent
];

View File

@ -0,0 +1,26 @@
export const REPOSITORY_STACKVIEW_STYLES: string = `
.option-right {
padding-right: 16px;
margin-top: 32px;
margin-bottom: 12px;
}
.sub-grid-custom {
position: relative;
left: 40px;
}
:host >>> .datagrid .datagrid-body .datagrid-row {
overflow-x: hidden;
overflow-y: hidden;
background-color: #ccc;
}
:host >>> .datagrid-body .datagrid-row .datagrid-row-master{
background-color: #FFFFFF;
}
:host >>> .datagrid .datagrid-placeholder-container {
display: none;
}
`;

View File

@ -0,0 +1,34 @@
export const REPOSITORY_STACKVIEW_TEMPLATE: string = `
<confirmation-dialog #confirmationDialog (confirmAction)="confirmDeletion($event)"></confirmation-dialog>
<div class="row">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<div class="row flex-items-xs-right option-right">
<div class="flex-xs-middle">
<hbr-filter filterPlaceholder="{{'REPOSITORY.FILTER_FOR_REPOSITORIES' | translate}}" (filter)="doSearchRepoNames($event)"></hbr-filter>
<a href="javascript:void(0)" (click)="refresh()"><clr-icon shape="refresh"></clr-icon></a>
</div>
</div>
</div>
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<clr-datagrid>
<clr-dg-column [clrDgField]="'name'">{{'REPOSITORY.NAME' | translate}}</clr-dg-column>
<clr-dg-column [clrDgSortBy]="tagsCountComparator">{{'REPOSITORY.TAGS_COUNT' | translate}}</clr-dg-column>
<clr-dg-column [clrDgSortBy]="pullCountComparator">{{'REPOSITORY.PULL_COUNT' | translate}}</clr-dg-column>
<clr-dg-row *clrDgItems="let r of repositories">
<clr-dg-action-overflow [hidden]="!hasProjectAdminRole">
<button class="action-item" (click)="deleteRepo(r.name)">{{'REPOSITORY.DELETE' | translate}}</button>
</clr-dg-action-overflow>
<clr-dg-cell>{{r.name}}</clr-dg-cell>
<clr-dg-cell>{{r.tags_count}}</clr-dg-cell>
<clr-dg-cell>{{r.pull_count}}</clr-dg-cell>
<hbr-tag *clrIfExpanded ngProjectAs="clr-dg-row-detail" class="sub-grid-custom" [repoName]="r.name" [sessionInfo]="sessionInfo" [projectId]="projectId" [isEmbedded]="true" (refreshRepo)="refresh($event)"></hbr-tag>
</clr-dg-row>
<clr-dg-footer>
{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} {{'REPOSITORY.OF' | translate}}
{{pagination.totalItems}} {{'REPOSITORY.ITEMS' | translate}}
<clr-dg-pagination #pagination [clrDgPageSize]="15"></clr-dg-pagination>
</clr-dg-footer>
</clr-datagrid>
</div>
</div>
`;

View File

@ -0,0 +1,152 @@
import { ComponentFixture, TestBed, async } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { SharedModule } from '../shared/shared.module';
import { ConfirmationDialogComponent } from '../confirmation-dialog/confirmation-dialog.component';
import { RepositoryStackviewComponent } from './repository-stackview.component';
import { TagComponent } from '../tag/tag.component';
import { FilterComponent } from '../filter/filter.component';
import { ErrorHandler } from '../error-handler/error-handler';
import { Repository, Tag } from '../service/interface';
import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
import { RepositoryService, RepositoryDefaultService } from '../service/repository.service';
import { TagService, TagDefaultService } from '../service/tag.service';
import { click } from '../utils';
describe('RepositoryComponentStackview (inline template)', ()=> {
let compRepo: RepositoryStackviewComponent;
let fixtureRepo: ComponentFixture<RepositoryStackviewComponent>;
let repositoryService: RepositoryService;
let spyRepos: jasmine.Spy;
let compTag: TagComponent;
let fixtureTag: ComponentFixture<TagComponent>;
let tagService: TagService;
let spyTags: jasmine.Spy;
let mockRepoData: Repository[] = [
{
"id": 1,
"name": "library/busybox",
"project_id": 1,
"description": "",
"pull_count": 0,
"star_count": 0,
"tags_count": 1
},
{
"id": 2,
"name": "library/nginx",
"project_id": 1,
"description": "",
"pull_count": 0,
"star_count": 0,
"tags_count": 1
}
];
let mockTagData: Tag[] = [
{
"digest": "sha256:e5c82328a509aeb7c18c1d7fb36633dc638fcf433f651bdcda59c1cc04d3ee55",
"name": "1.11.5",
"architecture": "amd64",
"os": "linux",
"docker_version": "1.12.3",
"author": "NGINX Docker Maintainers \"docker-maint@nginx.com\"",
"created": new Date("2016-11-08T22:41:15.912313785Z"),
"signature": null
}
];
let config: IServiceConfig = {
repositoryBaseEndpoint: '/api/repository/testing'
};
beforeEach(async(()=>{
TestBed.configureTestingModule({
imports: [
SharedModule
],
declarations: [
RepositoryStackviewComponent,
TagComponent,
ConfirmationDialogComponent,
FilterComponent
],
providers: [
ErrorHandler,
{ provide: SERVICE_CONFIG, useValue : config },
{ provide: RepositoryService, useClass: RepositoryDefaultService },
{ provide: TagService, useClass: TagDefaultService }
]
});
}));
beforeEach(()=>{
fixtureRepo = TestBed.createComponent(RepositoryStackviewComponent);
compRepo = fixtureRepo.componentInstance;
compRepo.projectId = 1;
compRepo.sessionInfo = {
hasProjectAdminRole: true
};
repositoryService = fixtureRepo.debugElement.injector.get(RepositoryService);
spyRepos = spyOn(repositoryService, 'getRepositories').and.returnValues(Promise.resolve(mockRepoData));
fixtureRepo.detectChanges();
});
beforeEach(()=>{
fixtureTag = TestBed.createComponent(TagComponent);
compTag = fixtureTag.componentInstance;
compTag.projectId = compRepo.projectId;
compTag.repoName = 'library/busybox';
compTag.sessionInfo = compRepo.sessionInfo;
tagService = fixtureTag.debugElement.injector.get(TagService);
spyTags = spyOn(tagService, 'getTags').and.returnValues(Promise.resolve(mockTagData));
fixtureTag.detectChanges();
});
it('should load and render data', async(()=>{
fixtureRepo.detectChanges();
fixtureRepo.whenStable().then(()=>{
fixtureRepo.detectChanges();
let deRepo: DebugElement = fixtureRepo.debugElement.query(By.css('datagrid-cell'));
fixtureRepo.detectChanges();
expect(deRepo).toBeTruthy();
let elRepo: HTMLElement = deRepo.nativeElement;
fixtureRepo.detectChanges();
expect(elRepo).toBeTruthy();
fixtureRepo.detectChanges();
expect(elRepo.textContent).toEqual('library/busybox');
click(deRepo);
fixtureTag.detectChanges();
let deTag: DebugElement = fixtureTag.debugElement.query(By.css('datagrid-cell'));
expect(deTag).toBeTruthy();
let elTag: HTMLElement = deTag.nativeElement;
expect(elTag).toBeTruthy();
expect(elTag.textContent).toEqual('1.12.5');
});
}));
it('should filter data by keyword', async(()=>{
fixtureRepo.detectChanges();
fixtureRepo.whenStable().then(()=>{
fixtureRepo.detectChanges();
compRepo.doSearchRepoNames('nginx');
fixtureRepo.detectChanges();
let de: DebugElement[] = fixtureRepo.debugElement.queryAll(By.css('datagrid-cell'));
fixtureRepo.detectChanges();
expect(de).toBeTruthy();
expect(de.length).toEqual(1);
let el: HTMLElement = de[0].nativeElement;
fixtureRepo.detectChanges();
expect(el).toBeTruthy();
expect(el.textContent).toEqual('library/nginx');
});
}));
});

View File

@ -0,0 +1,111 @@
import { Component, Input, OnInit, ViewChild, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { Comparator } from 'clarity-angular';
import { REPOSITORY_STACKVIEW_TEMPLATE } from './repository-stackview.component.html';
import { REPOSITORY_STACKVIEW_STYLES } from './repository-stackview.component.css';
import { Repository, SessionInfo } from '../service/interface';
import { ErrorHandler } from '../error-handler/error-handler';
import { RepositoryService } from '../service/repository.service';
import { toPromise, CustomComparator } from '../utils';
import { ConfirmationState, ConfirmationTargets, ConfirmationButtons } from '../shared/shared.const';
import { ConfirmationDialogComponent } from '../confirmation-dialog/confirmation-dialog.component';
import { ConfirmationMessage } from '../confirmation-dialog/confirmation-message';
import { ConfirmationAcknowledgement } from '../confirmation-dialog/confirmation-state-message';
import { Subscription } from 'rxjs/Subscription';
@Component({
selector: 'hbr-repository-stackview',
template: REPOSITORY_STACKVIEW_TEMPLATE,
styles: [ REPOSITORY_STACKVIEW_STYLES ],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class RepositoryStackviewComponent implements OnInit {
@Input() projectId: number;
@Input() sessionInfo: SessionInfo;
lastFilteredRepoName: string;
hasProjectAdminRole: boolean;
repositories: Repository[];
@ViewChild('confirmationDialog')
confirmationDialog: ConfirmationDialogComponent;
pullCountComparator: Comparator<Repository> = new CustomComparator<Repository>('pull_count', 'number');
tagsCountComparator: Comparator<Repository> = new CustomComparator<Repository>('tags_count', 'number');
constructor(
private errorHandler: ErrorHandler,
private translateService: TranslateService,
private repositoryService: RepositoryService,
private ref: ChangeDetectorRef){}
confirmDeletion(message: ConfirmationAcknowledgement) {
if (message &&
message.source === ConfirmationTargets.REPOSITORY &&
message.state === ConfirmationState.CONFIRMED) {
let repoName = message.data;
toPromise<number>(this.repositoryService
.deleteRepository(repoName))
.then(
response => {
this.refresh();
this.translateService.get('REPOSITORY.DELETED_REPO_SUCCESS')
.subscribe(res=>this.errorHandler.info(res));
}).catch(error => this.errorHandler.error(error));
}
}
ngOnInit(): void {
if(!this.projectId) {
this.errorHandler.error('Project ID cannot be unset.');
return;
}
if(!this.sessionInfo) {
this.errorHandler.error('Session info cannot be unset.');
return;
}
this.hasProjectAdminRole = this.sessionInfo.hasProjectAdminRole || false;
this.lastFilteredRepoName = '';
this.retrieve();
}
retrieve() {
toPromise<Repository[]>(this.repositoryService
.getRepositories(this.projectId, this.lastFilteredRepoName))
.then(
repos => this.repositories = repos,
error => this.errorHandler.error(error));
let hnd = setInterval(()=>this.ref.markForCheck(), 100);
setTimeout(()=>clearInterval(hnd), 1000);
}
doSearchRepoNames(repoName: string) {
this.lastFilteredRepoName = repoName;
this.retrieve();
}
deleteRepo(repoName: string) {
let message = new ConfirmationMessage(
'REPOSITORY.DELETION_TITLE_REPO',
'REPOSITORY.DELETION_SUMMARY_REPO',
repoName,
repoName,
ConfirmationTargets.REPOSITORY,
ConfirmationButtons.DELETE_CANCEL);
this.confirmationDialog.open(message);
}
refresh() {
this.retrieve();
}
}

View File

@ -10,6 +10,6 @@ export const REPOSITORY_TEMPLATE = `
</div>
</div>
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<hbr-list-repository [projectId]="projectId" [repositories]="changedRepositories" (delete)="deleteRepo($event)" [hasProjectAdminRole]="hasProjectAdminRole" (paginate)="retrieve($event)"></hbr-list-repository>
<hbr-list-repository [urlPrefix]="urlPrefix" [projectId]="projectId" [repositories]="changedRepositories" (delete)="deleteRepo($event)" [hasProjectAdminRole]="hasProjectAdminRole" (paginate)="retrieve($event)"></hbr-list-repository>
</div>
</div>`;

View File

@ -1,6 +1,7 @@
import { ComponentFixture, TestBed, async } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { Router } from '@angular/router';
import { SharedModule } from '../shared/shared.module';
import { ConfirmationDialogComponent } from '../confirmation-dialog/confirmation-dialog.component';
@ -13,6 +14,10 @@ import { Repository } from '../service/interface';
import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
import { RepositoryService, RepositoryDefaultService } from '../service/repository.service';
class RouterStub {
navigateByUrl(url: string) { return url; }
}
describe('RepositoryComponent (inline template)', ()=> {
let comp: RepositoryComponent;
@ -59,7 +64,8 @@ describe('RepositoryComponent (inline template)', ()=> {
providers: [
ErrorHandler,
{ provide: SERVICE_CONFIG, useValue : config },
{ provide: RepositoryService, useClass: RepositoryDefaultService }
{ provide: RepositoryService, useClass: RepositoryDefaultService },
{ provide: Router, useClass: RouterStub }
]
});
}));

View File

@ -43,6 +43,7 @@ export class RepositoryComponent implements OnInit {
@Input() projectId: number;
@Input() sessionInfo: SessionInfo;
@Input() urlPrefix: string;
lastFilteredRepoName: string;

View File

@ -67,7 +67,7 @@ export class AccessLogDefaultService extends AccessLogService {
url = '/api/logs';
}
return this.http.get(url+`?lines=${lines}`, HTTP_JSON_OPTIONS).toPromise()
return this.http.get(url+`?page_size=${lines}`, HTTP_JSON_OPTIONS).toPromise()
.then(response => response.json() as AccessLog[])
.catch(error => Promise.reject(error));
}

View File

@ -1,4 +1,33 @@
export const TAG_STYLE = `
.sub-header-title {
margin-top: 12px;
}`;
margin: 12px 0;
}
.embeded-datagrid {
width: 98%;
}
.hidden-tag {
display: block; height: 0;
}
:host >>> .datagrid {
margin: 0;
}
:host >>> .datagrid-placeholder {
display: none;
}
:host >>> .datagrid .datagrid-body {
background-color: #eee;
}
:host >>> .datagrid .datagrid-head .datagrid-row {
background-color: #eee;
}
:host >>> .datagrid .datagrid-body .datagrid-row-master {
background-color: #eee;
}
`;

View File

@ -1,6 +1,6 @@
export const TAG_TEMPLATE = `
<confirmation-dialog #confirmationDialog (confirmAction)="confirmDeletion($event)"></confirmation-dialog>
<clr-modal [(clrModalOpen)]="showTagManifestOpened" [clrModalStaticBackdrop]="staticBackdrop" [clrModalClosable]="closable">
<confirmation-dialog class="hidden-tag" #confirmationDialog (confirmAction)="confirmDeletion($event)"></confirmation-dialog>
<clr-modal class="hidden-tag" [(clrModalOpen)]="showTagManifestOpened" [clrModalStaticBackdrop]="staticBackdrop" [clrModalClosable]="closable">
<h3 class="modal-title">{{ manifestInfoTitle | translate }}</h3>
<div class="modal-body">
<div class="row col-md-12">
@ -11,8 +11,9 @@ export const TAG_TEMPLATE = `
<button type="button" class="btn btn-primary" (click)="showTagManifestOpened = false">{{'BUTTON.OK' | translate}}</button>
</div>
</clr-modal>
<h2 class="sub-header-title">{{repoName}}</h2>
<clr-datagrid [clrDgLoading]="loading">
<h2 *ngIf="!isEmbedded" class="sub-header-title">{{repoName}}</h2>
<clr-datagrid [clrDgLoading]="loading" [class.embeded-datagrid]="isEmbedded">
<clr-dg-column [clrDgField]="'name'">{{'REPOSITORY.TAG' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.PULL_COMMAND' | translate}}</clr-dg-column>
<clr-dg-column *ngIf="withNotary">{{'REPOSITORY.SIGNED' | translate}}</clr-dg-column>
@ -42,9 +43,9 @@ export const TAG_TEMPLATE = `
<clr-dg-cell>{{t.architecture}}</clr-dg-cell>
<clr-dg-cell>{{t.os}}</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>
<clr-dg-footer>
{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} {{'REPOSITORY.OF' | translate}}
{{pagination.totalItems}} {{'REPOSITORY.ITEMS' | translate}}
<clr-dg-pagination #pagination [clrDgPageSize]="10"></clr-dg-pagination>
{{pagination.totalItems}} {{'REPOSITORY.ITEMS' | translate}}&nbsp;&nbsp;&nbsp;&nbsp;
<clr-dg-pagination #pagination [clrDgPageSize]="5"></clr-dg-pagination>
</clr-dg-footer>
</clr-datagrid>`;

View File

@ -11,7 +11,7 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit, ViewChild, Input, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
import { Component, OnInit, ViewChild, Input, Output, EventEmitter, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
import { TagService } from '../service/tag.service';
import { ErrorHandler } from '../error-handler/error-handler';
@ -43,6 +43,9 @@ export class TagComponent implements OnInit {
@Input() projectId: number;
@Input() repoName: string;
@Input() sessionInfo: SessionInfo;
@Input() isEmbedded: boolean;
@Output() refreshRepo = new EventEmitter<boolean>();
hasProjectAdminRole: boolean;
@ -73,23 +76,23 @@ export class TagComponent implements OnInit {
confirmDeletion(message: ConfirmationAcknowledgement) {
if (message &&
message.source === ConfirmationTargets.TAG
&& message.state === ConfirmationState.CONFIRMED) {
let tag: Tag = message.data;
if (tag) {
if (tag.signature) {
return;
} else {
toPromise<number>(this.tagService
.deleteTag(this.repoName, tag.name))
.then(
response => {
this.retrieve();
this.translateService.get('REPOSITORY.DELETED_TAG_SUCCESS')
.subscribe(res=>this.errorHandler.info(res));
}).catch(error => this.errorHandler.error(error));
}
message.source === ConfirmationTargets.TAG
&& message.state === ConfirmationState.CONFIRMED) {
let tag: Tag = message.data;
if (tag) {
if (tag.signature) {
return;
} else {
toPromise<number>(this.tagService
.deleteTag(this.repoName, tag.name))
.then(
response => {
this.retrieve();
this.translateService.get('REPOSITORY.DELETED_TAG_SUCCESS')
.subscribe(res=>this.errorHandler.info(res));
}).catch(error => this.errorHandler.error(error));
}
}
}
}
@ -122,6 +125,9 @@ export class TagComponent implements OnInit {
.then(items => {
this.tags = items;
this.loading = false;
if(this.tags && this.tags.length === 0) {
this.refreshRepo.emit(true);
}
})
.catch(error => {
this.errorHandler.error(error);

View File

@ -104,7 +104,7 @@ export class CustomComparator<T> implements Comparator<T> {
compare(a: {[key: string]: any| any[]}, b: {[key: string]: any| any[]}) {
let comp = 0;
if(a && b && a[this.fieldName] && b[this.fieldName]) {
if(a && b) {
let fieldA = a[this.fieldName];
let fieldB = b[this.fieldName];
switch(this.type) {

View File

@ -49,7 +49,7 @@ export class AuditLogService {
}
getRecentLogs(lines: number): Observable<AuditLog[]> {
return this.http.get(logEndpoint + "?lines=" + lines, this.httpOptions)
return this.http.get(logEndpoint + "?page_size=" + lines, this.httpOptions)
.map(response => response.json() as AuditLog[])
.catch(error => Observable.throw(error));
}