diff --git a/AUTHORS b/AUTHORS index e03722c33..f2416e0c0 100644 --- a/AUTHORS +++ b/AUTHORS @@ -5,6 +5,7 @@ Alexey Erkak Allen Heavey Amanda Zhang Benniu Ji +Bin Liu Bobby Zhang Chaofeng Wu Daniel Jiang diff --git a/README.md b/README.md index 479707edd..1d76c2964 100644 --- a/README.md +++ b/README.md @@ -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).     CaiCloud ### Users -MaDaiLiCai +MaDaiLiCai Dianrong ### Supporting Technologies beego Harbor is powered by Beego, an open source framework to build and develop applications in the Go way. diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 000000000..ed4e382c0 --- /dev/null +++ b/ROADMAP.md @@ -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. + diff --git a/api/base.go b/api/base.go index f2529b61e..7a7ff80df 100644 --- a/api/base.go +++ b/api/base.go @@ -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 { diff --git a/api/replication_policy.go b/api/replication_policy.go index 10906ba39..57a7c91a2 100644 --- a/api/replication_policy.go +++ b/api/replication_policy.go @@ -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") diff --git a/api/target.go b/api/target.go index e159b5779..9a9366cf1 100644 --- a/api/target.go +++ b/api/target.go @@ -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) } diff --git a/dao/dao_test.go b/dao/dao_test.go index 7cb897fcd..9e829fa36 100644 --- a/dao/dao_test.go +++ b/dao/dao_test.go @@ -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 { diff --git a/dao/replication_job.go b/dao/replication_job.go index 58d5669a5..dd2b29702 100644 --- a/dao/replication_job.go +++ b/dao/replication_job.go @@ -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, diff --git a/docs/img/dianrong.png b/docs/img/dianrong.png new file mode 100644 index 000000000..08f3cf2c8 Binary files /dev/null and b/docs/img/dianrong.png differ diff --git a/migration/README.md b/migration/README.md index dde8e00c9..ee1b12dcb 100644 --- a/migration/README.md +++ b/migration/README.md @@ -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 + ``` diff --git a/migration/db_meta.py b/migration/db_meta.py index e20dd924c..dcbdd4311 100644 --- a/migration/db_meta.py +++ b/migration/db_meta.py @@ -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') diff --git a/migration/migration_harbor/versions/0_1_1.py b/migration/migration_harbor/versions/0_1_1.py index ecec2cfb3..f3ea874a5 100644 --- a/migration/migration_harbor/versions/0_1_1.py +++ b/migration/migration_harbor/versions/0_1_1.py @@ -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(): diff --git a/models/replication_job.go b/models/replication_job.go index 97fc8443a..7f3f94082 100644 --- a/models/replication_job.go +++ b/models/replication_job.go @@ -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" } diff --git a/static/resources/js/item-detail.js b/static/resources/js/item-detail.js index 6f445f717..c63b1a69b 100644 --- a/static/resources/js/item-detail.js +++ b/static/resources/js/item-detail.js @@ -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 = '
' + '