mirror of
https://github.com/goharbor/harbor.git
synced 2024-12-28 03:27:41 +01:00
Merge remote-tracking branch 'upstream/master' into 170525_log_pagination
Conflicts: src/common/security/rbac/context_test.go src/ui/api/statistic.go src/ui/projectmanager/pm.go
This commit is contained in:
commit
9c4adbe8c9
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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")
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -110,6 +110,11 @@ func (f *fakePM) GetAll(*models.ProjectQueryParam) ([]*models.Project, error) {
|
||||
return []*models.Project{}, nil
|
||||
}
|
||||
|
||||
// nil implement
|
||||
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
|
||||
|
@ -20,7 +20,6 @@ import (
|
||||
"strconv"
|
||||
|
||||
"github.com/vmware/harbor/src/common"
|
||||
"github.com/vmware/harbor/src/common/api"
|
||||
"github.com/vmware/harbor/src/common/dao"
|
||||
"github.com/vmware/harbor/src/common/utils/log"
|
||||
"github.com/vmware/harbor/src/ui/config"
|
||||
@ -91,20 +90,19 @@ var (
|
||||
|
||||
// ConfigAPI ...
|
||||
type ConfigAPI struct {
|
||||
api.BaseAPI
|
||||
BaseController
|
||||
}
|
||||
|
||||
// Prepare validates the user
|
||||
func (c *ConfigAPI) Prepare() {
|
||||
userID := c.ValidateUser()
|
||||
isSysAdmin, err := dao.IsAdminRole(userID)
|
||||
if err != nil {
|
||||
log.Errorf("failed to check the role of user: %v", err)
|
||||
c.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
||||
c.BaseController.Prepare()
|
||||
if !c.SecurityCtx.IsAuthenticated() {
|
||||
c.HandleUnauthorized()
|
||||
return
|
||||
}
|
||||
|
||||
if !isSysAdmin {
|
||||
c.CustomAbort(http.StatusForbidden, http.StatusText(http.StatusForbidden))
|
||||
if !c.SecurityCtx.IsSysAdmin() {
|
||||
c.HandleForbidden(c.SecurityCtx.GetUsername())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -20,8 +20,6 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/vmware/harbor/src/common/api"
|
||||
"github.com/vmware/harbor/src/common/dao"
|
||||
"github.com/vmware/harbor/src/common/models"
|
||||
ldapUtils "github.com/vmware/harbor/src/common/utils/ldap"
|
||||
"github.com/vmware/harbor/src/common/utils/log"
|
||||
@ -29,25 +27,22 @@ import (
|
||||
|
||||
// LdapAPI handles requesst to /api/ldap/ping /api/ldap/user/search /api/ldap/user/import
|
||||
type LdapAPI struct {
|
||||
api.BaseAPI
|
||||
BaseController
|
||||
}
|
||||
|
||||
const metaChars = "&|!=~*<>()"
|
||||
|
||||
// Prepare ...
|
||||
func (l *LdapAPI) Prepare() {
|
||||
|
||||
userID := l.ValidateUser()
|
||||
isSysAdmin, err := dao.IsAdminRole(userID)
|
||||
if err != nil {
|
||||
log.Errorf("error occurred in IsAdminRole: %v", err)
|
||||
l.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
||||
l.BaseController.Prepare()
|
||||
if !l.SecurityCtx.IsAuthenticated() {
|
||||
l.HandleUnauthorized()
|
||||
return
|
||||
}
|
||||
|
||||
if !isSysAdmin {
|
||||
l.CustomAbort(http.StatusForbidden, http.StatusText(http.StatusForbidden))
|
||||
if !l.SecurityCtx.IsSysAdmin() {
|
||||
l.HandleForbidden(l.SecurityCtx.GetUsername())
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Ping ...
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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))
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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.ProjectQueryParam{
|
||||
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.ProjectQueryParam{
|
||||
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
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
|
@ -200,3 +200,13 @@ 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])
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -36,4 +36,8 @@ type ProjectManager interface {
|
||||
GetAll(query *models.ProjectQueryParam) ([]*models.Project, error)
|
||||
// GetTotal returns the total count according to the query parameters
|
||||
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)
|
||||
}
|
||||
|
@ -1,2 +1,530 @@
|
||||
# harbor-angular
|
||||
For publishing Harbor shared UI components.
|
||||
# Harbor UI library
|
||||
Wrap the following Harbor UI components into a sharable library and published as npm package for other third-party applications to import and reuse.
|
||||
|
||||
* Repository and tag management view
|
||||
* Replication rules and jobs management view
|
||||
* Replication endpoints management view
|
||||
* Access log list view
|
||||
* Vulnerability scanning result bar chart and list view (Embedded in tag management view)
|
||||
|
||||
The Harbor UI library is built on **[Angular ](https://angular.io/)** 4.x and **[Clarity ](https://vmware.github.io/clarity/)** 0.9.x .
|
||||
|
||||
The library is published to the public npm repository with name **[harbor-ui](https://www.npmjs.com/package/harbor-ui)**.
|
||||
|
||||
## Build & Test
|
||||
Build library with command:
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
|
||||
Execute the testing specs with command:
|
||||
```
|
||||
npm run test
|
||||
```
|
||||
|
||||
## Usage
|
||||
**Add dependency to application**
|
||||
|
||||
Execute install command to add dependency to package.json
|
||||
```
|
||||
npm install harbor-ui --save
|
||||
```
|
||||
The latest version of the library will be installed.
|
||||
|
||||
**Import the library module into the root Angular module**
|
||||
```
|
||||
import { HarborLibraryModule } from 'harbor-ui';
|
||||
|
||||
@NgModule({
|
||||
declarations: [...],
|
||||
imports: [
|
||||
HarborLibraryModule.forRoot()
|
||||
],
|
||||
providers: [...],
|
||||
bootstrap: [...]
|
||||
})
|
||||
export class AppModule {
|
||||
}
|
||||
```
|
||||
If no parameters are passed to **'forRoot'**, the module will be initialized with default configurations. If re-configuration required, please refer the **Configurations** parts.
|
||||
|
||||
## Configurations
|
||||
All the related configurations are defined in the **HarborModuleConfig** interface.
|
||||
|
||||
**1. config**
|
||||
The base configuration for the module. Mainly used to define the relevant endpoints of services which are in charge of retrieving data from backend APIs. It's a 'OpaqueToken' and defined by 'IServiceConfig' interface. If **config** is not set, the default value will be used.
|
||||
```
|
||||
export const DefaultServiceConfig: IServiceConfig = {
|
||||
repositoryBaseEndpoint: "/api/repositories",
|
||||
logBaseEndpoint: "/api/logs",
|
||||
targetBaseEndpoint: "/api/targets",
|
||||
replicationRuleEndpoint: "/api/policies/replication",
|
||||
replicationJobEndpoint: "/api/jobs/replication",
|
||||
langCookieKey: DEFAULT_LANG_COOKIE_KEY,
|
||||
supportedLangs: DEFAULT_SUPPORTING_LANGS,
|
||||
enablei18Support: false
|
||||
};
|
||||
```
|
||||
If you want to override the related items, declare your own 'IServiceConfig' interface and define the configuration value. E.g: Override 'repositoryBaseEndpoint'
|
||||
```
|
||||
export const MyServiceConfig: IServiceConfig = {
|
||||
repositoryBaseEndpoint: "/api/wrap/repositories"
|
||||
}
|
||||
|
||||
...
|
||||
HarborLibraryModule.forRoot({
|
||||
config: { provide: SERVICE_CONFIG, useValue: MyServiceConfig }
|
||||
})
|
||||
...
|
||||
|
||||
```
|
||||
It supports partially overriding. For the items not overridden, default values will be adopted. The items contained in **config** are:
|
||||
* **repositoryBaseEndpoint:** The base endpoint of the service used to handle the repositories of registry and/or tags of repository. Default value is "/api/repositories".
|
||||
* **logBaseEndpoint:** The base endpoint of the service used to handle the recent access logs. Default is "/api/logs".
|
||||
* **targetBaseEndpoint:** The base endpoint of the service used to handle the registry endpoints. Default is "/api/targets".
|
||||
* **replicationRuleEndpoint:** The base endpoint of the service used to handle the replication rules. Default is "/api/policies/replication".
|
||||
* **replicationJobEndpoint:** The base endpoint of the service used to handle the replication jobs. Default is "/api/jobs/replication".
|
||||
* **langCookieKey:** The cookie key used to store the current used language preference. Default is "harbor-lang".
|
||||
* **supportedLangs:** Declare what languages are supported. Default is ['en-us', 'zh-cn', 'es-es'].
|
||||
* **enablei18Support:** To determine whether to not enable the i18 multiple languages supporting. Default is false.
|
||||
|
||||
**2. errorHandler**
|
||||
UI components in the library use this interface to pass the errors/warnings/infos/logs to the top component or page. The top component or page can display those information in their message panel or notification system.
|
||||
If not set, the console will be used as default output approach.
|
||||
|
||||
```
|
||||
@Injectable()
|
||||
export class MyErrorHandler extends ErrorHandler {
|
||||
public error(error: any): void {
|
||||
...
|
||||
}
|
||||
|
||||
public warning(warning: any): void {
|
||||
...
|
||||
}
|
||||
|
||||
public info(info: any): void {
|
||||
...
|
||||
}
|
||||
|
||||
public log(log: any): void {
|
||||
...
|
||||
}
|
||||
}
|
||||
|
||||
...
|
||||
HarborLibraryModule.forRoot({
|
||||
errorHandler: { provide: ErrorHandler, useClass: MyErrorHandler }
|
||||
})
|
||||
...
|
||||
|
||||
```
|
||||
**3. user session(Ongoing/Discussing)**
|
||||
Some components may need the user authorization and authentication information to display different views. There might be two alternatives to select:
|
||||
* Use @Input properties or interface to let top component or page to pass the required user session information in.
|
||||
* Component retrieves the required information from some API provided by top component or page when necessary.
|
||||
|
||||
**4. services**
|
||||
The library has its own service implementations to communicate with backend APIs and transfer data. If you want to use your own data handling logic, you can implement your own services based on the defined interfaces.
|
||||
|
||||
* **AccessLogService:** Define service methods to handle the access log related things.
|
||||
```
|
||||
@Injectable()
|
||||
export class MyAccessLogService extends AccessLogService {
|
||||
/**
|
||||
* Get the audit logs for the specified project.
|
||||
* Set query parameters through 'queryParams', support:
|
||||
* - page
|
||||
* - pageSize
|
||||
*
|
||||
* @abstract
|
||||
* @param {(number | string)} projectId
|
||||
* @param {RequestQueryParams} [queryParams]
|
||||
* @returns {(Observable<AccessLog[]> | Promise<AccessLog[]> | AccessLog[])}
|
||||
*
|
||||
* @memberOf AccessLogService
|
||||
*/
|
||||
getAuditLogs(projectId: number | string, queryParams?: RequestQueryParams): Observable<AccessLog[]> | Promise<AccessLog[]> | AccessLog[]{
|
||||
...
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the recent logs.
|
||||
*
|
||||
* @abstract
|
||||
* @param {number} lines : Specify how many lines should be returned.
|
||||
* @returns {(Observable<AccessLog[]> | Promise<AccessLog[]> | AccessLog[])}
|
||||
*
|
||||
* @memberOf AccessLogService
|
||||
*/
|
||||
getRecentLogs(lines: number): Observable<AccessLog[]> | Promise<AccessLog[]> | AccessLog[]{
|
||||
...
|
||||
}
|
||||
}
|
||||
|
||||
...
|
||||
HarborLibraryModule.forRoot({
|
||||
logService: { provide: AccessLogService, useClass: MyAccessLogService }
|
||||
})
|
||||
...
|
||||
|
||||
```
|
||||
|
||||
* **EndpointService:** Define the service methods to handle the endpoint related things.
|
||||
```
|
||||
@Injectable()
|
||||
export class MyEndpointService extends EndpointService {
|
||||
/**
|
||||
* Get all the endpoints.
|
||||
* Set the argument 'endpointName' to return only the endpoints match the name pattern.
|
||||
*
|
||||
* @abstract
|
||||
* @param {string} [endpointName]
|
||||
* @param {RequestQueryParams} [queryParams]
|
||||
* @returns {(Observable<Endpoint[]> | Endpoint[])}
|
||||
*
|
||||
* @memberOf EndpointService
|
||||
*/
|
||||
getEndpoints(endpointName?: string, queryParams?: RequestQueryParams): Observable<Endpoint[]> | Promise<Endpoint[]> | Endpoint[] {
|
||||
...
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the specified endpoint.
|
||||
*
|
||||
* @abstract
|
||||
* @param {(number | string)} endpointId
|
||||
* @returns {(Observable<Endpoint> | Endpoint)}
|
||||
*
|
||||
* @memberOf EndpointService
|
||||
*/
|
||||
getEndpoint(endpointId: number | string): Observable<Endpoint> | Promise<Endpoint> | Endpoint {
|
||||
...
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new endpoint.
|
||||
*
|
||||
* @abstract
|
||||
* @param {Endpoint} endpoint
|
||||
* @returns {(Observable<any> | any)}
|
||||
*
|
||||
* @memberOf EndpointService
|
||||
*/
|
||||
createEndpoint(endpoint: Endpoint): Observable<any> | Promise<any> | any {
|
||||
...
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified endpoint.
|
||||
*
|
||||
* @abstract
|
||||
* @param {(number | string)} endpointId
|
||||
* @param {Endpoint} endpoint
|
||||
* @returns {(Observable<any> | any)}
|
||||
*
|
||||
* @memberOf EndpointService
|
||||
*/
|
||||
updateEndpoint(endpointId: number | string, endpoint: Endpoint): Observable<any> | Promise<any> | any {
|
||||
...
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the specified endpoint.
|
||||
*
|
||||
* @abstract
|
||||
* @param {(number | string)} endpointId
|
||||
* @returns {(Observable<any> | any)}
|
||||
*
|
||||
* @memberOf EndpointService
|
||||
*/
|
||||
deleteEndpoint(endpointId: number | string): Observable<any> | Promise<any> | any {
|
||||
...
|
||||
}
|
||||
|
||||
/**
|
||||
* Ping the specified endpoint.
|
||||
*
|
||||
* @abstract
|
||||
* @param {Endpoint} endpoint
|
||||
* @returns {(Observable<any> | any)}
|
||||
*
|
||||
* @memberOf EndpointService
|
||||
*/
|
||||
pingEndpoint(endpoint: Endpoint): Observable<any> | Promise<any> | any {
|
||||
...
|
||||
}
|
||||
|
||||
/**
|
||||
* Check endpoint whether in used with specific replication rule.
|
||||
*
|
||||
* @abstract
|
||||
* @param {{number | string}} endpointId
|
||||
* @returns {{Observable<any> | any}}
|
||||
*/
|
||||
getEndpointWithReplicationRules(endpointId: number | string): Observable<any> | Promise<any> | any {
|
||||
...
|
||||
}
|
||||
}
|
||||
|
||||
...
|
||||
HarborLibraryModule.forRoot({
|
||||
endpointService: { provide: EndpointService, useClass: MyEndpointService }
|
||||
})
|
||||
...
|
||||
|
||||
```
|
||||
|
||||
* **ReplicationService:** Define the service methods to handle the replication (rule and job) related things.
|
||||
```
|
||||
@Injectable()
|
||||
export class MyReplicationService extends ReplicationService {
|
||||
/**
|
||||
* Get the replication rules.
|
||||
* Set the argument 'projectId' to limit the data scope to the specified project;
|
||||
* set the argument 'ruleName' to return the rule only match the name pattern;
|
||||
* if pagination needed, use the queryParams to add query parameters.
|
||||
*
|
||||
* @abstract
|
||||
* @param {(number | string)} [projectId]
|
||||
* @param {string} [ruleName]
|
||||
* @param {RequestQueryParams} [queryParams]
|
||||
* @returns {(Observable<ReplicationRule[]> | Promise<ReplicationRule[]> | ReplicationRule[])}
|
||||
*
|
||||
* @memberOf ReplicationService
|
||||
*/
|
||||
getReplicationRules(projectId?: number | string, ruleName?: string, queryParams?: RequestQueryParams): Observable<ReplicationRule[]> | Promise<ReplicationRule[]> | ReplicationRule[] {
|
||||
...
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the specified replication rule.
|
||||
*
|
||||
* @abstract
|
||||
* @param {(number | string)} ruleId
|
||||
* @returns {(Observable<ReplicationRule> | Promise<ReplicationRule> | ReplicationRule)}
|
||||
*
|
||||
* @memberOf ReplicationService
|
||||
*/
|
||||
getReplicationRule(ruleId: number | string): Observable<ReplicationRule> | Promise<ReplicationRule> | ReplicationRule {
|
||||
...
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new replication rule.
|
||||
*
|
||||
* @abstract
|
||||
* @param {ReplicationRule} replicationRule
|
||||
* @returns {(Observable<any> | Promise<any> | any)}
|
||||
*
|
||||
* @memberOf ReplicationService
|
||||
*/
|
||||
createReplicationRule(replicationRule: ReplicationRule): Observable<any> | Promise<any> | any {
|
||||
...
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified replication rule.
|
||||
*
|
||||
* @abstract
|
||||
* @param {ReplicationRule} replicationRule
|
||||
* @returns {(Observable<any> | Promise<any> | any)}
|
||||
*
|
||||
* @memberOf ReplicationService
|
||||
*/
|
||||
updateReplicationRule(replicationRule: ReplicationRule): Observable<any> | Promise<any> | any {
|
||||
...
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the specified replication rule.
|
||||
*
|
||||
* @abstract
|
||||
* @param {(number | string)} ruleId
|
||||
* @returns {(Observable<any> | Promise<any> | any)}
|
||||
*
|
||||
* @memberOf ReplicationService
|
||||
*/
|
||||
deleteReplicationRule(ruleId: number | string): Observable<any> | Promise<any> | any {
|
||||
...
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable the specified replication rule.
|
||||
*
|
||||
* @abstract
|
||||
* @param {(number | string)} ruleId
|
||||
* @returns {(Observable<any> | Promise<any> | any)}
|
||||
*
|
||||
* @memberOf ReplicationService
|
||||
*/
|
||||
enableReplicationRule(ruleId: number | string, enablement: number): Observable<any> | Promise<any> | any {
|
||||
...
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable the specified replication rule.
|
||||
*
|
||||
* @abstract
|
||||
* @param {(number | string)} ruleId
|
||||
* @returns {(Observable<any> | Promise<any> | any)}
|
||||
*
|
||||
* @memberOf ReplicationService
|
||||
*/
|
||||
disableReplicationRule(ruleId: number | string): Observable<any> | Promise<any> | any {
|
||||
...
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the jobs for the specified replication rule.
|
||||
* Set query parameters through 'queryParams', support:
|
||||
* - status
|
||||
* - repository
|
||||
* - startTime and endTime
|
||||
* - page
|
||||
* - pageSize
|
||||
*
|
||||
* @abstract
|
||||
* @param {(number | string)} ruleId
|
||||
* @param {RequestQueryParams} [queryParams]
|
||||
* @returns {(Observable<ReplicationJob> | Promise<ReplicationJob[]> | ReplicationJob)}
|
||||
*
|
||||
* @memberOf ReplicationService
|
||||
*/
|
||||
getJobs(ruleId: number | string, queryParams?: RequestQueryParams): Observable<ReplicationJob[]> | Promise<ReplicationJob[]> | ReplicationJob[] {
|
||||
...
|
||||
}
|
||||
}
|
||||
|
||||
...
|
||||
HarborLibraryModule.forRoot({
|
||||
replicationService: { provide: ReplicationService, useClass: MyReplicationService }
|
||||
})
|
||||
...
|
||||
|
||||
```
|
||||
|
||||
* **RepositoryService:** Define service methods for handling the repository related things.
|
||||
```
|
||||
@Injectable()
|
||||
export class MyRepositoryService extends RepositoryService {
|
||||
/**
|
||||
* List all the repositories in the specified project.
|
||||
* Specify the 'repositoryName' to only return the repositories which match the name pattern.
|
||||
* If pagination needed, set the following parameters in queryParams:
|
||||
* 'page': current page,
|
||||
* 'page_size': page size.
|
||||
*
|
||||
* @abstract
|
||||
* @param {(number | string)} projectId
|
||||
* @param {string} repositoryName
|
||||
* @param {RequestQueryParams} [queryParams]
|
||||
* @returns {(Observable<Repository[]> | Promise<Repository[]> | Repository[])}
|
||||
*
|
||||
* @memberOf RepositoryService
|
||||
*/
|
||||
getRepositories(projectId: number | string, repositoryName?: string, queryParams?: RequestQueryParams): Observable<Repository[]> | Promise<Repository[]> | Repository[] {
|
||||
...
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE the specified repository.
|
||||
*
|
||||
* @abstract
|
||||
* @param {string} repositoryName
|
||||
* @returns {(Observable<any> | Promise<any> | any)}
|
||||
*
|
||||
* @memberOf RepositoryService
|
||||
*/
|
||||
deleteRepository(repositoryName: string): Observable<any> | Promise<any> | any {
|
||||
...
|
||||
}
|
||||
}
|
||||
|
||||
...
|
||||
HarborLibraryModule.forRoot({
|
||||
repositoryService: { provide: RepositoryService, useClass: MyRepositoryService }
|
||||
})
|
||||
...
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
@Injectable()
|
||||
export class MyTagService extends TagService {
|
||||
/**
|
||||
* Get all the tags under the specified repository.
|
||||
* NOTES: If the Notary is enabled, the signatures should be included in the returned data.
|
||||
*
|
||||
* @abstract
|
||||
* @param {string} repositoryName
|
||||
* @param {RequestQueryParams} [queryParams]
|
||||
* @returns {(Observable<Tag[]> | Promise<Tag[]> | Tag[])}
|
||||
*
|
||||
* @memberOf TagService
|
||||
*/
|
||||
getTags(repositoryName: string, queryParams?: RequestQueryParams): Observable<Tag[]> | Promise<Tag[]> | Tag[] {
|
||||
...
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the specified tag.
|
||||
*
|
||||
* @abstract
|
||||
* @param {string} repositoryName
|
||||
* @param {string} tag
|
||||
* @returns {(Observable<any> | any)}
|
||||
*
|
||||
* @memberOf TagService
|
||||
*/
|
||||
deleteTag(repositoryName: string, tag: string): Observable<any> | Promise<Tag> | any {
|
||||
...
|
||||
}
|
||||
}
|
||||
|
||||
...
|
||||
HarborLibraryModule.forRoot({
|
||||
tagService: { provide: TagService, useClass: MyTagService }
|
||||
})
|
||||
...
|
||||
|
||||
```
|
||||
|
||||
* **ScanningResultService:** Get the vulnerabilities scanning results for the specified tag.
|
||||
```
|
||||
@Injectable()
|
||||
export class MyScanningResultService extends ScanningResultService {
|
||||
/**
|
||||
* Get the summary of vulnerability scanning result.
|
||||
*
|
||||
* @abstract
|
||||
* @param {string} tagId
|
||||
* @returns {(Observable<ScanningResultSummary> | Promise<ScanningResultSummary> | ScanningResultSummary)}
|
||||
*
|
||||
* @memberOf ScanningResultService
|
||||
*/
|
||||
getScanningResultSummary(tagId: string): Observable<ScanningResultSummary> | Promise<ScanningResultSummary> | ScanningResultSummary {
|
||||
...
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the detailed vulnerabilities scanning results.
|
||||
*
|
||||
* @abstract
|
||||
* @param {string} tagId
|
||||
* @returns {(Observable<ScanningDetailResult[]> | Promise<ScanningDetailResult[]> | ScanningDetailResult[])}
|
||||
*
|
||||
* @memberOf ScanningResultService
|
||||
*/
|
||||
getScanningResults(tagId: string): Observable<ScanningDetailResult[]> | Promise<ScanningDetailResult[]> | ScanningDetailResult[] {
|
||||
...
|
||||
}
|
||||
}
|
||||
|
||||
...
|
||||
HarborLibraryModule.forRoot({
|
||||
scanningService: { provide: ScanningResultService, useClass: MyScanningResultService }
|
||||
})
|
||||
...
|
||||
|
||||
```
|
@ -203,6 +203,7 @@ export class CreateEditEndpointComponent implements AfterViewChecked {
|
||||
let payload: Endpoint = this.initEndpoint;
|
||||
if(this.targetNameHasChanged) {
|
||||
payload.name = this.target.name;
|
||||
delete payload.endpoint;
|
||||
}
|
||||
if (this.endpointHasChanged) {
|
||||
payload.endpoint = this.target.endpoint;
|
||||
|
@ -16,10 +16,10 @@ export const ENDPOINT_TEMPLATE: string = `
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<clr-datagrid>
|
||||
<clr-dg-column>{{'DESTINATION.NAME' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'DESTINATION.URL' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'DESTINATION.CREATION_TIME' | translate}}</clr-dg-column>
|
||||
<clr-datagrid [clrDgLoading]="loading">
|
||||
<clr-dg-column [clrDgField]="'name'">{{'DESTINATION.NAME' | translate}}</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'endpoint'">{{'DESTINATION.URL' | translate}}</clr-dg-column>
|
||||
<clr-dg-column [clrDgSortBy]="creationTimeComparator">{{'DESTINATION.CREATION_TIME' | translate}}</clr-dg-column>
|
||||
<clr-dg-row *clrDgItems="let t of targets" [clrDgItem]='t'>
|
||||
<clr-dg-action-overflow>
|
||||
<button class="action-item" (click)="editTarget(t)">{{'DESTINATION.TITLE_EDIT' | translate}}</button>
|
||||
@ -29,7 +29,11 @@ export const ENDPOINT_TEMPLATE: string = `
|
||||
<clr-dg-cell>{{t.endpoint}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{t.creation_time | date: 'short'}}</clr-dg-cell>
|
||||
</clr-dg-row>
|
||||
<clr-dg-footer>{{ (targets ? targets.length : 0) }} {{'DESTINATION.ITEMS' | translate}}</clr-dg-footer>
|
||||
<clr-dg-footer>
|
||||
{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} {{'DESTINATION.OF' | translate}}
|
||||
{{pagination.totalItems}} {{'DESTINATION.ITEMS' | translate}}
|
||||
<clr-dg-pagination #pagination [clrDgPageSize]="15"></clr-dg-pagination>
|
||||
</clr-dg-footer>
|
||||
</clr-datagrid>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -116,8 +116,9 @@ describe('EndpointComponent (inline template)', () => {
|
||||
|
||||
it('should open create endpoint modal', async(() => {
|
||||
fixture.detectChanges();
|
||||
comp.editTarget(mockOne);
|
||||
fixture.whenStable().then(()=>{
|
||||
fixture.whenStable().then(()=>{
|
||||
fixture.detectChanges();
|
||||
comp.editTarget(mockOne);
|
||||
fixture.detectChanges();
|
||||
expect(comp.target.name).toEqual('target_01');
|
||||
});
|
||||
@ -125,7 +126,6 @@ describe('EndpointComponent (inline template)', () => {
|
||||
|
||||
it('should filter endpoints by keyword', async(() => {
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(()=>{
|
||||
fixture.detectChanges();
|
||||
comp.doSearchTargets('target_02');
|
||||
|
@ -32,7 +32,9 @@ import { CreateEditEndpointComponent } from '../create-edit-endpoint/create-edit
|
||||
import { ENDPOINT_STYLE } from './endpoint.component.css';
|
||||
import { ENDPOINT_TEMPLATE } from './endpoint.component.html';
|
||||
|
||||
import { toPromise } from '../utils';
|
||||
import { toPromise, CustomComparator } from '../utils';
|
||||
|
||||
import { State, Comparator } from 'clarity-angular';
|
||||
|
||||
@Component({
|
||||
selector: 'hbr-endpoint',
|
||||
@ -55,6 +57,10 @@ export class EndpointComponent implements OnInit {
|
||||
targetName: string;
|
||||
subscription: Subscription;
|
||||
|
||||
loading: boolean = false;
|
||||
|
||||
creationTimeComparator: Comparator<Endpoint> = new CustomComparator<Endpoint>('creation_time', 'date');
|
||||
|
||||
get initEndpoint(): Endpoint {
|
||||
return {
|
||||
endpoint: "",
|
||||
@ -101,7 +107,7 @@ export class EndpointComponent implements OnInit {
|
||||
|
||||
ngOnInit(): void {
|
||||
this.targetName = '';
|
||||
this.retrieve('');
|
||||
this.retrieve();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
@ -110,29 +116,34 @@ export class EndpointComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
retrieve(targetName: string): void {
|
||||
retrieve(): void {
|
||||
this.loading = true;
|
||||
toPromise<Endpoint[]>(this.endpointService
|
||||
.getEndpoints(targetName))
|
||||
.getEndpoints(this.targetName))
|
||||
.then(
|
||||
targets => {
|
||||
this.targets = targets || [];
|
||||
let hnd = setInterval(()=>this.ref.markForCheck(), 100);
|
||||
setTimeout(()=>clearInterval(hnd), 1000);
|
||||
}).catch(error => this.errorHandler.error(error));
|
||||
this.loading = false;
|
||||
}).catch(error => {
|
||||
this.errorHandler.error(error);
|
||||
this.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
doSearchTargets(targetName: string) {
|
||||
this.targetName = targetName;
|
||||
this.retrieve(targetName);
|
||||
this.retrieve();
|
||||
}
|
||||
|
||||
refreshTargets() {
|
||||
this.retrieve('');
|
||||
this.retrieve();
|
||||
}
|
||||
|
||||
reload($event: any) {
|
||||
this.targetName = '';
|
||||
this.retrieve('');
|
||||
this.retrieve();
|
||||
}
|
||||
|
||||
openModal() {
|
||||
|
@ -290,6 +290,7 @@ export const EN_US_LANG: any = {
|
||||
"INVALID_NAME": "Invalid endpoint name.",
|
||||
"FAILED_TO_GET_TARGET": "Failed to get endpoint.",
|
||||
"CREATION_TIME": "Creation Time",
|
||||
"OF": "of",
|
||||
"ITEMS": "item(s)",
|
||||
"CREATED_SUCCESS": "Created endpoint successfully.",
|
||||
"UPDATED_SUCCESS": "Updated endpoint successfully.",
|
||||
@ -299,8 +300,7 @@ export const EN_US_LANG: any = {
|
||||
"FAILED_TO_DELETE_TARGET_IN_USED": "Failed to delete the endpoint in use."
|
||||
},
|
||||
"REPOSITORY": {
|
||||
"COPY_ID": "Copy ID",
|
||||
"COPY_PARENT_ID": "Copy Parent ID",
|
||||
"COPY_DIGEST_ID": "Copy Digest ID",
|
||||
"DELETE": "Delete",
|
||||
"NAME": "Name",
|
||||
"TAGS_COUNT": "Tags",
|
||||
@ -324,6 +324,7 @@ export const EN_US_LANG: any = {
|
||||
"OS": "OS",
|
||||
"SHOW_DETAILS": "Show Details",
|
||||
"REPOSITORIES": "Repositories",
|
||||
"OF": "of",
|
||||
"ITEMS": "item(s)",
|
||||
"POP_REPOS": "Popular Repositories",
|
||||
"DELETED_REPO_SUCCESS": "Deleted repository successfully.",
|
||||
|
@ -290,6 +290,7 @@ export const ZH_CN_LANG: any = {
|
||||
"INVALID_NAME": "无效的目标名称。",
|
||||
"FAILED_TO_GET_TARGET": "获取目标失败。",
|
||||
"CREATION_TIME": "创建时间",
|
||||
"OF": "共计",
|
||||
"ITEMS": "条记录",
|
||||
"CREATED_SUCCESS": "成功创建目标。",
|
||||
"UPDATED_SUCCESS": "成功更新目标。",
|
||||
@ -299,8 +300,7 @@ export const ZH_CN_LANG: any = {
|
||||
"FAILED_TO_DELETE_TARGET_IN_USED": "无法删除正在使用的目标。"
|
||||
},
|
||||
"REPOSITORY": {
|
||||
"COPY_ID": "复制ID",
|
||||
"COPY_PARENT_ID": "复制父级ID",
|
||||
"COPY_DIGEST_ID": "复制摘要ID",
|
||||
"DELETE": "删除",
|
||||
"NAME": "名称",
|
||||
"TAGS_COUNT": "标签数",
|
||||
@ -324,6 +324,7 @@ export const ZH_CN_LANG: any = {
|
||||
"OS": "操作系统",
|
||||
"SHOW_DETAILS": "显示详细",
|
||||
"REPOSITORIES": "镜像仓库",
|
||||
"OF": "共计",
|
||||
"ITEMS": "条记录",
|
||||
"POP_REPOS": "受欢迎的镜像仓库",
|
||||
"DELETED_REPO_SUCCESS": "成功删除镜像仓库。",
|
||||
|
@ -1,13 +1,13 @@
|
||||
export const LIST_REPLICATION_RULE_TEMPLATE: string = `
|
||||
<confirmation-dialog #toggleConfirmDialog (confirmAction)="toggleConfirm($event)"></confirmation-dialog>
|
||||
<confirmation-dialog #deletionConfirmDialog (confirmAction)="deletionConfirm($event)"></confirmation-dialog>
|
||||
<clr-datagrid>
|
||||
<clr-dg-column>{{'REPLICATION.NAME' | translate}}</clr-dg-column>
|
||||
<clr-dg-column *ngIf="projectless">{{'REPLICATION.PROJECT' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'REPLICATION.DESCRIPTION' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'REPLICATION.DESTINATION_NAME' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'REPLICATION.LAST_START_TIME' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'REPLICATION.ACTIVATION' | translate}}</clr-dg-column>
|
||||
<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]="'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-action-overflow>
|
||||
<button class="action-item" (click)="editRule(p)">{{'REPLICATION.EDIT_POLICY' | translate}}</button>
|
||||
@ -27,7 +27,7 @@ export const LIST_REPLICATION_RULE_TEMPLATE: string = `
|
||||
<clr-dg-cell>{{p.target_name}}</clr-dg-cell>
|
||||
<clr-dg-cell>
|
||||
<ng-template [ngIf]="p.start_time === nullTime">-</ng-template>
|
||||
<ng-template [ngIf]="p.start_time !== nullTime">{{p.start_time}}</ng-template>
|
||||
<ng-template [ngIf]="p.start_time !== nullTime">{{p.start_time | date: 'short'}}</ng-template>
|
||||
</clr-dg-cell>
|
||||
<clr-dg-cell>
|
||||
{{ (p.enabled === 1 ? 'REPLICATION.ENABLED' : 'REPLICATION.DISABLED') | translate}}
|
||||
|
@ -25,9 +25,9 @@ import { ConfirmationState, ConfirmationTargets, ConfirmationButtons } from '../
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { ErrorHandler } from '../error-handler/error-handler';
|
||||
import { toPromise } from '../utils';
|
||||
import { toPromise, CustomComparator } from '../utils';
|
||||
|
||||
import { State } from 'clarity-angular';
|
||||
import { State, Comparator } from 'clarity-angular';
|
||||
|
||||
import { LIST_REPLICATION_RULE_TEMPLATE } from './list-replication-rule.component.html';
|
||||
|
||||
@ -44,6 +44,8 @@ export class ListReplicationRuleComponent {
|
||||
@Input() projectless: boolean;
|
||||
@Input() selectedId: number | string;
|
||||
|
||||
@Input() loading: boolean = false;
|
||||
|
||||
@Output() reload = new EventEmitter<boolean>();
|
||||
@Output() selectOne = new EventEmitter<ReplicationRule>();
|
||||
@Output() editOne = new EventEmitter<ReplicationRule>();
|
||||
@ -55,11 +57,14 @@ export class ListReplicationRuleComponent {
|
||||
@ViewChild('deletionConfirmDialog')
|
||||
deletionConfirmDialog: ConfirmationDialogComponent;
|
||||
|
||||
startTimeComparator: Comparator<ReplicationRule> = new CustomComparator<ReplicationRule>('start_time', 'date');
|
||||
enabledComparator: Comparator<ReplicationRule> = new CustomComparator<ReplicationRule>('enabled', 'number');
|
||||
|
||||
constructor(
|
||||
private replicationService: ReplicationService,
|
||||
private translateService: TranslateService,
|
||||
private errorHandler: ErrorHandler,
|
||||
private ref: ChangeDetectorRef) {
|
||||
private ref: ChangeDetectorRef) {
|
||||
setInterval(()=>ref.markForCheck(), 500);
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
export const LIST_REPOSITORY_TEMPLATE = `
|
||||
<clr-datagrid (clrDgRefresh)="refresh($event)">
|
||||
<clr-dg-column>{{'REPOSITORY.NAME' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'REPOSITORY.TAGS_COUNT' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'REPOSITORY.PULL_COUNT' | translate}}</clr-dg-column>
|
||||
<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" [clrDgItem]='r'>
|
||||
<clr-dg-action-overflow [hidden]="!hasProjectAdminRole">
|
||||
<button class="action-item" (click)="deleteRepo(r.name)">{{'REPOSITORY.DELETE' | translate}}</button>
|
||||
@ -12,7 +12,8 @@ export const LIST_REPOSITORY_TEMPLATE = `
|
||||
<clr-dg-cell>{{r.pull_count}}</clr-dg-cell>
|
||||
</clr-dg-row>
|
||||
<clr-dg-footer>
|
||||
{{(repositories ? repositories.length : 0)}} {{'REPOSITORY.ITEMS' | translate}}
|
||||
<clr-dg-pagination [clrDgPageSize]="15"></clr-dg-pagination>
|
||||
{{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>`;
|
@ -1,10 +1,12 @@
|
||||
import { Component, Input, Output, EventEmitter, ChangeDetectorRef, ChangeDetectionStrategy } from '@angular/core';
|
||||
|
||||
import { State } from 'clarity-angular';
|
||||
import { State, Comparator } from 'clarity-angular';
|
||||
|
||||
import { Repository } from '../service/interface';
|
||||
import { LIST_REPOSITORY_TEMPLATE } from './list-repository.component.html';
|
||||
|
||||
import { CustomComparator } from '../utils';
|
||||
|
||||
@Component({
|
||||
selector: 'hbr-list-repository',
|
||||
template: LIST_REPOSITORY_TEMPLATE,
|
||||
@ -21,6 +23,10 @@ export class ListRepositoryComponent {
|
||||
|
||||
pageOffset: number = 1;
|
||||
|
||||
pullCountComparator: Comparator<Repository> = new CustomComparator<Repository>('pull_count', 'number');
|
||||
|
||||
tagsCountComparator: Comparator<Repository> = new CustomComparator<Repository>('tags_count', 'number');
|
||||
|
||||
constructor(
|
||||
private ref: ChangeDetectorRef) {
|
||||
let hnd = setInterval(()=>ref.markForCheck(), 100);
|
||||
|
@ -19,9 +19,11 @@ import {
|
||||
} from '../service/index';
|
||||
import { ErrorHandler } from '../error-handler/index';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { toPromise } from '../utils';
|
||||
import { toPromise, CustomComparator } from '../utils';
|
||||
import { LOG_TEMPLATE, LOG_STYLES } from './recent-log.template';
|
||||
|
||||
import { Comparator } from 'clarity-angular';
|
||||
|
||||
@Component({
|
||||
selector: 'hbr-log',
|
||||
styles: [LOG_STYLES],
|
||||
@ -35,6 +37,10 @@ export class RecentLogComponent implements OnInit {
|
||||
lines: number = 10; //Support 10, 25 and 50
|
||||
currentTerm: string;
|
||||
|
||||
loading: boolean;
|
||||
|
||||
opTimeComparator: Comparator<AccessLog> = new CustomComparator<AccessLog>('op_time', 'date');
|
||||
|
||||
constructor(
|
||||
private logService: AccessLogService,
|
||||
private errorHandler: ErrorHandler) { }
|
||||
@ -81,14 +87,17 @@ export class RecentLogComponent implements OnInit {
|
||||
}
|
||||
|
||||
this.onGoing = true;
|
||||
this.loading = true;
|
||||
toPromise<AccessLog[]>(this.logService.getRecentLogs(this.lines))
|
||||
.then(response => {
|
||||
this.onGoing = false;
|
||||
this.loading = false;
|
||||
this.logsCache = response; //Keep the data
|
||||
this.recentLogs = this.logsCache.filter(log => log.username != "");//To display
|
||||
})
|
||||
.catch(error => {
|
||||
this.onGoing = false;
|
||||
this.loading = false;
|
||||
this.errorHandler.error(error);
|
||||
});
|
||||
}
|
||||
|
@ -24,18 +24,18 @@ export const LOG_TEMPLATE: string = `
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<clr-datagrid>
|
||||
<clr-dg-column>{{'AUDIT_LOG.USERNAME' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'AUDIT_LOG.REPOSITORY_NAME' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'AUDIT_LOG.TAGS' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'AUDIT_LOG.OPERATION' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'AUDIT_LOG.TIMESTAMP' | translate}}</clr-dg-column>
|
||||
<clr-datagrid [clrDgLoading]="loading">
|
||||
<clr-dg-column [clrDgField]="'username'">{{'AUDIT_LOG.USERNAME' | translate}}</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'repo_name'">{{'AUDIT_LOG.REPOSITORY_NAME' | translate}}</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'repo_tag'">{{'AUDIT_LOG.TAGS' | translate}}</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'operation'">{{'AUDIT_LOG.OPERATION' | translate}}</clr-dg-column>
|
||||
<clr-dg-column [clrDgSortBy]="opTimeComparator">{{'AUDIT_LOG.TIMESTAMP' | translate}}</clr-dg-column>
|
||||
<clr-dg-row *clrDgItems="let l of recentLogs">
|
||||
<clr-dg-cell>{{l.username}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{l.repo_name}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{l.repo_tag}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{l.operation}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{l.op_time}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{l.op_time | date: 'short'}}</clr-dg-cell>
|
||||
</clr-dg-row>
|
||||
<clr-dg-footer>{{ (recentLogs ? recentLogs.length : 0) }} {{'AUDIT_LOG.ITEMS' | translate}}</clr-dg-footer>
|
||||
</clr-datagrid>
|
||||
|
@ -20,7 +20,7 @@ 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)"></list-replication-rule>
|
||||
<list-replication-rule [rules]="changedRules" [projectless]="false" [selectedId]="initSelectedId" (selectOne)="selectOneRule($event)" (editOne)="openEditRule($event)" (reload)="reloadRules($event)" [loading]="loading"></list-replication-rule>
|
||||
</div>
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<div class="row flex-items-xs-between">
|
||||
@ -46,19 +46,19 @@ export const REPLICATION_TEMPLATE: string = `
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<clr-datagrid>
|
||||
<clr-dg-column>{{'REPLICATION.NAME' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'REPLICATION.STATUS' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'REPLICATION.OPERATION' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'REPLICATION.CREATION_TIME' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'REPLICATION.END_TIME' | translate}}</clr-dg-column>
|
||||
<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>
|
||||
<clr-dg-column [clrDgField]="'operation'">{{'REPLICATION.OPERATION' | translate}}</clr-dg-column>
|
||||
<clr-dg-column [clrDgSortBy]="creationTimeComparator">{{'REPLICATION.CREATION_TIME' | translate}}</clr-dg-column>
|
||||
<clr-dg-column [clrDgSortBy]="updateTimeComparator">{{'REPLICATION.END_TIME' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'REPLICATION.LOGS' | translate}}</clr-dg-column>
|
||||
<clr-dg-row *clrDgItems="let j of jobs" [clrDgItem]='j'>
|
||||
<clr-dg-cell>{{j.repository}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{j.status}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{j.operation}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{j.creation_time}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{j.update_time}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{j.creation_time | date: 'short'}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{j.update_time | date: 'short'}}</clr-dg-cell>
|
||||
<clr-dg-cell>
|
||||
<a href="/api/jobs/replication/{{j.id}}/log" target="_BLANK">
|
||||
<clr-icon shape="clipboard"></clr-icon>
|
||||
|
@ -24,7 +24,9 @@ import { ReplicationService } from '../service/replication.service';
|
||||
import { RequestQueryParams } from '../service/RequestQueryParams';
|
||||
import { ReplicationRule, ReplicationJob, Endpoint } from '../service/interface';
|
||||
|
||||
import { toPromise } from '../utils';
|
||||
import { toPromise, CustomComparator } from '../utils';
|
||||
|
||||
import { Comparator } from 'clarity-angular';
|
||||
|
||||
import { REPLICATION_TEMPLATE } from './replication.component.html';
|
||||
import { REPLICATION_STYLE } from './replication.component.css';
|
||||
@ -81,8 +83,10 @@ export class ReplicationComponent implements OnInit {
|
||||
changedRules: ReplicationRule[];
|
||||
initSelectedId: number | string;
|
||||
|
||||
rules: ReplicationRule[];
|
||||
jobs: ReplicationJob[];
|
||||
rules: ReplicationRule[];
|
||||
loading: boolean;
|
||||
|
||||
jobs: ReplicationJob[];
|
||||
|
||||
jobsTotalRecordCount: number;
|
||||
jobsTotalPage: number;
|
||||
@ -93,23 +97,28 @@ export class ReplicationComponent implements OnInit {
|
||||
@ViewChild(CreateEditRuleComponent)
|
||||
createEditPolicyComponent: CreateEditRuleComponent;
|
||||
|
||||
creationTimeComparator: Comparator<ReplicationJob> = new CustomComparator<ReplicationJob>('creation_time', 'date');
|
||||
updateTimeComparator: Comparator<ReplicationJob> = new CustomComparator<ReplicationJob>('update_time', 'date');
|
||||
|
||||
constructor(
|
||||
private errorHandler: ErrorHandler,
|
||||
private replicationService: ReplicationService,
|
||||
private translateService: TranslateService) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
ngOnInit() {
|
||||
if(!this.projectId) {
|
||||
this.errorHandler.warning('Project ID is unset.');
|
||||
}
|
||||
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=>{
|
||||
@ -122,8 +131,12 @@ export class ReplicationComponent implements OnInit {
|
||||
this.search.ruleId = this.changedRules[0].id || '';
|
||||
this.fetchReplicationJobs();
|
||||
}
|
||||
this.loading = false;
|
||||
}
|
||||
).catch(error=>this.errorHandler.error(error));
|
||||
).catch(error=>{
|
||||
this.errorHandler.error(error);
|
||||
this.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
openModal(): void {
|
||||
@ -147,13 +160,15 @@ export class ReplicationComponent implements OnInit {
|
||||
params.set('repository', this.search.repoName);
|
||||
params.set('start_time', this.search.startTimestamp);
|
||||
params.set('end_time', this.search.endTimestamp);
|
||||
|
||||
|
||||
toPromise<ReplicationJob[]>(this.replicationService
|
||||
.getJobs(this.search.ruleId, params))
|
||||
.then(
|
||||
response=>{
|
||||
this.jobs = response;
|
||||
}).catch(error=>this.errorHandler.error(error));
|
||||
}).catch(error=>{
|
||||
this.errorHandler.error(error);
|
||||
});
|
||||
}
|
||||
|
||||
selectOneRule(rule: ReplicationRule) {
|
||||
|
@ -11,30 +11,6 @@ export interface Base {
|
||||
update_time?: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for tag history
|
||||
*
|
||||
* @export
|
||||
* @interface TagCompatibility
|
||||
*/
|
||||
export interface TagCompatibility {
|
||||
v1Compatibility: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for tag manifest
|
||||
*
|
||||
* @export
|
||||
* @interface TagManifest
|
||||
*/
|
||||
export interface TagManifest {
|
||||
schemaVersion: number;
|
||||
name: string;
|
||||
tag: string;
|
||||
architecture: string;
|
||||
history: TagCompatibility[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for Repository
|
||||
*
|
||||
@ -59,10 +35,16 @@ export interface Repository extends Base {
|
||||
* @interface Tag
|
||||
* @extends {Base}
|
||||
*/
|
||||
|
||||
export interface Tag extends Base {
|
||||
tag: string;
|
||||
manifest: TagManifest;
|
||||
signed?: number; //May NOT exist
|
||||
digest: string;
|
||||
name: string;
|
||||
architecture: string;
|
||||
os: string;
|
||||
docker_version: string;
|
||||
author: string;
|
||||
created: Date;
|
||||
signature?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -87,7 +87,7 @@ export class RepositoryDefaultService extends RepositoryService {
|
||||
return Promise.reject('Bad argument');
|
||||
}
|
||||
let url: string = this.config.repositoryBaseEndpoint ? this.config.repositoryBaseEndpoint : '/api/repositories';
|
||||
url = `${url}/${repositoryName}/tags`;
|
||||
url = `${url}/${repositoryName}`;
|
||||
|
||||
return this.http.delete(url, HTTP_JSON_OPTIONS).toPromise()
|
||||
.then(response => response)
|
||||
|
@ -4,41 +4,24 @@ import { TagService, TagDefaultService } from './tag.service';
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
|
||||
|
||||
import { Tag, TagCompatibility, TagManifest } from './interface';
|
||||
import { Tag } from './interface';
|
||||
|
||||
import { VerifiedSignature } from './tag.service';
|
||||
import { toPromise } from '../utils';
|
||||
|
||||
describe('TagService', () => {
|
||||
let mockComp: TagCompatibility[] = [{
|
||||
v1Compatibility: '{"architecture":"amd64","author":"NGINX Docker Maintainers \\"docker-maint@nginx.com\\"","config":{"Hostname":"6b3797ab1e90","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"ExposedPorts":{"443/tcp":{},"80/tcp":{}},"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","NGINX_VERSION=1.11.5-1~jessie"],"Cmd":["nginx","-g","daemon off;"],"ArgsEscaped":true,"Image":"sha256:47a33f0928217b307cf9f20920a0c6445b34ae974a60c1b4fe73b809379ad928","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":[],"Labels":{}},"container":"f1883a3fb44b0756a2a3b1e990736a44b1387183125351370042ce7bd9ffc338","container_config":{"Hostname":"6b3797ab1e90","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"ExposedPorts":{"443/tcp":{},"80/tcp":{}},"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","NGINX_VERSION=1.11.5-1~jessie"],"Cmd":["/bin/sh","-c","#(nop) ","CMD [\\"nginx\\" \\"-g\\" \\"daemon off;\\"]"],"ArgsEscaped":true,"Image":"sha256:47a33f0928217b307cf9f20920a0c6445b34ae974a60c1b4fe73b809379ad928","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":[],"Labels":{}},"created":"2016-11-08T22:41:15.912313785Z","docker_version":"1.12.3","id":"db3700426e6d7c1402667f42917109b2467dd49daa85d38ac99854449edc20b3","os":"linux","parent":"f3ef5f96caf99a18c6821487102c136b00e0275b1da0c7558d7090351f9d447e","throwaway":true}'
|
||||
}];
|
||||
let mockManifest: TagManifest = {
|
||||
schemaVersion: 1,
|
||||
name: 'library/nginx',
|
||||
tag: '1.11.5',
|
||||
architecture: 'amd64',
|
||||
history: mockComp
|
||||
};
|
||||
|
||||
let mockTags: Tag[] = [{
|
||||
tag: '1.11.5',
|
||||
manifest: mockManifest
|
||||
}];
|
||||
|
||||
let mockSignatures: VerifiedSignature[] = [{
|
||||
tag: '1.11.5',
|
||||
hashes: {
|
||||
sha256: 'fake'
|
||||
let mockTags: 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 mockSignatures2: VerifiedSignature[] = [{
|
||||
tag: '1.11.15',
|
||||
hashes: {
|
||||
sha256: 'fake2'
|
||||
}
|
||||
}];
|
||||
];
|
||||
|
||||
const mockConfig: IServiceConfig = {
|
||||
repositoryBaseEndpoint: "/api/repositories/testing"
|
||||
@ -65,50 +48,4 @@ describe('TagService', () => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
|
||||
it('should get tags with signed status[1] if signatures exists', async(inject([TagDefaultService], (service: TagService) => {
|
||||
expect(service).toBeTruthy();
|
||||
let spy1: jasmine.Spy = spyOn(service, '_getTags')
|
||||
.and.returnValue(Promise.resolve(mockTags));
|
||||
let spy2: jasmine.Spy = spyOn(service, '_getSignatures')
|
||||
.and.returnValue(Promise.resolve(mockSignatures));
|
||||
|
||||
toPromise<Tag[]>(service.getTags('library/nginx'))
|
||||
.then(tags => {
|
||||
expect(tags).toBeTruthy();
|
||||
expect(tags.length).toBe(1);
|
||||
expect(tags[0].signed).toBe(1);
|
||||
});
|
||||
})));
|
||||
|
||||
it('should get tags with not-signed status[0] if signatures exists', async(inject([TagDefaultService], (service: TagService) => {
|
||||
expect(service).toBeTruthy();
|
||||
let spy1: jasmine.Spy = spyOn(service, '_getTags')
|
||||
.and.returnValue(Promise.resolve(mockTags));
|
||||
let spy2: jasmine.Spy = spyOn(service, '_getSignatures')
|
||||
.and.returnValue(Promise.resolve(mockSignatures2));
|
||||
|
||||
toPromise<Tag[]>(service.getTags('library/nginx'))
|
||||
.then(tags => {
|
||||
expect(tags).toBeTruthy();
|
||||
expect(tags.length).toBe(1);
|
||||
expect(tags[0].signed).toBe(0);
|
||||
});
|
||||
})));
|
||||
|
||||
it('should get tags with default signed status[-1] if signatures not exist', async(inject([TagDefaultService], (service: TagService) => {
|
||||
expect(service).toBeTruthy();
|
||||
let spy1: jasmine.Spy = spyOn(service, '_getTags')
|
||||
.and.returnValue(Promise.resolve(mockTags));
|
||||
let spy2: jasmine.Spy = spyOn(service, '_getSignatures')
|
||||
.and.returnValue(Promise.reject("Error"));
|
||||
|
||||
toPromise<Tag[]>(service.getTags('library/nginx'))
|
||||
.then(tags => {
|
||||
expect(tags).toBeTruthy();
|
||||
expect(tags.length).toBe(1);
|
||||
expect(tags[0].signed).toBe(-1);
|
||||
});
|
||||
})));
|
||||
|
||||
|
||||
});
|
||||
|
@ -100,27 +100,7 @@ export class TagDefaultService extends TagService {
|
||||
if (!repositoryName) {
|
||||
return Promise.reject("Bad argument");
|
||||
}
|
||||
|
||||
return this._getTags(repositoryName, queryParams)
|
||||
.then(tags => {
|
||||
return this._getSignatures(repositoryName)
|
||||
.then(signatures => {
|
||||
tags.forEach(tag => {
|
||||
let foundOne: VerifiedSignature | undefined = signatures.find(signature => signature.tag === tag.tag);
|
||||
if (foundOne) {
|
||||
tag.signed = 1;//Signed
|
||||
} else {
|
||||
tag.signed = 0;//Not signed
|
||||
}
|
||||
});
|
||||
return tags;
|
||||
})
|
||||
.catch(error => {
|
||||
tags.forEach(tag => tag.signed = -1);//No signature info
|
||||
return tags;
|
||||
})
|
||||
})
|
||||
.catch(error => Promise.reject(error))
|
||||
return this._getTags(repositoryName, queryParams);
|
||||
}
|
||||
|
||||
public deleteTag(repositoryName: string, tag: string): Observable<any> | Promise<Tag> | any {
|
||||
|
@ -4,7 +4,7 @@ export const TAG_TEMPLATE = `
|
||||
<h3 class="modal-title">{{ manifestInfoTitle | translate }}</h3>
|
||||
<div class="modal-body">
|
||||
<div class="row col-md-12">
|
||||
<textarea rows="3" (click)="selectAndCopy($event)">{{tagID}}</textarea>
|
||||
<textarea rows="3" (click)="selectAndCopy($event)">{{digestId}}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
@ -12,36 +12,39 @@ export const TAG_TEMPLATE = `
|
||||
</div>
|
||||
</clr-modal>
|
||||
<h2 class="sub-header-title">{{repoName}}</h2>
|
||||
<clr-datagrid>
|
||||
<clr-dg-column>{{'REPOSITORY.TAG' | translate}}</clr-dg-column>
|
||||
<clr-datagrid [clrDgLoading]="loading">
|
||||
<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>
|
||||
<clr-dg-column>{{'REPOSITORY.AUTHOR' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'REPOSITORY.CREATED' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'REPOSITORY.DOCKER_VERSION' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'REPOSITORY.ARCHITECTURE' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'REPOSITORY.OS' | translate}}</clr-dg-column>
|
||||
<clr-dg-column [clrDgSortBy]="createdComparator">{{'REPOSITORY.CREATED' | translate}}</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'docker_version'">{{'REPOSITORY.DOCKER_VERSION' | translate}}</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'architecture'">{{'REPOSITORY.ARCHITECTURE' | translate}}</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'os'">{{'REPOSITORY.OS' | translate}}</clr-dg-column>
|
||||
<clr-dg-row *clrDgItems="let t of tags" [clrDgItem]='t'>
|
||||
<clr-dg-action-overflow>
|
||||
<button class="action-item" (click)="showTagID('tag', t)">{{'REPOSITORY.COPY_ID' | translate}}</button>
|
||||
<button class="action-item" (click)="showTagID('parent', t)">{{'REPOSITORY.COPY_PARENT_ID' | translate}}</button>
|
||||
<button class="action-item" (click)="showDigestId(t)">{{'REPOSITORY.COPY_DIGEST_ID' | translate}}</button>
|
||||
<button class="action-item" [hidden]="!hasProjectAdminRole" (click)="deleteTag(t)">{{'REPOSITORY.DELETE' | translate}}</button>
|
||||
</clr-dg-action-overflow>
|
||||
<clr-dg-cell>{{t.tag}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{t.pullCommand}}</clr-dg-cell>
|
||||
<clr-dg-cell *ngIf="withNotary" [ngSwitch]="t.signed">
|
||||
<clr-icon shape="check" *ngSwitchCase="1" style="color: #1D5100;"></clr-icon>
|
||||
<clr-icon shape="close" *ngSwitchCase="0" style="color: #C92100;"></clr-icon>
|
||||
<clr-dg-cell>{{t.name}}</clr-dg-cell>
|
||||
<clr-dg-cell>docker pull {{registryUrl}}/{{repoName}}:{{t.name}}</clr-dg-cell>
|
||||
<clr-dg-cell *ngIf="withNotary" [ngSwitch]="t.signature !== null">
|
||||
<clr-icon shape="check" *ngSwitchCase="true" style="color: #1D5100;"></clr-icon>
|
||||
<clr-icon shape="close" *ngSwitchCase="false" style="color: #C92100;"></clr-icon>
|
||||
<a href="javascript:void(0)" *ngSwitchDefault role="tooltip" aria-haspopup="true" class="tooltip tooltip-top-right">
|
||||
<clr-icon shape="help" style="color: #565656;" size="16"></clr-icon>
|
||||
<span class="tooltip-content">{{'REPOSITORY.NOTARY_IS_UNDETERMINED' | translate}}</span>
|
||||
</a>
|
||||
</clr-dg-cell>
|
||||
<clr-dg-cell>{{t.author}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{t.created}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{t.dockerVersion}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{t.created | date: 'short'}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{t.docker_version}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{t.architecture}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{t.os}}</clr-dg-cell>
|
||||
</clr-dg-row>
|
||||
<clr-dg-footer>{{tags ? tags.length : 0}} {{'REPOSITORY.ITEMS' | translate}}</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>
|
||||
</clr-dg-footer>
|
||||
</clr-datagrid>`;
|
@ -8,7 +8,7 @@ import { ConfirmationDialogComponent } from '../confirmation-dialog/confirmation
|
||||
import { TagComponent } from './tag.component';
|
||||
|
||||
import { ErrorHandler } from '../error-handler/error-handler';
|
||||
import { Tag, TagCompatibility, TagManifest, TagView } from '../service/interface';
|
||||
import { Tag } from '../service/interface';
|
||||
import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
|
||||
import { TagService, TagDefaultService } from '../service/tag.service';
|
||||
|
||||
@ -19,21 +19,18 @@ describe('TagComponent (inline template)', ()=> {
|
||||
let tagService: TagService;
|
||||
let spy: jasmine.Spy;
|
||||
|
||||
let mockComp: TagCompatibility[] = [{
|
||||
v1Compatibility: '{"architecture":"amd64","author":"NGINX Docker Maintainers \\"docker-maint@nginx.com\\"","config":{"Hostname":"6b3797ab1e90","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"ExposedPorts":{"443/tcp":{},"80/tcp":{}},"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","NGINX_VERSION=1.11.5-1~jessie"],"Cmd":["nginx","-g","daemon off;"],"ArgsEscaped":true,"Image":"sha256:47a33f0928217b307cf9f20920a0c6445b34ae974a60c1b4fe73b809379ad928","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":[],"Labels":{}},"container":"f1883a3fb44b0756a2a3b1e990736a44b1387183125351370042ce7bd9ffc338","container_config":{"Hostname":"6b3797ab1e90","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"ExposedPorts":{"443/tcp":{},"80/tcp":{}},"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","NGINX_VERSION=1.11.5-1~jessie"],"Cmd":["/bin/sh","-c","#(nop) ","CMD [\\"nginx\\" \\"-g\\" \\"daemon off;\\"]"],"ArgsEscaped":true,"Image":"sha256:47a33f0928217b307cf9f20920a0c6445b34ae974a60c1b4fe73b809379ad928","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":[],"Labels":{}},"created":"2016-11-08T22:41:15.912313785Z","docker_version":"1.12.3","id":"db3700426e6d7c1402667f42917109b2467dd49daa85d38ac99854449edc20b3","os":"linux","parent":"f3ef5f96caf99a18c6821487102c136b00e0275b1da0c7558d7090351f9d447e","throwaway":true}'
|
||||
}];
|
||||
let mockManifest: TagManifest = {
|
||||
schemaVersion: 1,
|
||||
name: 'library/nginx',
|
||||
tag: '1.11.5',
|
||||
architecture: 'amd64',
|
||||
history: mockComp
|
||||
};
|
||||
|
||||
let mockTags: Tag[] = [{
|
||||
tag: '1.11.5',
|
||||
manifest: mockManifest
|
||||
}];
|
||||
let mockTags: 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/repositories/testing'
|
||||
|
@ -26,30 +26,17 @@ import { Tag, SessionInfo } from '../service/interface';
|
||||
import { TAG_TEMPLATE } from './tag.component.html';
|
||||
import { TAG_STYLE } from './tag.component.css';
|
||||
|
||||
import { toPromise } from '../utils';
|
||||
import { toPromise, CustomComparator } from '../utils';
|
||||
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
/**
|
||||
* Inteface for the tag view
|
||||
*/
|
||||
export interface TagView {
|
||||
tag: string;
|
||||
pullCommand: string;
|
||||
signed: number;
|
||||
author: string;
|
||||
created: Date;
|
||||
dockerVersion: string;
|
||||
architecture: string;
|
||||
os: string;
|
||||
id: string;
|
||||
parent: string;
|
||||
}
|
||||
import { State, Comparator } from 'clarity-angular';
|
||||
|
||||
@Component({
|
||||
selector: 'hbr-tag',
|
||||
template: TAG_TEMPLATE,
|
||||
styles: [ TAG_STYLE ]
|
||||
styles: [ TAG_STYLE ],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class TagComponent implements OnInit {
|
||||
|
||||
@ -59,7 +46,7 @@ export class TagComponent implements OnInit {
|
||||
|
||||
hasProjectAdminRole: boolean;
|
||||
|
||||
tags: TagView[];
|
||||
tags: Tag[];
|
||||
|
||||
registryUrl: string;
|
||||
withNotary: boolean;
|
||||
@ -67,28 +54,17 @@ export class TagComponent implements OnInit {
|
||||
|
||||
showTagManifestOpened: boolean;
|
||||
manifestInfoTitle: string;
|
||||
tagID: string;
|
||||
digestId: string;
|
||||
staticBackdrop: boolean = true;
|
||||
closable: boolean = false;
|
||||
|
||||
createdComparator: Comparator<Tag> = new CustomComparator<Tag>('created', 'date');
|
||||
|
||||
loading: boolean = false;
|
||||
|
||||
@ViewChild('confirmationDialog')
|
||||
confirmationDialog: ConfirmationDialogComponent;
|
||||
|
||||
get initTagView() {
|
||||
return {
|
||||
tag: '',
|
||||
pullCommand: '',
|
||||
signed: -1,
|
||||
author: '',
|
||||
created: new Date(),
|
||||
dockerVersion: '',
|
||||
architecture: '',
|
||||
os: '',
|
||||
id: '',
|
||||
parent: ''
|
||||
};
|
||||
}
|
||||
|
||||
constructor(
|
||||
private errorHandler: ErrorHandler,
|
||||
private tagService: TagService,
|
||||
@ -99,14 +75,13 @@ export class TagComponent implements OnInit {
|
||||
if (message &&
|
||||
message.source === ConfirmationTargets.TAG
|
||||
&& message.state === ConfirmationState.CONFIRMED) {
|
||||
let tag = message.data;
|
||||
let tag: Tag = message.data;
|
||||
if (tag) {
|
||||
if (tag.signed) {
|
||||
if (tag.signature) {
|
||||
return;
|
||||
} else {
|
||||
let tagName = tag.tag;
|
||||
toPromise<number>(this.tagService
|
||||
.deleteTag(this.repoName, tagName))
|
||||
.deleteTag(this.repoName, tag.name))
|
||||
.then(
|
||||
response => {
|
||||
this.retrieve();
|
||||
@ -141,49 +116,34 @@ export class TagComponent implements OnInit {
|
||||
|
||||
retrieve() {
|
||||
this.tags = [];
|
||||
this.loading = true;
|
||||
toPromise<Tag[]>(this.tagService
|
||||
.getTags(this.repoName))
|
||||
.then(items => this.listTags(items))
|
||||
.catch(error => this.errorHandler.error(error));
|
||||
}
|
||||
|
||||
listTags(tags: Tag[]): void {
|
||||
tags.forEach(t => {
|
||||
let tag = this.initTagView;
|
||||
tag.tag = t.tag;
|
||||
let data = JSON.parse(t.manifest.history[0].v1Compatibility);
|
||||
tag.architecture = data['architecture'];
|
||||
tag.author = data['author'];
|
||||
if(!t.signed && t.signed !== 0) {
|
||||
tag.signed = -1;
|
||||
} else {
|
||||
tag.signed = t.signed;
|
||||
}
|
||||
tag.created = data['created'];
|
||||
tag.dockerVersion = data['docker_version'];
|
||||
tag.pullCommand = 'docker pull ' + this.registryUrl + '/' + t.manifest.name + ':' + t.tag;
|
||||
tag.os = data['os'];
|
||||
tag.id = data['id'];
|
||||
tag.parent = data['parent'];
|
||||
this.tags.push(tag);
|
||||
});
|
||||
.then(items => {
|
||||
this.tags = items;
|
||||
this.loading = false;
|
||||
})
|
||||
.catch(error => {
|
||||
this.errorHandler.error(error);
|
||||
this.loading = false;
|
||||
});
|
||||
let hnd = setInterval(()=>this.ref.markForCheck(), 100);
|
||||
setTimeout(()=>clearInterval(hnd), 1000);
|
||||
}
|
||||
|
||||
deleteTag(tag: TagView) {
|
||||
deleteTag(tag: Tag) {
|
||||
if (tag) {
|
||||
let titleKey: string, summaryKey: string, content: string, buttons: ConfirmationButtons;
|
||||
if (tag.signed) {
|
||||
if (tag.signature) {
|
||||
titleKey = 'REPOSITORY.DELETION_TITLE_TAG_DENIED';
|
||||
summaryKey = 'REPOSITORY.DELETION_SUMMARY_TAG_DENIED';
|
||||
buttons = ConfirmationButtons.CLOSE;
|
||||
content = 'notary -s https://' + this.registryUrl + ':4443 -d ~/.docker/trust remove -p ' + this.registryUrl + '/' + this.repoName + ' ' + tag.tag;
|
||||
content = 'notary -s https://' + this.registryUrl + ':4443 -d ~/.docker/trust remove -p ' + this.registryUrl + '/' + this.repoName + ' ' + tag.name;
|
||||
} else {
|
||||
titleKey = 'REPOSITORY.DELETION_TITLE_TAG';
|
||||
summaryKey = 'REPOSITORY.DELETION_SUMMARY_TAG';
|
||||
buttons = ConfirmationButtons.DELETE_CANCEL;
|
||||
content = tag.tag;
|
||||
content = tag.name;
|
||||
}
|
||||
let message = new ConfirmationMessage(
|
||||
titleKey,
|
||||
@ -196,15 +156,10 @@ export class TagComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
showTagID(type: string, tag: TagView) {
|
||||
showDigestId(tag: Tag) {
|
||||
if(tag) {
|
||||
if(type === 'tag') {
|
||||
this.manifestInfoTitle = 'REPOSITORY.COPY_ID';
|
||||
this.tagID = tag.id;
|
||||
} else if(type === 'parent') {
|
||||
this.manifestInfoTitle = 'REPOSITORY.COPY_PARENT_ID';
|
||||
this.tagID = tag.parent;
|
||||
}
|
||||
this.manifestInfoTitle = 'REPOSITORY.COPY_DIGEST_ID';
|
||||
this.digestId = tag.digest;
|
||||
this.showTagManifestOpened = true;
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import 'rxjs/add/operator/toPromise';
|
||||
import { RequestOptions, Headers } from '@angular/http';
|
||||
import { RequestQueryParams } from './service/RequestQueryParams';
|
||||
import { DebugElement } from '@angular/core';
|
||||
import { Comparator } from 'clarity-angular';
|
||||
|
||||
/**
|
||||
* Convert the different async channels to the Promise<T> type.
|
||||
@ -85,4 +86,36 @@ export function click(el: DebugElement | HTMLElement, eventObj: any = ButtonClic
|
||||
} else {
|
||||
el.triggerEventHandler('click', eventObj);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Comparator for fields with specific type.
|
||||
*
|
||||
*/
|
||||
export class CustomComparator<T> implements Comparator<T> {
|
||||
|
||||
fieldName: string;
|
||||
type: string;
|
||||
|
||||
constructor(fieldName: string, type: string) {
|
||||
this.fieldName = fieldName;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
compare(a: {[key: string]: any| any[]}, b: {[key: string]: any| any[]}) {
|
||||
let comp = 0;
|
||||
if(a && b && a[this.fieldName] && b[this.fieldName]) {
|
||||
let fieldA = a[this.fieldName];
|
||||
let fieldB = b[this.fieldName];
|
||||
switch(this.type) {
|
||||
case "number":
|
||||
comp = fieldB - fieldA;
|
||||
break;
|
||||
case "date":
|
||||
comp = new Date(fieldB).getTime() - new Date(fieldA).getTime();
|
||||
break;
|
||||
}
|
||||
}
|
||||
return comp;
|
||||
}
|
||||
}
|
@ -16,7 +16,6 @@ import { Http, URLSearchParams, Response } from '@angular/http';
|
||||
|
||||
import { Repository } from './repository';
|
||||
import { Tag } from './tag';
|
||||
import { VerifiedSignature } from './verified-signature';
|
||||
|
||||
import { Observable } from 'rxjs/Observable'
|
||||
import 'rxjs/add/observable/of';
|
||||
@ -46,35 +45,6 @@ export class RepositoryService {
|
||||
.catch(error=>Observable.throw(error));
|
||||
}
|
||||
|
||||
listNotarySignatures(repoName: string): Observable<VerifiedSignature[]> {
|
||||
return this.http
|
||||
.get(`/api/repositories/${repoName}/signatures`)
|
||||
.map(response=>response.json())
|
||||
.catch(error=>Observable.throw(error));
|
||||
}
|
||||
|
||||
listTagsWithVerifiedSignatures(repoName: string): Observable<Tag[]> {
|
||||
return this.listTags(repoName)
|
||||
.map(res=>res)
|
||||
.flatMap(tags=>{
|
||||
return this.listNotarySignatures(repoName).map(signatures=>{
|
||||
tags.forEach(t=>{
|
||||
for(let i = 0; i < signatures.length; i++) {
|
||||
if(signatures[i].tag === t.tag) {
|
||||
t.signed = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
return tags;
|
||||
})
|
||||
.catch(error=>{
|
||||
return Observable.of(tags);
|
||||
})
|
||||
})
|
||||
.catch(error=>Observable.throw(error));
|
||||
}
|
||||
|
||||
deleteRepository(repoName: string): Observable<any> {
|
||||
return this.http
|
||||
.delete(`/api/repositories/${repoName}`)
|
||||
|
@ -6,7 +6,7 @@
|
||||
<h3 class="modal-title">{{ manifestInfoTitle | translate }}</h3>
|
||||
<div class="modal-body">
|
||||
<div class="row col-md-12">
|
||||
<textarea rows="3" (click)="selectAndCopy($event)">{{tagID}}</textarea>
|
||||
<textarea rows="3" (click)="selectAndCopy($event)">{{digestId}}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
@ -24,25 +24,20 @@
|
||||
<clr-dg-column>{{'REPOSITORY.DOCKER_VERSION' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'REPOSITORY.ARCHITECTURE' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'REPOSITORY.OS' | translate}}</clr-dg-column>
|
||||
<clr-dg-row *clrDgItems="let t of tags" [clrDgItem]='t'>
|
||||
<clr-dg-row *clrDgItems="let t of tags">
|
||||
<clr-dg-action-overflow>
|
||||
<button class="action-item" (click)="showTagID('tag', t)">{{'REPOSITORY.COPY_ID' | translate}}</button>
|
||||
<button class="action-item" (click)="showTagID('parent', t)">{{'REPOSITORY.COPY_PARENT_ID' | translate}}</button>
|
||||
<button class="action-item" (click)="showDigestId(t)">{{'REPOSITORY.COPY_DIGEST_ID' | translate}}</button>
|
||||
<button class="action-item" [hidden]="!hasProjectAdminRole" (click)="deleteTag(t)">{{'REPOSITORY.DELETE' | translate}}</button>
|
||||
</clr-dg-action-overflow>
|
||||
<clr-dg-cell>{{t.tag}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{t.pullCommand}}</clr-dg-cell>
|
||||
<clr-dg-cell *ngIf="withNotary" [ngSwitch]="t.signed">
|
||||
<clr-icon shape="check" *ngSwitchCase="1" style="color: #1D5100;"></clr-icon>
|
||||
<clr-icon shape="close" *ngSwitchCase="0" style="color: #C92100;"></clr-icon>
|
||||
<a href="javascript:void(0)" *ngSwitchDefault role="tooltip" aria-haspopup="true" class="tooltip tooltip-top-right">
|
||||
<clr-icon shape="help" style="color: #565656;" size="16"></clr-icon>
|
||||
<span class="tooltip-content">{{'REPOSITORY.NOTARY_IS_UNDETERMINED' | translate}}</span>
|
||||
</a>
|
||||
<clr-dg-cell>{{t.name}}</clr-dg-cell>
|
||||
<clr-dg-cell>docker pull {{registryUrl}}/{{repoName}}:{{t.name}}</clr-dg-cell>
|
||||
<clr-dg-cell *ngIf="withNotary">
|
||||
<clr-icon *ngIf="t.signature" shape="check" style="color: #1D5100;"></clr-icon>
|
||||
<clr-icon *ngIf="!t.signature" shape="close" style="color: #C92100;"></clr-icon>
|
||||
</clr-dg-cell>
|
||||
<clr-dg-cell>{{t.author}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{t.created | date: 'short'}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{t.dockerVersion}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{t.docker_version}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{t.architecture}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{t.os}}</clr-dg-cell>
|
||||
</clr-dg-row>
|
||||
|
@ -24,7 +24,6 @@ import { ConfirmationMessage } from '../../shared/confirmation-dialog/confirmati
|
||||
import { Subscription } from 'rxjs/Subscription';
|
||||
|
||||
import { Tag } from '../tag';
|
||||
import { TagView } from '../tag-view';
|
||||
|
||||
import { AppConfigService } from '../../app-config.service';
|
||||
|
||||
@ -45,7 +44,7 @@ export class TagRepositoryComponent implements OnInit, OnDestroy {
|
||||
|
||||
hasProjectAdminRole: boolean = false;
|
||||
|
||||
tags: TagView[];
|
||||
tags: Tag[];
|
||||
registryUrl: string;
|
||||
withNotary: boolean;
|
||||
|
||||
@ -53,7 +52,7 @@ export class TagRepositoryComponent implements OnInit, OnDestroy {
|
||||
|
||||
showTagManifestOpened: boolean;
|
||||
manifestInfoTitle: string;
|
||||
tagID: string;
|
||||
digestId: string;
|
||||
staticBackdrop: boolean = true;
|
||||
closable: boolean = false;
|
||||
|
||||
@ -79,9 +78,8 @@ export class TagRepositoryComponent implements OnInit, OnDestroy {
|
||||
if (tag.signed) {
|
||||
return;
|
||||
} else {
|
||||
let tagName = tag.tag;
|
||||
this.repositoryService
|
||||
.deleteRepoByTag(this.repoName, tagName)
|
||||
.deleteRepoByTag(this.repoName, tag.name)
|
||||
.subscribe(
|
||||
response => {
|
||||
this.retrieve();
|
||||
@ -103,10 +101,11 @@ export class TagRepositoryComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
this.projectId = this.route.snapshot.params['id'];
|
||||
this.repoName = this.route.snapshot.params['repo'];
|
||||
this.tags = [];
|
||||
|
||||
this.registryUrl = this.appConfigService.getConfig().registry_url;
|
||||
this.withNotary = this.appConfigService.getConfig().with_notary;
|
||||
this.retrieve();
|
||||
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
@ -120,63 +119,25 @@ export class TagRepositoryComponent implements OnInit, OnDestroy {
|
||||
this.repositoryService
|
||||
.listTags(this.repoName)
|
||||
.subscribe(
|
||||
items => this.listTags(items),
|
||||
tags => this.tags = tags,
|
||||
error => this.messageHandlerService.handleError(error));
|
||||
|
||||
if(this.withNotary) {
|
||||
this.repositoryService
|
||||
.listNotarySignatures(this.repoName)
|
||||
.subscribe(
|
||||
signatures => {
|
||||
this.tags.forEach((t, n)=>{
|
||||
let signed = false;
|
||||
for(let i = 0; i < signatures.length; i++) {
|
||||
if (signatures[i].tag === t.tag) {
|
||||
signed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.tags[n].signed = (signed) ? 1 : 0;
|
||||
this.ref.markForCheck();
|
||||
});
|
||||
},
|
||||
error => console.error('Cannot determine the signature of this tag.'));
|
||||
}
|
||||
}
|
||||
|
||||
listTags(tags: Tag[]): void {
|
||||
tags.forEach(t => {
|
||||
let tag = new TagView();
|
||||
tag.tag = t.tag;
|
||||
let data = JSON.parse(t.manifest.history[0].v1Compatibility);
|
||||
tag.architecture = data['architecture'];
|
||||
tag.author = data['author'];
|
||||
tag.signed = t.signed;
|
||||
tag.created = data['created'];
|
||||
tag.dockerVersion = data['docker_version'];
|
||||
tag.pullCommand = 'docker pull ' + this.registryUrl + '/' + t.manifest.name + ':' + t.tag;
|
||||
tag.os = data['os'];
|
||||
tag.id = data['id'];
|
||||
tag.parent = data['parent'];
|
||||
this.tags.push(tag);
|
||||
});
|
||||
let hnd = setInterval(()=>this.ref.markForCheck(), 100);
|
||||
setTimeout(()=>clearInterval(hnd), 1000);
|
||||
}
|
||||
|
||||
deleteTag(tag: TagView) {
|
||||
deleteTag(tag: Tag) {
|
||||
if (tag) {
|
||||
let titleKey: string, summaryKey: string, content: string, buttons: ConfirmationButtons;
|
||||
if (tag.signed) {
|
||||
if (tag.signature) {
|
||||
titleKey = 'REPOSITORY.DELETION_TITLE_TAG_DENIED';
|
||||
summaryKey = 'REPOSITORY.DELETION_SUMMARY_TAG_DENIED';
|
||||
buttons = ConfirmationButtons.CLOSE;
|
||||
content = 'notary -s https://' + this.registryUrl + ':4443 -d ~/.docker/trust remove -p ' + this.registryUrl + '/' + this.repoName + ' ' + tag.tag;
|
||||
content = 'notary -s https://' + this.registryUrl + ':4443 -d ~/.docker/trust remove -p ' + this.registryUrl + '/' + this.repoName + ' ' + tag.name;
|
||||
} else {
|
||||
titleKey = 'REPOSITORY.DELETION_TITLE_TAG';
|
||||
summaryKey = 'REPOSITORY.DELETION_SUMMARY_TAG';
|
||||
buttons = ConfirmationButtons.DELETE_CANCEL;
|
||||
content = tag.tag;
|
||||
content = tag.name;
|
||||
}
|
||||
let message = new ConfirmationMessage(
|
||||
titleKey,
|
||||
@ -189,15 +150,10 @@ export class TagRepositoryComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
showTagID(type: string, tag: TagView) {
|
||||
showDigestId(tag: Tag) {
|
||||
if(tag) {
|
||||
if(type === 'tag') {
|
||||
this.manifestInfoTitle = 'REPOSITORY.COPY_ID';
|
||||
this.tagID = tag.id;
|
||||
} else if(type === 'parent') {
|
||||
this.manifestInfoTitle = 'REPOSITORY.COPY_PARENT_ID';
|
||||
this.tagID = tag.parent;
|
||||
}
|
||||
this.manifestInfoTitle = 'REPOSITORY.COPY_DIGEST_ID';
|
||||
this.digestId = tag.digest;
|
||||
this.showTagManifestOpened = true;
|
||||
}
|
||||
}
|
||||
|
@ -1,25 +0,0 @@
|
||||
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
export class TagView {
|
||||
tag: string;
|
||||
pullCommand: string;
|
||||
signed: number = -1;
|
||||
author: string;
|
||||
created: Date;
|
||||
dockerVersion: string;
|
||||
architecture: string;
|
||||
os: string;
|
||||
id: string;
|
||||
parent: string;
|
||||
}
|
@ -11,30 +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.
|
||||
/*
|
||||
{
|
||||
"tag": "latest",
|
||||
"manifest": {
|
||||
"schemaVersion": 1,
|
||||
"name": "library/photon",
|
||||
"tag": "latest",
|
||||
"architecture": "amd64",
|
||||
"history": []
|
||||
},
|
||||
|
||||
*/
|
||||
export class Tag {
|
||||
tag: string;
|
||||
manifest: {
|
||||
schemaVersion: number;
|
||||
name: string;
|
||||
tag: string;
|
||||
architecture: string;
|
||||
history: [
|
||||
{
|
||||
v1Compatibility: string;
|
||||
}
|
||||
];
|
||||
};
|
||||
signed: number;
|
||||
digest: string;
|
||||
name: string;
|
||||
architecture: string;
|
||||
os: string;
|
||||
docker_version: string;
|
||||
author: string;
|
||||
created: Date;
|
||||
signature?: {[key: string]: any | any[]}
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
/*
|
||||
[
|
||||
{
|
||||
"tag": "2.0",
|
||||
"hashes": {
|
||||
"sha256": "E1lggRW5RZnlZBY4usWu8d36p5u5YFfr9B68jTOs+Kc="
|
||||
}
|
||||
}
|
||||
]
|
||||
*/
|
||||
|
||||
export class VerifiedSignature {
|
||||
tag: string;
|
||||
hashes: {
|
||||
sha256: string;
|
||||
}
|
||||
}
|
@ -300,8 +300,7 @@
|
||||
"FAILED_TO_DELETE_TARGET_IN_USED": "Failed to delete the endpoint in use."
|
||||
},
|
||||
"REPOSITORY": {
|
||||
"COPY_ID": "Copy ID",
|
||||
"COPY_PARENT_ID": "Copy Parent ID",
|
||||
"COPY_DIGEST_ID": "Copy Digest ID",
|
||||
"DELETE": "Delete",
|
||||
"NAME": "Name",
|
||||
"TAGS_COUNT": "Tags",
|
||||
|
@ -300,8 +300,7 @@
|
||||
"FAILED_TO_DELETE_TARGET_IN_USED": "无法删除正在使用的目标。"
|
||||
},
|
||||
"REPOSITORY": {
|
||||
"COPY_ID": "复制ID",
|
||||
"COPY_PARENT_ID": "复制父级ID",
|
||||
"COPY_DIGEST_ID": "复制摘要ID",
|
||||
"DELETE": "删除",
|
||||
"NAME": "名称",
|
||||
"TAGS_COUNT": "标签数",
|
||||
|
Loading…
Reference in New Issue
Block a user