mirror of
https://github.com/goharbor/harbor
synced 2025-04-15 19:18:14 +00:00
Merge remote-tracking branch 'upstream/job-service' into develop
This commit is contained in:
commit
0147ec8d3b
1
AUTHORS
1
AUTHORS
|
@ -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>
|
||||
|
|
|
@ -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).
|
|||
<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
47
ROADMAP.md
Normal 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.
|
||||
|
26
api/base.go
26
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 {
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
BIN
docs/img/dianrong.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.4 KiB |
|
@ -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
|
||||
```
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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){
|
||||
|
|
Loading…
Reference in New Issue
Block a user