Merge remote-tracking branch 'upstream/job-service' into develop

This commit is contained in:
wemeya 2016-06-07 20:35:24 +08:00
commit 0147ec8d3b
14 changed files with 380 additions and 94 deletions

View File

@ -5,6 +5,7 @@ Alexey Erkak <eryigin at mail.ru>
Allen Heavey <xheavey at gmail.com>
Amanda Zhang <amzhang at vmware.com>
Benniu Ji <benniuji at gmail.com>
Bin Liu <liubin0329 at gmail.com>
Bobby Zhang <junzhang at vmware.com>
Chaofeng Wu <chaofengw at vmware.com>
Daniel Jiang <jiangd at vmware.com>

View File

@ -13,7 +13,7 @@ Project Harbor is an enterprise-class registry server, which extends the open so
* **Graphical user portal**: User can easily browse, search Docker repositories, manage projects/namespaces.
* **AD/LDAP support**: Harbor integrates with existing enterprise AD/LDAP for user authentication and management.
* **Auditing**: All the operations to the repositories are tracked.
* **Internationalization**: Already localized for English, Chinese, German and Russian. More languages can be added.
* **Internationalization**: Already localized for English, Chinese, German, Japanese and Russian. More languages can be added.
* **RESTful API**: RESTful APIs for most administrative operations, easing intergration with external management platforms.
### Getting Started
@ -67,7 +67,7 @@ Harbor is available under the [Apache 2 license](LICENSE).
&nbsp; &nbsp; <a href="https://www.caicloud.io" border="0"><img alt="CaiCloud" src="docs/img/caicloudLogoWeb.png"></a>
### Users
<a href="https://www.madailicai.com/" border="0" target="_blank"><img alt="MaDaiLiCai" src="docs/img/UserMaDai.jpg"></a>
<a href="https://www.madailicai.com/" border="0" target="_blank"><img alt="MaDaiLiCai" src="docs/img/UserMaDai.jpg"></a> <a href="https://www.dianrong.com/" border="0" target="_blank"><img alt="Dianrong" src="docs/img/dianrong.png"></a>
### Supporting Technologies
<img alt="beego" src="docs/img/beegoLogo.png"> Harbor is powered by <a href="http://beego.me/">Beego</a>, an open source framework to build and develop applications in the Go way.

47
ROADMAP.md Normal file
View File

@ -0,0 +1,47 @@
## Harbor Roadmap
### About this document
This document provides description of items that are gathered from the community and planned in Harbor's roadmap. This should serve as a reference point for Harbor users and contributors to understand where the project is heading, and help determine if a contribution could be conflicting with a longer term plan.
### How to help?
Discussion on the roadmap can take place in threads under [Issues](https://github.com/vmware/harbor/issues). Please open and comment on an issue if you want to provide suggestions and feedback to an item in the roadmap. Please review the roadmap to avoid potential duplicated effort.
### How to add an item to the roadmap?
Please open an issue to track any initiative on the roadmap of Harbor. We will work with and rely on our community to focus our efforts to improve Harbor.
---
### 1. Image replication between Harbor instances
Enable images to be replicated between two or more Harbor instances. This is useful to have multiple registry servers servicing a large cluster of nodes, or have distributed registry instances with identical images.
### 2. Image deletion and garbage collection
a) Images can be deleted from UI. The files of deleted images are not removed immediately.
b) The files of deleted images are recycled by an administrator during system maintenance(Garbage collection). The registry service must be shut down during the process of garbage collection.
### 3. Authentication (OAuth2)
In addition to LDAP/AD and local users, OAuth 2.0 can be used to authenticate a user.
### 4. High Availability
Support multi-node deployment of Harbor for high availability, scalability and load-balancing purposes.
### 5. Statistics and description for repositories
User can add a description to a repository. The access count of a repo can be aggregated and displayed.
### 6. Audit all operations in the system
Currently only image related operations are logged. Other operations in Harbor, such as user creation/deletion, role changes, password reset, should be tracked as well.
### 7. Migration tool to move from an existing registry to Harbor
A tool to migrate images from a vanilla registry server to Harbor, without the need to export/import a large amount of data.
### 8. Support API versioning
Provide versioning of Harbor's API.

View File

@ -17,8 +17,10 @@ package api
import (
"encoding/json"
"fmt"
"net/http"
"github.com/astaxie/beego/validation"
"github.com/vmware/harbor/auth"
"github.com/vmware/harbor/dao"
"github.com/vmware/harbor/models"
@ -51,6 +53,30 @@ func (b *BaseAPI) DecodeJSONReq(v interface{}) {
}
}
// Validate validates v if it implements interface validation.ValidFormer
func (b *BaseAPI) Validate(v interface{}) {
validator := validation.Validation{}
isValid, err := validator.Valid(v)
if err != nil {
log.Errorf("failed to validate: %v", err)
b.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
if !isValid {
message := ""
for _, e := range validator.Errors {
message += fmt.Sprintf("%s %s \n", e.Field, e.Message)
}
b.CustomAbort(http.StatusBadRequest, message)
}
}
// DecodeJSONReqAndValidate does both decoding and validation
func (b *BaseAPI) DecodeJSONReqAndValidate(v interface{}) {
b.DecodeJSONReq(v)
b.Validate(v)
}
// ValidateUser checks if the request triggered by a valid user
func (b *BaseAPI) ValidateUser() int {

View File

@ -69,9 +69,40 @@ func (pa *RepPolicyAPI) Get() {
// Post creates a policy, and if it is enbled, the replication will be triggered right now.
func (pa *RepPolicyAPI) Post() {
policy := models.RepPolicy{}
pa.DecodeJSONReq(&policy)
pid, err := dao.AddRepPolicy(policy)
policy := &models.RepPolicy{}
pa.DecodeJSONReqAndValidate(policy)
po, err := dao.GetRepPolicyByName(policy.Name)
if err != nil {
log.Errorf("failed to get policy %s: %v", policy.Name, err)
pa.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
if po != nil {
pa.CustomAbort(http.StatusConflict, "name is already used")
}
project, err := dao.GetProjectByID(policy.ProjectID)
if err != nil {
log.Errorf("failed to get project %d: %v", policy.ProjectID, err)
pa.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
if project == nil {
pa.CustomAbort(http.StatusBadRequest, fmt.Sprintf("project %d does not exist", policy.ProjectID))
}
target, err := dao.GetRepTarget(policy.TargetID)
if err != nil {
log.Errorf("failed to get target %d: %v", policy.TargetID, err)
pa.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
if target == nil {
pa.CustomAbort(http.StatusBadRequest, fmt.Sprintf("target %d does not exist", policy.TargetID))
}
pid, err := dao.AddRepPolicy(*policy)
if err != nil {
log.Errorf("Failed to add policy to DB, error: %v", err)
pa.RenderError(http.StatusInternalServerError, "Internal Error")

View File

@ -164,10 +164,16 @@ func (t *TargetAPI) Get() {
// Post ...
func (t *TargetAPI) Post() {
target := &models.RepTarget{}
t.DecodeJSONReq(target)
t.DecodeJSONReqAndValidate(target)
if len(target.Name) == 0 || len(target.URL) == 0 {
t.CustomAbort(http.StatusBadRequest, "name or URL is nil")
ta, err := dao.GetRepTargetByName(target.Name)
if err != nil {
log.Errorf("failed to get target %s: %v", target.Name, err)
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
if ta != nil {
t.CustomAbort(http.StatusConflict, "name is already used")
}
if len(target.Password) != 0 {
@ -187,16 +193,32 @@ func (t *TargetAPI) Post() {
func (t *TargetAPI) Put() {
id := t.getIDFromURL()
if id == 0 {
t.CustomAbort(http.StatusBadRequest, http.StatusText(http.StatusBadRequest))
t.CustomAbort(http.StatusBadRequest, "id can not be empty or 0")
}
target := &models.RepTarget{}
t.DecodeJSONReq(target)
t.DecodeJSONReqAndValidate(target)
if target.ID == 0 {
target.ID = id
originTarget, err := dao.GetRepTarget(id)
if err != nil {
log.Errorf("failed to get target %d: %v", id, err)
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
if target.Name != originTarget.Name {
ta, err := dao.GetRepTargetByName(target.Name)
if err != nil {
log.Errorf("failed to get target %s: %v", target.Name, err)
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
if ta != nil {
t.CustomAbort(http.StatusConflict, "name is already used")
}
}
target.ID = id
if len(target.Password) != 0 {
target.Password = utils.ReversibleEncrypt(target.Password)
}

View File

@ -799,6 +799,78 @@ func TestAddRepTarget(t *testing.T) {
}
}
func TestGetRepTargetByName(t *testing.T) {
target, err := GetRepTarget(targetID)
if err != nil {
t.Fatalf("failed to get target %d: %v", targetID, err)
}
target2, err := GetRepTargetByName(target.Name)
if err != nil {
t.Fatalf("failed to get target %s: %v", target.Name, err)
}
if target.Name != target2.Name {
t.Errorf("unexpected target name: %s, expected: %s", target2.Name, target.Name)
}
}
func TestUpdateRepTarget(t *testing.T) {
target := &models.RepTarget{
Name: "name",
URL: "http://url",
Username: "username",
Password: "password",
}
id, err := AddRepTarget(*target)
if err != nil {
t.Fatalf("failed to add target: %v", err)
}
defer func() {
if err := DeleteRepTarget(id); err != nil {
t.Logf("failed to delete target %d: %v", id, err)
}
}()
target.ID = id
target.Name = "new_name"
target.URL = "http://new_url"
target.Username = "new_username"
target.Password = "new_password"
if err = UpdateRepTarget(*target); err != nil {
t.Fatalf("failed to update target: %v", err)
}
target, err = GetRepTarget(id)
if err != nil {
t.Fatalf("failed to get target %d: %v", id, err)
}
if target.Name != "new_name" {
t.Errorf("unexpected name: %s, expected: %s", target.Name, "new_name")
}
if target.URL != "http://new_url" {
t.Errorf("unexpected url: %s, expected: %s", target.URL, "http://new_url")
}
if target.Username != "new_username" {
t.Errorf("unexpected username: %s, expected: %s", target.Username, "new_username")
}
if target.Password != "new_password" {
t.Errorf("unexpected password: %s, expected: %s", target.Password, "new_password")
}
}
func TestGetAllRepTargets(t *testing.T) {
if _, err := GetAllRepTargets(); err != nil {
t.Fatalf("failed to get all targets: %v", err)
}
}
func TestAddRepPolicy(t *testing.T) {
policy := models.RepPolicy{
ProjectID: 1,
@ -833,6 +905,23 @@ func TestAddRepPolicy(t *testing.T) {
}
func TestGetRepPolicyByName(t *testing.T) {
policy, err := GetRepPolicy(policyID)
if err != nil {
t.Fatalf("failed to get policy %d: %v", policyID, err)
}
policy2, err := GetRepPolicyByName(policy.Name)
if err != nil {
t.Fatalf("failed to get policy %s: %v", policy.Name, err)
}
if policy.Name != policy2.Name {
t.Errorf("unexpected name: %s, expected: %s", policy2.Name, policy.Name)
}
}
func TestDisableRepPolicy(t *testing.T) {
err := DisableRepPolicy(policyID)
if err != nil {

View File

@ -11,13 +11,13 @@ import (
// AddRepTarget ...
func AddRepTarget(target models.RepTarget) (int64, error) {
o := orm.NewOrm()
o := GetOrmer()
return o.Insert(&target)
}
// GetRepTarget ...
func GetRepTarget(id int64) (*models.RepTarget, error) {
o := orm.NewOrm()
o := GetOrmer()
t := models.RepTarget{ID: id}
err := o.Read(&t)
if err == orm.ErrNoRows {
@ -26,28 +26,34 @@ func GetRepTarget(id int64) (*models.RepTarget, error) {
return &t, err
}
// GetRepTargetByName ...
func GetRepTargetByName(name string) (*models.RepTarget, error) {
o := GetOrmer()
t := models.RepTarget{Name: name}
err := o.Read(&t, "Name")
if err == orm.ErrNoRows {
return nil, nil
}
return &t, err
}
// DeleteRepTarget ...
func DeleteRepTarget(id int64) error {
o := orm.NewOrm()
o := GetOrmer()
_, err := o.Delete(&models.RepTarget{ID: id})
return err
}
// UpdateRepTarget ...
func UpdateRepTarget(target models.RepTarget) error {
o := orm.NewOrm()
if len(target.Password) != 0 {
_, err := o.Update(&target)
return err
}
_, err := o.Update(&target, "URL", "Name", "Username")
o := GetOrmer()
_, err := o.Update(&target, "URL", "Name", "Username", "Password")
return err
}
// GetAllRepTargets ...
func GetAllRepTargets() ([]*models.RepTarget, error) {
o := orm.NewOrm()
o := GetOrmer()
qs := o.QueryTable(&models.RepTarget{})
var targets []*models.RepTarget
_, err := qs.All(&targets)
@ -56,7 +62,7 @@ func GetAllRepTargets() ([]*models.RepTarget, error) {
// AddRepPolicy ...
func AddRepPolicy(policy models.RepPolicy) (int64, error) {
o := orm.NewOrm()
o := GetOrmer()
sqlTpl := `insert into replication_policy (name, project_id, target_id, enabled, description, cron_str, start_time, creation_time, update_time ) values (?, ?, ?, ?, ?, ?, %s, NOW(), NOW())`
var sql string
if policy.Enabled == 1 {
@ -78,7 +84,7 @@ func AddRepPolicy(policy models.RepPolicy) (int64, error) {
// GetRepPolicy ...
func GetRepPolicy(id int64) (*models.RepPolicy, error) {
o := orm.NewOrm()
o := GetOrmer()
p := models.RepPolicy{ID: id}
err := o.Read(&p)
if err == orm.ErrNoRows {
@ -87,24 +93,35 @@ func GetRepPolicy(id int64) (*models.RepPolicy, error) {
return &p, err
}
// GetRepPolicyByName ...
func GetRepPolicyByName(name string) (*models.RepPolicy, error) {
o := GetOrmer()
p := models.RepPolicy{Name: name}
err := o.Read(&p, "Name")
if err == orm.ErrNoRows {
return nil, nil
}
return &p, err
}
// GetRepPolicyByProject ...
func GetRepPolicyByProject(projectID int64) ([]*models.RepPolicy, error) {
var res []*models.RepPolicy
o := orm.NewOrm()
o := GetOrmer()
_, err := o.QueryTable("replication_policy").Filter("project_id", projectID).All(&res)
return res, err
}
// DeleteRepPolicy ...
func DeleteRepPolicy(id int64) error {
o := orm.NewOrm()
o := GetOrmer()
_, err := o.Delete(&models.RepPolicy{ID: id})
return err
}
// UpdateRepPolicyEnablement ...
func UpdateRepPolicyEnablement(id int64, enabled int) error {
o := orm.NewOrm()
o := GetOrmer()
p := models.RepPolicy{
ID: id,
Enabled: enabled}
@ -125,7 +142,7 @@ func DisableRepPolicy(id int64) error {
// AddRepJob ...
func AddRepJob(job models.RepJob) (int64, error) {
o := orm.NewOrm()
o := GetOrmer()
if len(job.Status) == 0 {
job.Status = models.JobPending
}
@ -137,7 +154,7 @@ func AddRepJob(job models.RepJob) (int64, error) {
// GetRepJob ...
func GetRepJob(id int64) (*models.RepJob, error) {
o := orm.NewOrm()
o := GetOrmer()
j := models.RepJob{ID: id}
err := o.Read(&j)
if err == orm.ErrNoRows {
@ -164,20 +181,20 @@ func GetRepJobToStop(policyID int64) ([]*models.RepJob, error) {
}
func repJobPolicyIDQs(policyID int64) orm.QuerySeter {
o := orm.NewOrm()
o := GetOrmer()
return o.QueryTable("replication_job").Filter("policy_id", policyID)
}
// DeleteRepJob ...
func DeleteRepJob(id int64) error {
o := orm.NewOrm()
o := GetOrmer()
_, err := o.Delete(&models.RepJob{ID: id})
return err
}
// UpdateRepJobStatus ...
func UpdateRepJobStatus(id int64, status string) error {
o := orm.NewOrm()
o := GetOrmer()
j := models.RepJob{
ID: id,
Status: status,

BIN
docs/img/dianrong.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@ -1,54 +1,56 @@
# migration
Migration is a module for migrating database schema between different version of project [harbor](https://github.com/vmware/harbor)
# Migration guide
Migration is a module for migrating database schema between different version of project [Harbor](https://github.com/vmware/harbor)
This module is for those machine running Harbor's old version, such as 0.1.0. If your Harbor' version is up to date, please ignore this module.
**WARNING!!** You must backup your data before migrating
###installation
- step 1: modify migration.cfg
###Installation
- step 1: change `db_username`, `db_password`, `db_port`, `db_name` in migration.cfg
- step 2: build image from dockerfile
```
cd harbor-migration
docker build -t your-image-name .
docker build -t migrate-tool .
```
###migration operation
- show instruction of harbor-migration
```docker run your-image-name help```
- test mysql connection in harbor-migration
```docker run -v /data/database:/var/lib/mysql your-image-name test```
- create backup file in `/path/to/backup`
```
docker run -ti -v /data/database:/var/lib/mysql -v /path/to/backup:/harbor-migration/backup your-image-name backup
```
- restore from backup file in `/path/to/backup`
```
docker run -ti -v /data/database:/var/lib/mysql -v /path/to/backup:/harbor-migration/backup your-image-name restore
```
- perform database schema upgrade
```docker run -ti -v /data/database:/var/lib/mysql your-image-name up head```
you can use `-v /etc/localtime:/etc/localtime` to sync container timezone with host timezone.
you may change `/data/database` to the mysql volumes path you set in docker-compose.yml.
###migration step
- step 1: stop and remove harbor service
###Migrate Step
- step 1: stop and remove Harbor service
```
docker-compose down
```
- step 2: perform migration operation
- step 3: rebuild newest harbor images and restart service
- step 2: create backup file in `/path/to/backup`
```
docker run -ti --rm -v /data/database:/var/lib/mysql -v /path/to/backup:/harbor-migration/backup migrate-tool backup
```
- step 3: perform database schema upgrade
```docker run -ti --rm -v /data/database:/var/lib/mysql migrate-tool up head```
- step 4: rebuild newest Harbor images and restart service
```
docker-compose build && docker-compose up -d
```
You may change `/data/database` to the mysql volumes path you set in docker-compose.yml.
###Migration operation reference
- You can use `help` to show instruction of Harbor migration
```docker run migrate-tool help```
- You can use `test` to test mysql connection in Harbor migration
```docker run --rm -v /data/database:/var/lib/mysql migrate-tool test```
- You can restore from backup file in `/path/to/backup`
```
docker run -ti --rm -v /data/database:/var/lib/mysql -v /path/to/backup:/harbor-migration/backup migrate-tool restore
```

View File

@ -4,6 +4,7 @@
import sqlalchemy as sa
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, relationship
from sqlalchemy.dialects import mysql
Base = declarative_base()
@ -20,8 +21,8 @@ class User(Base):
reset_uuid = sa.Column(sa.String(40))
salt = sa.Column(sa.String(40))
sysadmin_flag = sa.Column(sa.Integer)
creation_time = sa.Column(sa.DateTime)
update_time = sa.Column(sa.DateTime)
creation_time = sa.Column(mysql.TIMESTAMP)
update_time = sa.Column(mysql.TIMESTAMP)
class Properties(Base):
__tablename__ = 'properties'
@ -35,8 +36,8 @@ class ProjectMember(Base):
project_id = sa.Column(sa.Integer(), primary_key = True)
user_id = sa.Column(sa.Integer(), primary_key = True)
role = sa.Column(sa.Integer(), nullable = False)
creation_time = sa.Column(sa.DateTime(), nullable = True)
update_time = sa.Column(sa.DateTime(), nullable = True)
creation_time = sa.Column(mysql.TIMESTAMP, nullable = True)
update_time = sa.Column(mysql.TIMESTAMP, nullable = True)
sa.ForeignKeyConstraint(['project_id'], [u'project.project_id'], ),
sa.ForeignKeyConstraint(['role'], [u'role.role_id'], ),
sa.ForeignKeyConstraint(['user_id'], [u'user.user_id'], ),
@ -79,8 +80,8 @@ class Project(Base):
project_id = sa.Column(sa.Integer, primary_key=True)
owner_id = sa.Column(sa.ForeignKey(u'user.user_id'), nullable=False, index=True)
name = sa.Column(sa.String(30), nullable=False, unique=True)
creation_time = sa.Column(sa.DateTime)
update_time = sa.Column(sa.DateTime)
creation_time = sa.Column(mysql.TIMESTAMP)
update_time = sa.Column(mysql.TIMESTAMP)
deleted = sa.Column(sa.Integer, nullable=False, server_default=sa.text("'0'"))
public = sa.Column(sa.Integer, nullable=False, server_default=sa.text("'0'"))
owner = relationship(u'User')

View File

@ -27,9 +27,10 @@ branch_labels = None
depends_on = None
from alembic import op
from datetime import datetime
from db_meta import *
from sqlalchemy.dialects import mysql
Session = sessionmaker()
def upgrade():
@ -44,12 +45,9 @@ def upgrade():
session.add(Properties(k='schema_version', v='0.1.1'))
#add column to table user
op.add_column('user', sa.Column('creation_time', sa.DateTime(), nullable=True))
op.add_column('user', sa.Column('creation_time', mysql.TIMESTAMP, nullable=True))
op.add_column('user', sa.Column('sysadmin_flag', sa.Integer(), nullable=True))
op.add_column('user', sa.Column('update_time', sa.DateTime(), nullable=True))
#fill update_time data into table user
session.query(User).update({User.update_time: datetime.now()})
op.add_column('user', sa.Column('update_time', mysql.TIMESTAMP, nullable=True))
#init all sysadmin_flag = 0
session.query(User).update({User.sysadmin_flag: 0})
@ -62,7 +60,7 @@ def upgrade():
for result in join_result:
session.add(ProjectMember(project_id=result.project_role.project_id, \
user_id=result.user_id, role=result.project_role.role_id, \
creation_time=datetime.now(), update_time=datetime.now()))
creation_time=None, update_time=None))
#update sysadmin_flag
sys_admin_result = session.query(UserProjectRole).\
@ -88,11 +86,9 @@ def upgrade():
session.delete(acc)
session.query(Access).update({Access.access_id: Access.access_id - 1})
#add column to table project
op.add_column('project', sa.Column('update_time', sa.DateTime(), nullable=True))
#add column to table project
op.add_column('project', sa.Column('update_time', mysql.TIMESTAMP, nullable=True))
#fill update_time data into table project
session.query(Project).update({Project.update_time: datetime.now()})
session.commit()
def downgrade():

View File

@ -2,6 +2,8 @@ package models
import (
"time"
"github.com/astaxie/beego/validation"
)
const (
@ -42,6 +44,33 @@ type RepPolicy struct {
UpdateTime time.Time `orm:"column(update_time);auto_now" json:"update_time"`
}
// Valid ...
func (r *RepPolicy) Valid(v *validation.Validation) {
if len(r.Name) == 0 {
v.SetError("name", "can not be empty")
}
if len(r.Name) > 256 {
v.SetError("name", "max length is 256")
}
if r.ProjectID <= 0 {
v.SetError("project_id", "invalid")
}
if r.TargetID <= 0 {
v.SetError("target_id", "invalid")
}
if r.Enabled != 0 && r.Enabled != 1 {
v.SetError("enabled", "must be 0 or 1")
}
if len(r.CronStr) > 256 {
v.SetError("cron_str", "max length is 256")
}
}
// RepJob is the model for a replication job, which is the execution unit on job service, currently it is used to transfer/remove
// a repository to/from a remote registry instance.
type RepJob struct {
@ -68,17 +97,42 @@ type RepTarget struct {
UpdateTime time.Time `orm:"column(update_time);auto_now" json:"update_time"`
}
// Valid ...
func (r *RepTarget) Valid(v *validation.Validation) {
if len(r.Name) == 0 {
v.SetError("name", "can not be empty")
}
if len(r.Name) > 64 {
v.SetError("name", "max length is 64")
}
if len(r.URL) == 0 {
v.SetError("endpoint", "can not be empty")
}
if len(r.URL) > 64 {
v.SetError("endpoint", "max length is 64")
}
// password is encoded using base64, the length of this field
// in DB is 64, so the max length in request is 48
if len(r.Password) > 48 {
v.SetError("password", "max length is 48")
}
}
//TableName is required by by beego orm to map RepTarget to table replication_target
func (rt *RepTarget) TableName() string {
func (r *RepTarget) TableName() string {
return "replication_target"
}
//TableName is required by by beego orm to map RepJob to table replication_job
func (rj *RepJob) TableName() string {
func (r *RepJob) TableName() string {
return "replication_job"
}
//TableName is required by by beego orm to map RepPolicy to table replication_policy
func (rp *RepPolicy) TableName() string {
func (r *RepPolicy) TableName() string {
return "replication_policy"
}

View File

@ -62,7 +62,7 @@ jQuery(function(){
return;
}
$.each(data, function(i, e){
var targetId = e.replace(/\//g, "------");
var targetId = e.replace(/\//g, "------").replace(/\./g, "---");
var row = '<div class="panel panel-default" targetId="' + targetId + '">' +
'<div class="panel-heading" role="tab" id="heading' + i + '"+ >' +
'<h4 class="panel-title">' +
@ -105,7 +105,7 @@ jQuery(function(){
$('#accordionRepo').on('show.bs.collapse', function (e) {
$('#accordionRepo .in').collapse('hide');
var targetId = $(e.target).attr("targetId");
var repoName = targetId.replace(/------/g, "/");
var repoName = targetId.replace(/[-]{6}/g, "/").replace(/[-]{3}/g, '.');
new AjaxUtil({
url: "/api/repositories/tags?repo_name=" + repoName,
type: "get",
@ -113,8 +113,8 @@ jQuery(function(){
$('#' + targetId +' table tbody tr').remove();
var row = [];
for(var i in data){
var tagName = data[i]
row.push('<tr><td><a href="#" imageId="' + tagName + '" repoName="' + repoName + '">' + tagName + '</a></td><td><input type="text" style="width:100%" readonly value=" docker pull '+ $("#harborRegUrl").val() +'/'+ repoName + ':' + tagName +'"></td></tr>');
var tagName = data[i];
row.push('<tr><td><a href="#" imageId="' + tagName + '" repoName="' + repoName + '">' + tagName + '</a></td><td><input type="text" style="width:100%" readonly value=" docker pull '+ $("#harborRegUrl").val() +'/'+ repoName + ':' + tagName +'"></td></tr>');
}
$('#' + targetId +' table tbody').append(row.join(""));
$('#' + targetId +' table tbody tr a').on("click", function(e){