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:
Wenkai Yin 2017-06-05 16:53:15 +08:00
commit 9c4adbe8c9
54 changed files with 1043 additions and 759 deletions

View File

@ -504,7 +504,7 @@ func TestChangeUserPasswordWithIncorrectOldPassword(t *testing.T) {
}
func TestQueryRelevantProjectsWhenNoProjectAdded(t *testing.T) {
projects, err := SearchProjects(currentUser.UserID)
projects, err := GetHasReadPermProjects(currentUser.Username)
if err != nil {
t.Errorf("Error occurred in QueryRelevantProjects: %v", err)
}

View File

@ -156,11 +156,18 @@ func ToggleProjectPublicity(projectID int64, publicity int) error {
return err
}
// SearchProjects returns a project list,
// GetHasReadPermProjects returns a project list,
// which satisfies the following conditions:
// 1. the project is not deleted
// 2. the prject is public or the user is a member of the project
func SearchProjects(userID int) ([]*models.Project, error) {
func GetHasReadPermProjects(username string) ([]*models.Project, error) {
user, err := GetUser(models.User{
Username: username,
})
if err != nil {
return nil, err
}
o := GetOrmer()
sql :=
@ -174,7 +181,7 @@ func SearchProjects(userID int) ([]*models.Project, error) {
var projects []*models.Project
if _, err := o.Raw(sql, userID).QueryRows(&projects); err != nil {
if _, err := o.Raw(sql, user.UserID).QueryRows(&projects); err != nil {
return nil, err
}

View File

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

View File

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

View File

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

View File

@ -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

View File

@ -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
}
}

View File

@ -128,7 +128,7 @@ func init() {
_ = updateInitPassword(1, "Harbor12345")
//syncRegistry
if err := SyncRegistry(); err != nil {
if err := SyncRegistry(config.GlobalProjectMgr); err != nil {
log.Fatalf("failed to sync repositories from registry: %v", err)
}

View File

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

View File

@ -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 ...

View File

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

View File

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

View File

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

View File

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

View File

@ -15,9 +15,9 @@
package api
import (
"fmt"
"net/http"
"github.com/vmware/harbor/src/common/api"
"github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/common/models"
"github.com/vmware/harbor/src/common/utils/log"
@ -40,42 +40,44 @@ const (
// StatisticAPI handles request to /api/statistics/
type StatisticAPI struct {
api.BaseAPI
userID int
BaseController
username string
}
//Prepare validates the URL and the user
func (s *StatisticAPI) Prepare() {
s.userID = s.ValidateUser()
s.BaseController.Prepare()
if !s.SecurityCtx.IsAuthenticated() {
s.HandleUnauthorized()
return
}
s.username = s.SecurityCtx.GetUsername()
}
// Get total projects and repos of the user
func (s *StatisticAPI) Get() {
statistic := map[string]int64{}
t := true
n, err := dao.GetTotalOfProjects(&models.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
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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])
}

View File

@ -298,3 +298,58 @@ func TestGetAll(t *testing.T) {
}
assert.True(t, exist)
}
func TestGetHasReadPerm(t *testing.T) {
pm := &ProjectManager{}
// do not pass username
projects, err := pm.GetHasReadPerm()
assert.Nil(t, err)
assert.NotEqual(t, 0, len(projects))
exist := false
for _, project := range projects {
if project.ProjectID == 1 {
exist = true
break
}
}
assert.True(t, exist)
// username is nil
projects, err = pm.GetHasReadPerm("")
assert.Nil(t, err)
assert.NotEqual(t, 0, len(projects))
exist = false
for _, project := range projects {
if project.ProjectID == 1 {
exist = true
break
}
}
assert.True(t, exist)
// valid username
id, err := pm.Create(&models.Project{
Name: "get_has_read_perm_test",
OwnerID: 1,
Public: 0,
})
assert.Nil(t, err)
defer pm.Delete(id)
projects, err = pm.GetHasReadPerm("admin")
assert.Nil(t, err)
assert.NotEqual(t, 0, len(projects))
exist1 := false
exist2 := false
for _, project := range projects {
if project.ProjectID == 1 {
exist1 = true
}
if project.ProjectID == id {
exist2 = true
}
}
assert.True(t, exist1)
assert.True(t, exist2)
}

View File

@ -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)
}

View File

@ -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 }
})
...
```

View File

@ -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;

View File

@ -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>

View File

@ -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');

View File

@ -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() {

View File

@ -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.",

View File

@ -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": "成功删除镜像仓库。",

View File

@ -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}}

View File

@ -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);
}

View File

@ -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>`;

View File

@ -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);

View File

@ -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);
});
}

View File

@ -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>

View File

@ -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>

View File

@ -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) {

View File

@ -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;
}
/**

View File

@ -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)

View File

@ -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);
});
})));
});

View File

@ -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 {

View File

@ -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>`;

View File

@ -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'

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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}`)

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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[]}
}

View File

@ -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;
}
}

View File

@ -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",

View File

@ -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": "标签数",