Merge remote-tracking branch 'upstream/new-ui-with-sync-image' into new-ui-with-sync-image

This commit is contained in:
Wenkai Yin 2016-07-05 14:16:22 +08:00
commit d6abff410f
26 changed files with 251 additions and 110 deletions

7
.gitignore vendored
View File

@ -6,9 +6,4 @@ Deploy/config/db/env
Deploy/config/jobservice/env
ui/ui
*.pyc
jobservice/*.sql
jobservice/*.sh
jobservice/*.json
jobservice/jobservice
jobservice/test

View File

@ -1,23 +0,0 @@
/*
Copyright (c) 2016 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.
*/
package error
// RetryChecker checks whether a job should retry if encounters an error
type RetryChecker interface {
// Retry : if the error can be disappear after retrying the job, Retry
// returns true
Retry(error) bool
}

View File

@ -74,6 +74,17 @@ func (d *Deleter) Exit() error {
// Enter deletes repository or tags
func (d *Deleter) Enter() (string, error) {
state, err := d.enter()
if err != nil && retry(err) {
d.logger.Info("waiting for retrying...")
return models.JobRetrying, nil
}
return state, err
}
func (d *Deleter) enter() (string, error) {
if len(d.tags) == 0 {
tags, err := d.dstClient.ListTag()

View File

@ -19,12 +19,10 @@ import (
"net"
)
// ReplicaRetryChecker determines whether a job should be retried when an error occurred
type ReplicaRetryChecker struct {
}
// Retry ...
func (r *ReplicaRetryChecker) Retry(err error) bool {
func retry(err error) bool {
if err == nil {
return false
}
return isTemporary(err)
}

View File

@ -152,6 +152,17 @@ type Checker struct {
// Enter check existence of project, if it does not exist, create it,
// if it exists, check whether the user has write privilege to it.
func (c *Checker) Enter() (string, error) {
state, err := c.enter()
if err != nil && retry(err) {
c.logger.Info("waiting for retrying...")
return models.JobRetrying, nil
}
return state, err
}
func (c *Checker) enter() (string, error) {
enter:
exist, canWrite, err := c.projectExist()
if err != nil {
@ -316,6 +327,17 @@ type ManifestPuller struct {
// Enter pulls manifest of a tag and checks if all blobs exist in the destination registry
func (m *ManifestPuller) Enter() (string, error) {
state, err := m.enter()
if err != nil && retry(err) {
m.logger.Info("waiting for retrying...")
return models.JobRetrying, nil
}
return state, err
}
func (m *ManifestPuller) enter() (string, error) {
if len(m.tags) == 0 {
m.logger.Infof("no tag needs to be replicated, next state is \"finished\"")
return models.JobFinished, nil
@ -389,6 +411,17 @@ type BlobTransfer struct {
// Enter pulls blobs and then pushs them to destination registry.
func (b *BlobTransfer) Enter() (string, error) {
state, err := b.enter()
if err != nil && retry(err) {
b.logger.Info("waiting for retrying...")
return models.JobRetrying, nil
}
return state, err
}
func (b *BlobTransfer) enter() (string, error) {
name := b.repository
tag := b.tags[0]
for _, blob := range b.blobs {
@ -417,6 +450,17 @@ type ManifestPusher struct {
// exists, pushs it to destination registry. The checking operation is to avoid
// the situation that the tag is deleted during the blobs transfering
func (m *ManifestPusher) Enter() (string, error) {
state, err := m.enter()
if err != nil && retry(err) {
m.logger.Info("waiting for retrying...")
return models.JobRetrying, nil
}
return state, err
}
func (m *ManifestPusher) enter() (string, error) {
name := m.repository
tag := m.tags[0]
_, exist, err := m.srcClient.ManifestExist(tag)

View File

@ -1,23 +1,36 @@
/*
Copyright (c) 2016 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.
Copyright (c) 2016 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.
*/
package job
import (
"github.com/vmware/harbor/utils/log"
"time"
)
var jobQueue = make(chan int64)
// Schedule put a job id into job queue.
func Schedule(jobID int64) {
jobQueue <- jobID
}
// Reschedule is called by statemachine to retry a job
func Reschedule(jobID int64) {
log.Debugf("Job %d will be rescheduled in 5 minutes", jobID)
time.Sleep(5 * time.Minute)
log.Debugf("Rescheduling job %d", jobID)
Schedule(jobID)
}

View File

@ -1,16 +1,16 @@
/*
Copyright (c) 2016 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.
Copyright (c) 2016 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.
*/
package job
@ -33,25 +33,10 @@ type StateHandler interface {
Exit() error
}
// DummyHandler is the default implementation of StateHander interface, which has empty Enter and Exit methods.
type DummyHandler struct {
JobID int64
}
// Enter ...
func (dh DummyHandler) Enter() (string, error) {
return "", nil
}
// Exit ...
func (dh DummyHandler) Exit() error {
return nil
}
// StatusUpdater implements the StateHandler interface which updates the status of a job in DB when the job enters
// a status.
type StatusUpdater struct {
DummyHandler
JobID int64
State string
}
@ -69,9 +54,34 @@ func (su StatusUpdater) Enter() (string, error) {
return next, err
}
// Exit ...
func (su StatusUpdater) Exit() error {
return nil
}
// Retry handles a special "retrying" in which case it will update the status in DB and reschedule the job
// via scheduler
type Retry struct {
JobID int64
}
// Enter ...
func (jr Retry) Enter() (string, error) {
err := dao.UpdateRepJobStatus(jr.JobID, models.JobRetrying)
if err != nil {
log.Errorf("Failed to update state of job :%d to Retrying, error: %v", jr.JobID, err)
}
go Reschedule(jr.JobID)
return "", err
}
// Exit ...
func (jr Retry) Exit() error {
return nil
}
// ImgPuller was for testing
type ImgPuller struct {
DummyHandler
img string
logger *log.Logger
}
@ -80,13 +90,17 @@ type ImgPuller struct {
func (ip ImgPuller) Enter() (string, error) {
ip.logger.Infof("I'm pretending to pull img:%s, then sleep 30s", ip.img)
time.Sleep(30 * time.Second)
ip.logger.Infof("wake up from sleep....")
return "push-img", nil
ip.logger.Infof("wake up from sleep.... testing retry")
return models.JobRetrying, nil
}
// Exit ...
func (ip ImgPuller) Exit() error {
return nil
}
// ImgPusher is a statehandler for testing
type ImgPusher struct {
DummyHandler
targetURL string
logger *log.Logger
}
@ -95,6 +109,11 @@ type ImgPusher struct {
func (ip ImgPusher) Enter() (string, error) {
ip.logger.Infof("I'm pretending to push img to:%s, then sleep 30s", ip.targetURL)
time.Sleep(30 * time.Second)
ip.logger.Infof("wake up from sleep....")
return models.JobContinue, nil
ip.logger.Infof("wake up from sleep.... testing retry")
return models.JobRetrying, nil
}
// Exit ...
func (ip ImgPusher) Exit() error {
return nil
}

View File

@ -179,6 +179,7 @@ func (sm *SM) Init() {
models.JobError: struct{}{},
models.JobStopped: struct{}{},
models.JobCanceled: struct{}{},
models.JobRetrying: struct{}{},
}
}
@ -243,9 +244,11 @@ func (sm *SM) Reset(jid int64) error {
sm.Transitions = make(map[string]map[string]struct{})
sm.CurrentState = models.JobPending
sm.AddTransition(models.JobPending, models.JobRunning, StatusUpdater{DummyHandler{JobID: sm.JobID}, models.JobRunning})
sm.Handlers[models.JobError] = StatusUpdater{DummyHandler{JobID: sm.JobID}, models.JobError}
sm.Handlers[models.JobStopped] = StatusUpdater{DummyHandler{JobID: sm.JobID}, models.JobStopped}
sm.AddTransition(models.JobPending, models.JobRunning, StatusUpdater{sm.JobID, models.JobRunning})
sm.AddTransition(models.JobRetrying, models.JobRunning, StatusUpdater{sm.JobID, models.JobRunning})
sm.Handlers[models.JobError] = StatusUpdater{sm.JobID, models.JobError}
sm.Handlers[models.JobStopped] = StatusUpdater{sm.JobID, models.JobStopped}
sm.Handlers[models.JobRetrying] = Retry{sm.JobID}
switch sm.Parms.Operation {
case models.RepOpTransfer:
@ -259,6 +262,12 @@ func (sm *SM) Reset(jid int64) error {
return err
}
//for testing onlly
func addTestTransition(sm *SM) error {
sm.AddTransition(models.JobRunning, "pull-img", ImgPuller{img: sm.Parms.Repository, logger: sm.Logger})
return nil
}
func addImgTransferTransition(sm *SM) error {
base, err := replication.InitBaseHandler(sm.Parms.Repository, sm.Parms.LocalRegURL, config.UISecret(),
sm.Parms.TargetURL, sm.Parms.TargetUsername, sm.Parms.TargetPassword,
@ -269,7 +278,7 @@ func addImgTransferTransition(sm *SM) error {
sm.AddTransition(models.JobRunning, replication.StateCheck, &replication.Checker{BaseHandler: base})
sm.AddTransition(replication.StateCheck, replication.StatePullManifest, &replication.ManifestPuller{BaseHandler: base})
sm.AddTransition(replication.StatePullManifest, replication.StateTransferBlob, &replication.BlobTransfer{BaseHandler: base})
sm.AddTransition(replication.StatePullManifest, models.JobFinished, &StatusUpdater{DummyHandler{JobID: sm.JobID}, models.JobFinished})
sm.AddTransition(replication.StatePullManifest, models.JobFinished, &StatusUpdater{sm.JobID, models.JobFinished})
sm.AddTransition(replication.StateTransferBlob, replication.StatePushManifest, &replication.ManifestPusher{BaseHandler: base})
sm.AddTransition(replication.StatePushManifest, replication.StatePullManifest, &replication.ManifestPuller{BaseHandler: base})
return nil
@ -283,7 +292,7 @@ func addImgDeleteTransition(sm *SM) error {
}
sm.AddTransition(models.JobRunning, replication.StateDelete, deleter)
sm.AddTransition(replication.StateDelete, models.JobFinished, &StatusUpdater{DummyHandler{JobID: sm.JobID}, models.JobFinished})
sm.AddTransition(replication.StateDelete, models.JobFinished, &StatusUpdater{sm.JobID, models.JobFinished})
return nil
}

View File

@ -38,13 +38,13 @@ func resumeJobs() {
if err != nil {
log.Warningf("Failed to reset all running jobs to pending, error: %v", err)
}
jobs, err := dao.GetRepJobByStatus(models.JobPending)
jobs, err := dao.GetRepJobByStatus(models.JobPending, models.JobRetrying)
if err == nil {
for _, j := range jobs {
log.Debugf("Rescheduling job: %d", j.ID)
log.Debugf("Resuming job: %d", j.ID)
job.Schedule(j.ID)
}
} else {
log.Warningf("Failed to get pending jobs, error: %v", err)
log.Warningf("Failed to jobs to resume, error: %v", err)
}
}

View File

@ -124,4 +124,8 @@ nav .container-custom {
height: 1.2em;
margin-bottom: 2px;
vertical-align: middle;
}
}
a:hover, a:visited, a:link {
text-decoration: none;
}

View File

@ -0,0 +1,5 @@
<a href="javascript:void(0)" role="button" tab-index="0"
data-trigger="focus" data-toggle="popover" data-placement="right"
data-title="//vm.helpTitle//">
<span class="glyphicon glyphicon-info-sign"></span>
</a>

View File

@ -0,0 +1,34 @@
(function() {
'use strict';
angular
.module('harbor.inline.help')
.directive('inlineHelp', inlineHelp);
function InlineHelpController() {
var vm = this;
}
function inlineHelp() {
var directive = {
'restrict': 'E',
'templateUrl': '/static/resources/js/components/inline-help/inline-help.directive.html',
'scope': {
'helpTitle': '@',
'content': '@'
},
'link': link,
'controller': InlineHelpController,
'controllerAs': 'vm',
'bindToController': true
};
return directive;
function link(scope, element, attr, ctrl) {
element.popover({
'title': ctrl.helpTitle,
'content': ctrl.content,
'html': true
});
}
}
})();

View File

@ -0,0 +1,8 @@
(function() {
'use strict';
angular
.module('harbor.inline.help', []);
})();

View File

@ -45,7 +45,7 @@
function optionalMenu() {
var directive = {
'restrict': 'E',
'templateUrl': '/optional_menu',
'templateUrl': '/optional_menu?timestamp=' + new Date().getTime(),
'scope': true,
'controller': OptionalMenuController,
'controllerAs': 'vm',

View File

@ -12,9 +12,10 @@
</div>
</div>
<div class="form-group">
<label for="roleIdList">// 'role' | tr //:</label>&nbsp;&nbsp;
<label for="roleIdList">// 'role' | tr //:</label>
<inline-help help-title="//'inline_help_role_title' | tr//" content="//'inline_help_role' | tr//"></inline-help>&nbsp;&nbsp;
<span ng-repeat="role in vm.roles">
<input type="radio" name="role" ng-model="vm.optRole" value="//role.id//">&nbsp;//role.name//&nbsp;&nbsp;
<input type="radio" name="role" ng-model="vm.optRole" value="//role.id//">&nbsp;//role.name | tr//&nbsp;&nbsp;
</span>
</div>
</div>

View File

@ -9,15 +9,15 @@
function roles() {
return [
{'id': '1', 'name': 'Project Admin', 'roleName': 'projectAdmin'},
{'id': '2', 'name': 'Developer', 'roleName': 'developer'},
{'id': '3', 'name': 'Guest', 'roleName': 'guest'}
{'id': '1', 'name': 'project_admin', 'roleName': 'projectAdmin'},
{'id': '2', 'name': 'developer', 'roleName': 'developer'},
{'id': '3', 'name': 'guest', 'roleName': 'guest'}
];
}
getRole.$inject = ['roles'];
getRole.$inject = ['roles', '$filter', 'trFilter'];
function getRole(roles) {
function getRole(roles, $filter, trFilter) {
var r = roles();
return get;
function get(query) {

View File

@ -1,5 +1,5 @@
<ng-switch on="vm.editMode">
<span ng-switch-default>//vm.currentRole.name//</span>
<select class="form-control" style="width: auto; height: auto; padding: 0;" ng-switch-when="true" ng-model="vm.currentRole" ng-options="role as role.name for role in vm.roles track by role.roleName" ng-change="vm.selectRole(vm.currentRole)">
<span ng-switch-default>//vm.currentRole.name | tr//</span>
<select class="form-control" style="width: auto; height: auto; padding: 0;" ng-switch-when="true" ng-model="vm.currentRole" ng-options="role as (role.name | tr) for role in vm.roles track by role.roleName" ng-change="vm.selectRole(vm.currentRole)">
</select>
</ng-switch>

View File

@ -13,7 +13,8 @@
</div>
</div>
<div class="form-group" style="margin-top: 5px;">
<input type="checkbox" ng-model="vm.isPublic">&nbsp;// 'public' | tr //
<input type="checkbox" ng-model="vm.isPublic">&nbsp;// 'public' | tr //
<inline-help help-title="// 'inline_help_publicity_title' | tr //" content="// 'inline_help_publicity' | tr //"></inline-help>
</div>
</div>
<div class="col-xs-2 col-md-2">

View File

@ -409,7 +409,7 @@
if(!ctrl.toggleErrorMessage) {
element.find('#createPolicyModal').modal('hide');
}
}, 50);
}, 150);
}
}
}

View File

@ -21,10 +21,10 @@
return {
'responseError': function(rejection) {
var pathname = $window.location.pathname;
var exclusions = ['/', '/search', '/reset_password', '/sign_up', '/forgot_password'];
var exclusion = ['/', '/search', '/reset_password', '/sign_up', '/forgot_password', '/repository'];
var isExcluded = false;
for(var i in exclusions) {
if(exclusions[i] === pathname) {
for(var i in exclusion) {
if(exclusion[i] === pathname) {
isExcluded = true;
break;
}

View File

@ -44,6 +44,7 @@
'harbor.validator',
'harbor.replication',
'harbor.system.management',
'harbor.loading.progress'
'harbor.loading.progress',
'harbor.inline.help'
]);
})();

View File

@ -23,7 +23,7 @@
function navigationDetails() {
var directive = {
restrict: 'E',
templateUrl: '/navigation_detail',
templateUrl: '/navigation_detail?timestamp=' + new Date().getTime(),
link: link,
scope: {
'target': '='

View File

@ -16,7 +16,7 @@
function navigationHeader() {
var directive = {
restrict: 'E',
templateUrl: '/navigation_header',
templateUrl: '/navigation_header?timestamp=' + new Date().getTime(),
link: link,
scope: true,
controller: NavigationHeaderController,

View File

@ -235,5 +235,14 @@ var locale_messages = {
'failed_to_delete_destination': 'Failed to delete destination.',
'failed_to_create_destination': 'Failed to create destination.',
'failed_to_update_destination': 'Failed to update destination.',
'project_admin': 'Project Admin',
'developer': 'Developer',
'guest': 'Guest',
'inline_help_role_title': '<strong>The Definitions of Roles</strong>',
'inline_help_role': '<strong>Project Admin</strong>: When creating a new project, you will be assigned the "ProjectAdmin" role to the project. Besides read-write privileges, the "ProjectAdmin" also has some management privileges, such as adding and removing members.<br/>' +
'<strong>Developer</strong>: Developer has read and write privileges for a project.<br/>' +
'<strong>Guest</strong>: Guest has read-only privilege for a specified project.',
'inline_help_publicity_title': '<strong>Publicity of Project</strong>',
'inline_help_publicity': 'Setting the project as public.'
};

View File

@ -234,5 +234,14 @@ var locale_messages = {
'failed_to_update_replication_policy': '修改复制策略失败。',
'failed_to_delete_destination': '删除目标失败。',
'failed_to_create_destination': '创建目标失败。',
'failed_to_update_destination': '修改目标失败。'
'failed_to_update_destination': '修改目标失败。',
'project_admin': '项目管理员',
'developer': '开发人员',
'guest': '来宾用户',
'inline_help_role_title': '<strong>角色定义</strong>',
'inline_help_role': '<strong>项目管理员</strong>: 当创建一个新项目后,您将被指派一个“项目管理员”角色。除了具备读/写权限外,“项目管理员”还拥有添加、删除其他项目成员的管理权限。<br/>'+
'<strong>开发人员</strong>: “开发人员” 拥有一个项目的读/写权限。<br/>' +
'<strong>来宾用户</strong>: “来宾用户”拥有特定项目的只读权限。',
'inline_help_publicity_title': '<strong>公开项目</strong>',
'inline_help_publicity': '设置该项目为公开。'
};

View File

@ -237,4 +237,7 @@
<script src="/static/resources/js/components/top-repository/top-repository.directive.js"></script>
<script src="/static/resources/js/components/loading-progress/loading-progress.module.js"></script>
<script src="/static/resources/js/components/loading-progress/loading-progress.directive.js"></script>
<script src="/static/resources/js/components/loading-progress/loading-progress.directive.js"></script>
<script src="/static/resources/js/components/inline-help/inline-help.module.js"></script>
<script src="/static/resources/js/components/inline-help/inline-help.directive.js"></script>