mirror of
https://github.com/goharbor/harbor.git
synced 2024-11-27 20:59:10 +01:00
Merge branch 'new-ui-with-sync-image' of https://github.com/vmware/harbor into new-ui-with-sync-image
This commit is contained in:
commit
36c1e7ae72
2
.gitignore
vendored
2
.gitignore
vendored
@ -3,6 +3,6 @@ Deploy/config/registry/config.yml
|
||||
Deploy/config/ui/env
|
||||
Deploy/config/ui/app.conf
|
||||
Deploy/config/db/env
|
||||
Deploy/harbor.cfg
|
||||
Deploy/config/jobservice/env
|
||||
ui/ui
|
||||
*.pyc
|
||||
|
16
.travis.yml
16
.travis.yml
@ -5,13 +5,25 @@ go:
|
||||
|
||||
go_import_path: github.com/vmware/harbor
|
||||
|
||||
service:
|
||||
- mysql
|
||||
#service:
|
||||
# - mysql
|
||||
|
||||
env: DB_HOST=127.0.0.1 DB_PORT=3306 DB_USR=root DB_PWD=
|
||||
|
||||
install:
|
||||
- sudo apt-get update && sudo apt-get install -y libldap2-dev
|
||||
- sudo apt-get remove mysql-common mysql-server-5.5 mysql-server-core-5.5 mysql-client-5.5 mysql-client-core-5.5
|
||||
- sudo apt-get autoremove
|
||||
- sudo apt-get install libaio1
|
||||
- wget -O mysql-5.6.14.deb http://dev.mysql.com/get/Downloads/MySQL-5.6/mysql-5.6.14-debian6.0-x86_64.deb/from/http://cdn.mysql.com/
|
||||
- sudo dpkg -i mysql-5.6.14.deb
|
||||
- sudo cp /opt/mysql/server-5.6/support-files/mysql.server /etc/init.d/mysql.server
|
||||
- sudo ln -s /opt/mysql/server-5.6/bin/* /usr/bin/
|
||||
- sudo sed -i'' 's/table_cache/table_open_cache/' /etc/mysql/my.cnf
|
||||
- sudo sed -i'' 's/log_slow_queries/slow_query_log/' /etc/mysql/my.cnf
|
||||
- sudo sed -i'' 's/basedir[^=]\+=.*$/basedir = \/opt\/mysql\/server-5.6/' /etc/mysql/my.cnf
|
||||
- sudo /etc/init.d/mysql.server start
|
||||
- mysql --version
|
||||
- go get -d github.com/docker/distribution
|
||||
- go get -d github.com/docker/libtrust
|
||||
- go get -d github.com/go-sql-driver/mysql
|
||||
|
@ -103,6 +103,43 @@ create table access_log (
|
||||
FOREIGN KEY (project_id) REFERENCES project (project_id)
|
||||
);
|
||||
|
||||
create table replication_policy (
|
||||
id int NOT NULL AUTO_INCREMENT,
|
||||
name varchar(256),
|
||||
project_id int NOT NULL,
|
||||
target_id int NOT NULL,
|
||||
enabled tinyint(1) NOT NULL DEFAULT 1,
|
||||
description text,
|
||||
cron_str varchar(256),
|
||||
start_time timestamp NULL,
|
||||
creation_time timestamp default CURRENT_TIMESTAMP,
|
||||
update_time timestamp default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
create table replication_target (
|
||||
id int NOT NULL AUTO_INCREMENT,
|
||||
name varchar(64),
|
||||
url varchar(64),
|
||||
username varchar(40),
|
||||
password varchar(40),
|
||||
creation_time timestamp default CURRENT_TIMESTAMP,
|
||||
update_time timestamp default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
create table replication_job (
|
||||
id int NOT NULL AUTO_INCREMENT,
|
||||
status varchar(64) NOT NULL,
|
||||
policy_id int NOT NULL,
|
||||
repository varchar(256) NOT NULL,
|
||||
operation varchar(64) NOT NULL,
|
||||
tags varchar(16384),
|
||||
creation_time timestamp default CURRENT_TIMESTAMP,
|
||||
update_time timestamp default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
create table properties (
|
||||
k varchar(64) NOT NULL,
|
||||
v varchar(128) NOT NULL,
|
||||
|
@ -55,6 +55,19 @@ services:
|
||||
options:
|
||||
syslog-address: "tcp://127.0.0.1:1514"
|
||||
syslog-tag: "ui"
|
||||
jobservice:
|
||||
build:
|
||||
context: ../
|
||||
dockerfile: Dockerfile.job
|
||||
env_file:
|
||||
- ./config/jobservice/env
|
||||
depends_on:
|
||||
- ui
|
||||
logging:
|
||||
driver: "syslog"
|
||||
options:
|
||||
syslog-address: "tcp://127.0.0.1:1514"
|
||||
syslog-tag: "jobservice"
|
||||
proxy:
|
||||
image: library/nginx:1.9
|
||||
volumes:
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
#The IP address or hostname to access admin UI and registry service.
|
||||
#DO NOT use localhost or 127.0.0.1, because Harbor needs to be accessed by external clients.
|
||||
hostname = reg.mydomain.com
|
||||
hostname = reg.mydomain.org
|
||||
|
||||
#The protocol for accessing the UI and token/notification service, by default it is http.
|
||||
#It can be set to https if ssl is enabled on nginx.
|
||||
@ -38,6 +38,9 @@ self_registration = on
|
||||
#Turn on or off the customize your certicate
|
||||
customize_crt = on
|
||||
|
||||
#Number of job workers in job service, default is 10
|
||||
max_job_workers = 10
|
||||
|
||||
#fill in your certicate message
|
||||
crt_country = CN
|
||||
crt_state = State
|
||||
|
@ -2,6 +2,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import print_function, unicode_literals # We require Python 2.6 or later
|
||||
from string import Template
|
||||
import random
|
||||
import string
|
||||
import os
|
||||
import sys
|
||||
from io import open
|
||||
@ -44,13 +46,15 @@ crt_organization = rcp.get("configuration", "crt_organization")
|
||||
crt_organizationalunit = rcp.get("configuration", "crt_organizationalunit")
|
||||
crt_commonname = rcp.get("configuration", "crt_commonname")
|
||||
crt_email = rcp.get("configuration", "crt_email")
|
||||
max_job_workers = rcp.get("configuration", "max_job_workers")
|
||||
########
|
||||
|
||||
ui_secret = ''.join(random.choice(string.ascii_letters+string.digits) for i in range(16))
|
||||
|
||||
base_dir = os.path.dirname(__file__)
|
||||
config_dir = os.path.join(base_dir, "config")
|
||||
templates_dir = os.path.join(base_dir, "templates")
|
||||
|
||||
|
||||
ui_config_dir = os.path.join(config_dir,"ui")
|
||||
if not os.path.exists(ui_config_dir):
|
||||
os.makedirs(os.path.join(config_dir, "ui"))
|
||||
@ -59,6 +63,10 @@ db_config_dir = os.path.join(config_dir, "db")
|
||||
if not os.path.exists(db_config_dir):
|
||||
os.makedirs(os.path.join(config_dir, "db"))
|
||||
|
||||
job_config_dir = os.path.join(config_dir, "jobservice")
|
||||
if not os.path.exists(job_config_dir):
|
||||
os.makedirs(job_config_dir)
|
||||
|
||||
def render(src, dest, **kw):
|
||||
t = Template(open(src, 'r').read())
|
||||
with open(dest, 'w') as f:
|
||||
@ -69,8 +77,9 @@ ui_conf_env = os.path.join(config_dir, "ui", "env")
|
||||
ui_conf = os.path.join(config_dir, "ui", "app.conf")
|
||||
registry_conf = os.path.join(config_dir, "registry", "config.yml")
|
||||
db_conf_env = os.path.join(config_dir, "db", "env")
|
||||
job_conf_env = os.path.join(config_dir, "jobservice", "env")
|
||||
|
||||
conf_files = [ ui_conf, ui_conf_env, registry_conf, db_conf_env ]
|
||||
conf_files = [ ui_conf, ui_conf_env, registry_conf, db_conf_env, job_conf_env ]
|
||||
def rmdir(cf):
|
||||
for f in cf:
|
||||
if os.path.exists(f):
|
||||
@ -87,7 +96,8 @@ render(os.path.join(templates_dir, "ui", "env"),
|
||||
harbor_admin_password=harbor_admin_password,
|
||||
ldap_url=ldap_url,
|
||||
ldap_basedn=ldap_basedn,
|
||||
self_registration=self_registration)
|
||||
self_registration=self_registration,
|
||||
ui_secret=ui_secret)
|
||||
|
||||
render(os.path.join(templates_dir, "ui", "app.conf"),
|
||||
ui_conf,
|
||||
@ -107,6 +117,13 @@ render(os.path.join(templates_dir, "db", "env"),
|
||||
db_conf_env,
|
||||
db_password=db_password)
|
||||
|
||||
render(os.path.join(templates_dir, "jobservice", "env"),
|
||||
job_conf_env,
|
||||
db_password=db_password,
|
||||
ui_secret=ui_secret,
|
||||
max_job_workers=max_job_workers,
|
||||
ui_url=ui_url)
|
||||
|
||||
def validate_crt_subj(dirty_subj):
|
||||
subj_list = [item for item in dirty_subj.strip().split("/") \
|
||||
if len(item.split("=")) == 2 and len(item.split("=")[1]) > 0]
|
||||
|
9
Deploy/templates/jobservice/env
Normal file
9
Deploy/templates/jobservice/env
Normal file
@ -0,0 +1,9 @@
|
||||
MYSQL_HOST=mysql
|
||||
MYSQL_PORT=3306
|
||||
MYSQL_USR=root
|
||||
MYSQL_PWD=$db_password
|
||||
UI_SECRET=$ui_secret
|
||||
HARBOR_URL=$ui_url
|
||||
MAX_JOB_WORKERS=10
|
||||
LOG_LEVEL=debug
|
||||
GODEBUG=netdns=cgo
|
@ -2,9 +2,8 @@ appname = registry
|
||||
runmode = dev
|
||||
|
||||
[lang]
|
||||
types = en-US|zh-CN|de-DE|ru-RU|ja-JP
|
||||
names = en-US|zh-CN|de-DE|ru-RU|ja-JP
|
||||
|
||||
types = en-US|zh-CN
|
||||
names = English|中文
|
||||
|
||||
[dev]
|
||||
httpport = 80
|
||||
|
@ -10,6 +10,7 @@ HARBOR_URL=$hostname
|
||||
AUTH_MODE=$auth_mode
|
||||
LDAP_URL=$ldap_url
|
||||
LDAP_BASE_DN=$ldap_basedn
|
||||
UI_SECRET=$ui_secret
|
||||
SELF_REGISTRATION=$self_registration
|
||||
LOG_LEVEL=debug
|
||||
GODEBUG=netdns=cgo
|
||||
|
19
Dockerfile.job
Normal file
19
Dockerfile.job
Normal file
@ -0,0 +1,19 @@
|
||||
FROM golang:1.6.2
|
||||
|
||||
MAINTAINER jiangd@vmware.com
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y libldap2-dev \
|
||||
&& rm -r /var/lib/apt/lists/*
|
||||
COPY . /go/src/github.com/vmware/harbor
|
||||
|
||||
WORKDIR /go/src/github.com/vmware/harbor/jobservice
|
||||
|
||||
RUN go get -d github.com/docker/distribution \
|
||||
&& go get -d github.com/docker/libtrust \
|
||||
&& go get -d github.com/go-sql-driver/mysql \
|
||||
&& go build -v -a -o /go/bin/harbor_jobservice \
|
||||
&& chmod u+x /go/bin/harbor_jobservice
|
||||
ADD ./jobservice/conf /go/bin/conf
|
||||
WORKDIR /go/bin/
|
||||
ENTRYPOINT ["/go/bin/harbor_jobservice"]
|
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 {
|
||||
|
||||
|
184
api/jobs/replication.go
Normal file
184
api/jobs/replication.go
Normal file
@ -0,0 +1,184 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"strconv"
|
||||
|
||||
"github.com/vmware/harbor/api"
|
||||
"github.com/vmware/harbor/dao"
|
||||
"github.com/vmware/harbor/job"
|
||||
"github.com/vmware/harbor/job/config"
|
||||
"github.com/vmware/harbor/job/utils"
|
||||
"github.com/vmware/harbor/models"
|
||||
"github.com/vmware/harbor/utils/log"
|
||||
)
|
||||
|
||||
// ReplicationJob handles /api/replicationJobs /api/replicationJobs/:id/log
|
||||
// /api/replicationJobs/actions
|
||||
type ReplicationJob struct {
|
||||
api.BaseAPI
|
||||
}
|
||||
|
||||
// ReplicationReq holds informations of request for /api/replicationJobs
|
||||
type ReplicationReq struct {
|
||||
PolicyID int64 `json:"policy_id"`
|
||||
Repo string `json:"repository"`
|
||||
Operation string `json:"operation"`
|
||||
TagList []string `json:"tags"`
|
||||
}
|
||||
|
||||
// Post creates replication jobs according to the policy.
|
||||
func (rj *ReplicationJob) Post() {
|
||||
var data ReplicationReq
|
||||
rj.DecodeJSONReq(&data)
|
||||
log.Debugf("data: %+v", data)
|
||||
p, err := dao.GetRepPolicy(data.PolicyID)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to get policy, error: %v", err)
|
||||
rj.RenderError(http.StatusInternalServerError, fmt.Sprintf("Failed to get policy, id: %d", data.PolicyID))
|
||||
return
|
||||
}
|
||||
if p == nil {
|
||||
log.Errorf("Policy not found, id: %d", data.PolicyID)
|
||||
rj.RenderError(http.StatusNotFound, fmt.Sprintf("Policy not found, id: %d", data.PolicyID))
|
||||
return
|
||||
}
|
||||
if len(data.Repo) == 0 { // sync all repositories
|
||||
repoList, err := getRepoList(p.ProjectID)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to get repository list, project id: %d, error: %v", p.ProjectID, err)
|
||||
rj.RenderError(http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
log.Debugf("repo list: %v", repoList)
|
||||
for _, repo := range repoList {
|
||||
err := rj.addJob(repo, data.PolicyID, models.RepOpTransfer)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to insert job record, error: %v", err)
|
||||
rj.RenderError(http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
} else { // sync a single repository
|
||||
var op string
|
||||
if len(data.Operation) > 0 {
|
||||
op = data.Operation
|
||||
} else {
|
||||
op = models.RepOpTransfer
|
||||
}
|
||||
err := rj.addJob(data.Repo, data.PolicyID, op, data.TagList...)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to insert job record, error: %v", err)
|
||||
rj.RenderError(http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (rj *ReplicationJob) addJob(repo string, policyID int64, operation string, tags ...string) error {
|
||||
j := models.RepJob{
|
||||
Repository: repo,
|
||||
PolicyID: policyID,
|
||||
Operation: operation,
|
||||
TagList: tags,
|
||||
}
|
||||
log.Debugf("Creating job for repo: %s, policy: %d", repo, policyID)
|
||||
id, err := dao.AddRepJob(j)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Debugf("Send job to scheduler, job id: %d", id)
|
||||
job.Schedule(id)
|
||||
return nil
|
||||
}
|
||||
|
||||
// RepActionReq holds informations of request for /api/replicationJobs/actions
|
||||
type RepActionReq struct {
|
||||
PolicyID int64 `json:"policy_id"`
|
||||
Action string `json:"action"`
|
||||
}
|
||||
|
||||
// HandleAction supports some operations to all the jobs of one policy
|
||||
func (rj *ReplicationJob) HandleAction() {
|
||||
var data RepActionReq
|
||||
rj.DecodeJSONReq(&data)
|
||||
//Currently only support stop action
|
||||
if data.Action != "stop" {
|
||||
log.Errorf("Unrecognized action: %s", data.Action)
|
||||
rj.RenderError(http.StatusBadRequest, fmt.Sprintf("Unrecongized action: %s", data.Action))
|
||||
return
|
||||
}
|
||||
jobs, err := dao.GetRepJobToStop(data.PolicyID)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to get jobs to stop, error: %v", err)
|
||||
rj.RenderError(http.StatusInternalServerError, "Faild to get jobs to stop")
|
||||
return
|
||||
}
|
||||
var jobIDList []int64
|
||||
for _, j := range jobs {
|
||||
jobIDList = append(jobIDList, j.ID)
|
||||
}
|
||||
job.WorkerPool.StopJobs(jobIDList)
|
||||
}
|
||||
|
||||
// GetLog gets logs of the job
|
||||
func (rj *ReplicationJob) GetLog() {
|
||||
idStr := rj.Ctx.Input.Param(":id")
|
||||
jid, err := strconv.ParseInt(idStr, 10, 64)
|
||||
if err != nil {
|
||||
log.Errorf("Error parsing job id: %s, error: %v", idStr, err)
|
||||
rj.RenderError(http.StatusBadRequest, "Invalid job id")
|
||||
return
|
||||
}
|
||||
logFile := utils.GetJobLogPath(jid)
|
||||
rj.Ctx.Output.Download(logFile)
|
||||
}
|
||||
|
||||
// calls the api from UI to get repo list
|
||||
func getRepoList(projectID int64) ([]string, error) {
|
||||
/*
|
||||
uiUser := os.Getenv("UI_USR")
|
||||
if len(uiUser) == 0 {
|
||||
uiUser = "admin"
|
||||
}
|
||||
uiPwd := os.Getenv("UI_PWD")
|
||||
if len(uiPwd) == 0 {
|
||||
uiPwd = "Harbor12345"
|
||||
}
|
||||
*/
|
||||
uiURL := config.LocalHarborURL()
|
||||
client := &http.Client{}
|
||||
req, err := http.NewRequest("GET", uiURL+"/api/repositories?project_id="+strconv.Itoa(int(projectID)), nil)
|
||||
if err != nil {
|
||||
log.Errorf("Error when creating request: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
//req.SetBasicAuth(uiUser, uiPwd)
|
||||
req.AddCookie(&http.Cookie{Name: models.UISecretCookie, Value: config.UISecret()})
|
||||
//dump, err := httputil.DumpRequest(req, true)
|
||||
//log.Debugf("req: %q", dump)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
log.Errorf("Error when calling UI api to get repositories, error: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
log.Errorf("Unexpected status code: %d", resp.StatusCode)
|
||||
dump, _ := httputil.DumpResponse(resp, true)
|
||||
log.Debugf("response: %q", dump)
|
||||
return nil, fmt.Errorf("Unexpected status code when getting repository list: %d", resp.StatusCode)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to read the response body, error: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
var repoList []string
|
||||
err = json.Unmarshal(body, &repoList)
|
||||
return repoList, err
|
||||
}
|
119
api/replication_job.go
Normal file
119
api/replication_job.go
Normal file
@ -0,0 +1,119 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/vmware/harbor/dao"
|
||||
"github.com/vmware/harbor/models"
|
||||
"github.com/vmware/harbor/utils/log"
|
||||
)
|
||||
|
||||
// RepJobAPI handles request to /api/replicationJobs /api/replicationJobs/:id/log
|
||||
type RepJobAPI struct {
|
||||
BaseAPI
|
||||
jobID int64
|
||||
}
|
||||
|
||||
// Prepare validates that whether user has system admin role
|
||||
func (ra *RepJobAPI) Prepare() {
|
||||
uid := ra.ValidateUser()
|
||||
isAdmin, err := dao.IsAdminRole(uid)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to Check if the user is admin, error: %v, uid: %d", err, uid)
|
||||
}
|
||||
if !isAdmin {
|
||||
ra.CustomAbort(http.StatusForbidden, "")
|
||||
}
|
||||
|
||||
idStr := ra.Ctx.Input.Param(":id")
|
||||
if len(idStr) != 0 {
|
||||
id, err := strconv.ParseInt(idStr, 10, 64)
|
||||
if err != nil {
|
||||
ra.CustomAbort(http.StatusBadRequest, "ID is invalid")
|
||||
}
|
||||
ra.jobID = id
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Get gets all the jobs according to the policy
|
||||
func (ra *RepJobAPI) Get() {
|
||||
policyID, err := ra.GetInt64("policy_id")
|
||||
if err != nil {
|
||||
log.Errorf("Failed to get policy id, error: %v", err)
|
||||
ra.RenderError(http.StatusBadRequest, "Invalid policy id")
|
||||
return
|
||||
}
|
||||
jobs, err := dao.GetRepJobByPolicy(policyID)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to query job from db, error: %v", err)
|
||||
ra.RenderError(http.StatusInternalServerError, "Failed to query job")
|
||||
return
|
||||
}
|
||||
ra.Data["json"] = jobs
|
||||
ra.ServeJSON()
|
||||
}
|
||||
|
||||
// Delete ...
|
||||
func (ra *RepJobAPI) Delete() {
|
||||
if ra.jobID == 0 {
|
||||
ra.CustomAbort(http.StatusBadRequest, "id is nil")
|
||||
}
|
||||
|
||||
job, err := dao.GetRepJob(ra.jobID)
|
||||
if err != nil {
|
||||
log.Errorf("failed to get job %d: %v", ra.jobID, err)
|
||||
ra.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
||||
}
|
||||
|
||||
if job.Status == models.JobPending || job.Status == models.JobRunning {
|
||||
ra.CustomAbort(http.StatusBadRequest, fmt.Sprintf("job is %s, can not be deleted", job.Status))
|
||||
}
|
||||
|
||||
if err = dao.DeleteRepJob(ra.jobID); err != nil {
|
||||
log.Errorf("failed to deleted job %d: %v", ra.jobID, err)
|
||||
ra.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
||||
}
|
||||
}
|
||||
|
||||
// GetLog ...
|
||||
func (ra *RepJobAPI) GetLog() {
|
||||
if ra.jobID == 0 {
|
||||
ra.CustomAbort(http.StatusBadRequest, "id is nil")
|
||||
}
|
||||
|
||||
resp, err := http.Get(buildJobLogURL(strconv.FormatInt(ra.jobID, 10)))
|
||||
if err != nil {
|
||||
log.Errorf("failed to get log for job %d: %v", ra.jobID, err)
|
||||
ra.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
||||
}
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
for key, values := range resp.Header {
|
||||
for _, value := range values {
|
||||
ra.Ctx.ResponseWriter.Header().Set(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err = io.Copy(ra.Ctx.ResponseWriter, resp.Body); err != nil {
|
||||
log.Errorf("failed to write log to response; %v", err)
|
||||
ra.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
b, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Errorf("failed to read reponse body: %v", err)
|
||||
ra.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
||||
}
|
||||
|
||||
ra.CustomAbort(resp.StatusCode, string(b))
|
||||
}
|
||||
|
||||
//TODO:add Post handler to call job service API to submit jobs by policy
|
165
api/replication_policy.go
Normal file
165
api/replication_policy.go
Normal file
@ -0,0 +1,165 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/vmware/harbor/dao"
|
||||
"github.com/vmware/harbor/models"
|
||||
"github.com/vmware/harbor/utils/log"
|
||||
)
|
||||
|
||||
// RepPolicyAPI handles /api/replicationPolicies /api/replicationPolicies/:id/enablement
|
||||
type RepPolicyAPI struct {
|
||||
BaseAPI
|
||||
policyID int64
|
||||
policy *models.RepPolicy
|
||||
}
|
||||
|
||||
// Prepare validates whether the user has system admin role
|
||||
// and parsed the policy ID if it exists
|
||||
func (pa *RepPolicyAPI) Prepare() {
|
||||
uid := pa.ValidateUser()
|
||||
var err error
|
||||
isAdmin, err := dao.IsAdminRole(uid)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to Check if the user is admin, error: %v, uid: %d", err, uid)
|
||||
}
|
||||
if !isAdmin {
|
||||
pa.CustomAbort(http.StatusForbidden, "")
|
||||
}
|
||||
idStr := pa.Ctx.Input.Param(":id")
|
||||
if len(idStr) > 0 {
|
||||
pa.policyID, err = strconv.ParseInt(idStr, 10, 64)
|
||||
if err != nil {
|
||||
log.Errorf("Error parsing policy id: %s, error: %v", idStr, err)
|
||||
pa.CustomAbort(http.StatusBadRequest, "invalid policy id")
|
||||
}
|
||||
p, err := dao.GetRepPolicy(pa.policyID)
|
||||
if err != nil {
|
||||
log.Errorf("Error occurred in GetRepPolicy, error: %v", err)
|
||||
pa.CustomAbort(http.StatusInternalServerError, "Internal error.")
|
||||
}
|
||||
if p == nil {
|
||||
pa.CustomAbort(http.StatusNotFound, fmt.Sprintf("policy does not exist, id: %v", pa.policyID))
|
||||
}
|
||||
pa.policy = p
|
||||
}
|
||||
}
|
||||
|
||||
// Get gets all the policies according to the project
|
||||
func (pa *RepPolicyAPI) Get() {
|
||||
projectID, err := pa.GetInt64("project_id")
|
||||
if err != nil {
|
||||
log.Errorf("Failed to get project id, error: %v", err)
|
||||
pa.RenderError(http.StatusBadRequest, "Invalid project id")
|
||||
return
|
||||
}
|
||||
policies, err := dao.GetRepPolicyByProject(projectID)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to query policies from db, error: %v", err)
|
||||
pa.RenderError(http.StatusInternalServerError, "Failed to query policies")
|
||||
return
|
||||
}
|
||||
pa.Data["json"] = policies
|
||||
pa.ServeJSON()
|
||||
}
|
||||
|
||||
// Post creates a policy, and if it is enbled, the replication will be triggered right now.
|
||||
func (pa *RepPolicyAPI) Post() {
|
||||
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")
|
||||
return
|
||||
}
|
||||
|
||||
if policy.Enabled == 1 {
|
||||
go func() {
|
||||
if err := TriggerReplication(pid, "", nil, models.RepOpTransfer); err != nil {
|
||||
log.Errorf("failed to trigger replication of %d: %v", pid, err)
|
||||
} else {
|
||||
log.Infof("replication of %d triggered", pid)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
pa.Redirect(http.StatusCreated, strconv.FormatInt(pid, 10))
|
||||
}
|
||||
|
||||
type enablementReq struct {
|
||||
Enabled int `json:"enabled"`
|
||||
}
|
||||
|
||||
// UpdateEnablement changes the enablement of the policy
|
||||
func (pa *RepPolicyAPI) UpdateEnablement() {
|
||||
e := enablementReq{}
|
||||
pa.DecodeJSONReq(&e)
|
||||
if e.Enabled != 0 && e.Enabled != 1 {
|
||||
pa.RenderError(http.StatusBadRequest, "invalid enabled value")
|
||||
return
|
||||
}
|
||||
|
||||
if pa.policy.Enabled == e.Enabled {
|
||||
return
|
||||
}
|
||||
|
||||
if err := dao.UpdateRepPolicyEnablement(pa.policyID, e.Enabled); err != nil {
|
||||
log.Errorf("Failed to update policy enablement in DB, error: %v", err)
|
||||
pa.RenderError(http.StatusInternalServerError, "Internal Error")
|
||||
return
|
||||
}
|
||||
|
||||
if e.Enabled == 1 {
|
||||
go func() {
|
||||
if err := TriggerReplication(pa.policyID, "", nil, models.RepOpTransfer); err != nil {
|
||||
log.Errorf("failed to trigger replication of %d: %v", pa.policyID, err)
|
||||
} else {
|
||||
log.Infof("replication of %d triggered", pa.policyID)
|
||||
}
|
||||
}()
|
||||
} else {
|
||||
go func() {
|
||||
if err := postReplicationAction(pa.policyID, "stop"); err != nil {
|
||||
log.Errorf("failed to stop replication of %d: %v", pa.policyID, err)
|
||||
} else {
|
||||
log.Infof("try to stop replication of %d", pa.policyID)
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
@ -27,11 +27,14 @@ import (
|
||||
"github.com/docker/distribution/manifest/schema1"
|
||||
"github.com/vmware/harbor/dao"
|
||||
"github.com/vmware/harbor/models"
|
||||
"github.com/vmware/harbor/service/cache"
|
||||
svc_utils "github.com/vmware/harbor/service/utils"
|
||||
"github.com/vmware/harbor/utils/log"
|
||||
"github.com/vmware/harbor/utils/registry"
|
||||
|
||||
registry_error "github.com/vmware/harbor/utils/registry/error"
|
||||
|
||||
"github.com/vmware/harbor/utils/registry/auth"
|
||||
"github.com/vmware/harbor/utils/registry/errors"
|
||||
)
|
||||
|
||||
// RepositoryAPI handles request to /api/repositories /api/repositories/tags /api/repositories/manifests, the parm has to be put
|
||||
@ -62,7 +65,13 @@ func (ra *RepositoryAPI) Get() {
|
||||
}
|
||||
|
||||
if p.Public == 0 {
|
||||
userID := ra.ValidateUser()
|
||||
var userID int
|
||||
|
||||
if svc_utils.VerifySecret(ra.Ctx.Request) {
|
||||
userID = 1
|
||||
} else {
|
||||
userID = ra.ValidateUser()
|
||||
}
|
||||
|
||||
if !checkProjectPermission(userID, projectID) {
|
||||
ra.RenderError(http.StatusForbidden, "")
|
||||
@ -70,7 +79,7 @@ func (ra *RepositoryAPI) Get() {
|
||||
}
|
||||
}
|
||||
|
||||
repoList, err := svc_utils.GetRepoFromCache()
|
||||
repoList, err := cache.GetRepoFromCache()
|
||||
if err != nil {
|
||||
log.Errorf("Failed to get repo from cache, error: %v", err)
|
||||
ra.RenderError(http.StatusInternalServerError, "internal sever error")
|
||||
@ -117,14 +126,12 @@ func (ra *RepositoryAPI) Delete() {
|
||||
if len(tag) == 0 {
|
||||
tagList, err := rc.ListTag()
|
||||
if err != nil {
|
||||
e, ok := errors.ParseError(err)
|
||||
if ok {
|
||||
log.Info(e)
|
||||
ra.CustomAbort(e.StatusCode, e.Message)
|
||||
} else {
|
||||
log.Error(err)
|
||||
ra.CustomAbort(http.StatusInternalServerError, "internal error")
|
||||
if regErr, ok := err.(*registry_error.Error); ok {
|
||||
ra.CustomAbort(regErr.StatusCode, regErr.Detail)
|
||||
}
|
||||
|
||||
log.Errorf("error occurred while listing tags of %s: %v", repoName, err)
|
||||
ra.CustomAbort(http.StatusInternalServerError, "internal error")
|
||||
}
|
||||
tags = append(tags, tagList...)
|
||||
} else {
|
||||
@ -133,20 +140,21 @@ func (ra *RepositoryAPI) Delete() {
|
||||
|
||||
for _, t := range tags {
|
||||
if err := rc.DeleteTag(t); err != nil {
|
||||
e, ok := errors.ParseError(err)
|
||||
if ok {
|
||||
ra.CustomAbort(e.StatusCode, e.Message)
|
||||
} else {
|
||||
log.Error(err)
|
||||
ra.CustomAbort(http.StatusInternalServerError, "internal error")
|
||||
if regErr, ok := err.(*registry_error.Error); ok {
|
||||
ra.CustomAbort(regErr.StatusCode, regErr.Detail)
|
||||
}
|
||||
|
||||
log.Errorf("error occurred while deleting tags of %s: %v", repoName, err)
|
||||
ra.CustomAbort(http.StatusInternalServerError, "internal error")
|
||||
}
|
||||
log.Infof("delete tag: %s %s", repoName, t)
|
||||
go TriggerReplicationByRepository(repoName, []string{t}, models.RepOpDelete)
|
||||
|
||||
}
|
||||
|
||||
go func() {
|
||||
log.Debug("refreshing catalog cache")
|
||||
if err := svc_utils.RefreshCatalogCache(); err != nil {
|
||||
if err := cache.RefreshCatalogCache(); err != nil {
|
||||
log.Errorf("error occurred while refresh catalog cache: %v", err)
|
||||
}
|
||||
}()
|
||||
@ -175,13 +183,12 @@ func (ra *RepositoryAPI) GetTags() {
|
||||
|
||||
ts, err := rc.ListTag()
|
||||
if err != nil {
|
||||
e, ok := errors.ParseError(err)
|
||||
if ok {
|
||||
ra.CustomAbort(e.StatusCode, e.Message)
|
||||
} else {
|
||||
log.Error(err)
|
||||
ra.CustomAbort(http.StatusInternalServerError, "internal error")
|
||||
if regErr, ok := err.(*registry_error.Error); ok {
|
||||
ra.CustomAbort(regErr.StatusCode, regErr.Detail)
|
||||
}
|
||||
|
||||
log.Errorf("error occurred while listing tags of %s: %v", repoName, err)
|
||||
ra.CustomAbort(http.StatusInternalServerError, "internal error")
|
||||
}
|
||||
|
||||
tags = append(tags, ts...)
|
||||
@ -212,13 +219,12 @@ func (ra *RepositoryAPI) GetManifests() {
|
||||
mediaTypes := []string{schema1.MediaTypeManifest}
|
||||
_, _, payload, err := rc.PullManifest(tag, mediaTypes)
|
||||
if err != nil {
|
||||
e, ok := errors.ParseError(err)
|
||||
if ok {
|
||||
ra.CustomAbort(e.StatusCode, e.Message)
|
||||
} else {
|
||||
log.Error(err)
|
||||
ra.CustomAbort(http.StatusInternalServerError, "internal error")
|
||||
if regErr, ok := err.(*registry_error.Error); ok {
|
||||
ra.CustomAbort(regErr.StatusCode, regErr.Detail)
|
||||
}
|
||||
|
||||
log.Errorf("error occurred while getting manifest of %s:%s: %v", repoName, tag, err)
|
||||
ra.CustomAbort(http.StatusInternalServerError, "internal error")
|
||||
}
|
||||
mani := models.Manifest{}
|
||||
err = json.Unmarshal(payload, &mani)
|
||||
|
@ -22,7 +22,7 @@ import (
|
||||
|
||||
"github.com/vmware/harbor/dao"
|
||||
"github.com/vmware/harbor/models"
|
||||
svc_utils "github.com/vmware/harbor/service/utils"
|
||||
"github.com/vmware/harbor/service/cache"
|
||||
"github.com/vmware/harbor/utils"
|
||||
"github.com/vmware/harbor/utils/log"
|
||||
)
|
||||
@ -85,7 +85,7 @@ func (s *SearchAPI) Get() {
|
||||
}
|
||||
}
|
||||
|
||||
repositories, err2 := svc_utils.GetRepoFromCache()
|
||||
repositories, err2 := cache.GetRepoFromCache()
|
||||
if err2 != nil {
|
||||
log.Errorf("Failed to get repos from cache, error: %v", err2)
|
||||
s.CustomAbort(http.StatusInternalServerError, "Failed to get repositories search result")
|
||||
|
@ -21,7 +21,7 @@ import (
|
||||
|
||||
"github.com/vmware/harbor/dao"
|
||||
"github.com/vmware/harbor/models"
|
||||
svc_utils "github.com/vmware/harbor/service/utils"
|
||||
"github.com/vmware/harbor/service/cache"
|
||||
"github.com/vmware/harbor/utils/log"
|
||||
)
|
||||
|
||||
@ -88,7 +88,7 @@ func (s *StatisticAPI) Get() {
|
||||
|
||||
//getReposByProject returns repo numbers of specified project
|
||||
func getRepoCountByProject(projectName string) int {
|
||||
repoList, err := svc_utils.GetRepoFromCache()
|
||||
repoList, err := cache.GetRepoFromCache()
|
||||
if err != nil {
|
||||
log.Errorf("Failed to get repo from cache, error: %v", err)
|
||||
return 0
|
||||
@ -107,7 +107,7 @@ func getRepoCountByProject(projectName string) int {
|
||||
|
||||
//getTotalRepoCount returns total repo count
|
||||
func getTotalRepoCount() int {
|
||||
repoList, err := svc_utils.GetRepoFromCache()
|
||||
repoList, err := cache.GetRepoFromCache()
|
||||
if err != nil {
|
||||
log.Errorf("Failed to get repo from cache, error: %v", err)
|
||||
return 0
|
||||
|
267
api/target.go
Normal file
267
api/target.go
Normal file
@ -0,0 +1,267 @@
|
||||
/*
|
||||
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 api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"github.com/vmware/harbor/dao"
|
||||
"github.com/vmware/harbor/models"
|
||||
"github.com/vmware/harbor/utils"
|
||||
"github.com/vmware/harbor/utils/log"
|
||||
registry_util "github.com/vmware/harbor/utils/registry"
|
||||
"github.com/vmware/harbor/utils/registry/auth"
|
||||
registry_error "github.com/vmware/harbor/utils/registry/error"
|
||||
)
|
||||
|
||||
// TargetAPI handles request to /api/targets/ping /api/targets/{}
|
||||
type TargetAPI struct {
|
||||
BaseAPI
|
||||
}
|
||||
|
||||
// Prepare validates the user
|
||||
func (t *TargetAPI) Prepare() {
|
||||
userID := t.ValidateUser()
|
||||
isSysAdmin, err := dao.IsAdminRole(userID)
|
||||
if err != nil {
|
||||
log.Errorf("error occurred in IsAdminRole: %v", err)
|
||||
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
||||
}
|
||||
|
||||
if !isSysAdmin {
|
||||
t.CustomAbort(http.StatusForbidden, http.StatusText(http.StatusForbidden))
|
||||
}
|
||||
}
|
||||
|
||||
// Ping validates whether the target is reachable and whether the credential is valid
|
||||
func (t *TargetAPI) Ping() {
|
||||
var endpoint, username, password string
|
||||
|
||||
idStr := t.GetString("id")
|
||||
if len(idStr) != 0 {
|
||||
id, err := strconv.ParseInt(idStr, 10, 64)
|
||||
if err != nil {
|
||||
t.CustomAbort(http.StatusBadRequest, fmt.Sprintf("id %s is invalid", idStr))
|
||||
}
|
||||
|
||||
target, 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 == nil {
|
||||
t.CustomAbort(http.StatusNotFound, http.StatusText(http.StatusNotFound))
|
||||
}
|
||||
|
||||
endpoint = target.URL
|
||||
username = target.Username
|
||||
password = target.Password
|
||||
|
||||
if len(password) != 0 {
|
||||
password, err = utils.ReversibleDecrypt(password)
|
||||
if err != nil {
|
||||
log.Errorf("failed to decrypt password: %v", err)
|
||||
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
endpoint = t.GetString("endpoint")
|
||||
if len(endpoint) == 0 {
|
||||
t.CustomAbort(http.StatusBadRequest, "id or endpoint is needed")
|
||||
}
|
||||
|
||||
username = t.GetString("username")
|
||||
password = t.GetString("password")
|
||||
}
|
||||
|
||||
credential := auth.NewBasicAuthCredential(username, password)
|
||||
registry, err := registry_util.NewRegistryWithCredential(endpoint, credential)
|
||||
if err != nil {
|
||||
// timeout, dns resolve error, connection refused, etc.
|
||||
if urlErr, ok := err.(*url.Error); ok {
|
||||
if netErr, ok := urlErr.Err.(net.Error); ok {
|
||||
t.CustomAbort(http.StatusBadRequest, netErr.Error())
|
||||
}
|
||||
|
||||
t.CustomAbort(http.StatusBadRequest, urlErr.Error())
|
||||
}
|
||||
|
||||
log.Errorf("failed to create registry client: %#v", err)
|
||||
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
||||
}
|
||||
|
||||
if err = registry.Ping(); err != nil {
|
||||
if regErr, ok := err.(*registry_error.Error); ok {
|
||||
t.CustomAbort(regErr.StatusCode, regErr.Detail)
|
||||
}
|
||||
|
||||
log.Errorf("failed to ping registry %s: %v", registry.Endpoint.String(), err)
|
||||
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
||||
}
|
||||
}
|
||||
|
||||
// Get ...
|
||||
func (t *TargetAPI) Get() {
|
||||
id := t.getIDFromURL()
|
||||
// list targets
|
||||
if id == 0 {
|
||||
targets, err := dao.GetAllRepTargets()
|
||||
if err != nil {
|
||||
log.Errorf("failed to get all targets: %v", err)
|
||||
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
||||
}
|
||||
|
||||
for _, target := range targets {
|
||||
target.Password = ""
|
||||
}
|
||||
|
||||
t.Data["json"] = targets
|
||||
t.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
target, 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 == nil {
|
||||
t.CustomAbort(http.StatusNotFound, http.StatusText(http.StatusNotFound))
|
||||
}
|
||||
|
||||
if len(target.Password) != 0 {
|
||||
pwd, err := utils.ReversibleDecrypt(target.Password)
|
||||
if err != nil {
|
||||
log.Errorf("failed to decrypt password: %v", err)
|
||||
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
||||
}
|
||||
target.Password = pwd
|
||||
}
|
||||
|
||||
t.Data["json"] = target
|
||||
t.ServeJSON()
|
||||
}
|
||||
|
||||
// Post ...
|
||||
func (t *TargetAPI) Post() {
|
||||
target := &models.RepTarget{}
|
||||
t.DecodeJSONReqAndValidate(target)
|
||||
|
||||
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 {
|
||||
target.Password = utils.ReversibleEncrypt(target.Password)
|
||||
}
|
||||
|
||||
id, err := dao.AddRepTarget(*target)
|
||||
if err != nil {
|
||||
log.Errorf("failed to add target: %v", err)
|
||||
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
||||
}
|
||||
|
||||
t.Redirect(http.StatusCreated, strconv.FormatInt(id, 10))
|
||||
}
|
||||
|
||||
// Put ...
|
||||
func (t *TargetAPI) Put() {
|
||||
id := t.getIDFromURL()
|
||||
if id == 0 {
|
||||
t.CustomAbort(http.StatusBadRequest, "id can not be empty or 0")
|
||||
}
|
||||
|
||||
target := &models.RepTarget{}
|
||||
t.DecodeJSONReqAndValidate(target)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
if err := dao.UpdateRepTarget(*target); err != nil {
|
||||
log.Errorf("failed to update target %d: %v", id, err)
|
||||
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
||||
}
|
||||
}
|
||||
|
||||
// Delete ...
|
||||
func (t *TargetAPI) Delete() {
|
||||
id := t.getIDFromURL()
|
||||
if id == 0 {
|
||||
t.CustomAbort(http.StatusBadRequest, http.StatusText(http.StatusBadRequest))
|
||||
}
|
||||
|
||||
target, 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 == nil {
|
||||
t.CustomAbort(http.StatusNotFound, http.StatusText(http.StatusNotFound))
|
||||
}
|
||||
|
||||
if err = dao.DeleteRepTarget(id); err != nil {
|
||||
log.Errorf("failed to delete target %d: %v", id, err)
|
||||
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TargetAPI) getIDFromURL() int64 {
|
||||
idStr := t.Ctx.Input.Param(":id")
|
||||
if len(idStr) == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
id, err := strconv.ParseInt(idStr, 10, 64)
|
||||
if err != nil {
|
||||
t.CustomAbort(http.StatusBadRequest, "invalid ID in request URL")
|
||||
}
|
||||
|
||||
return id
|
||||
}
|
147
api/utils.go
147
api/utils.go
@ -16,6 +16,14 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/vmware/harbor/dao"
|
||||
"github.com/vmware/harbor/models"
|
||||
"github.com/vmware/harbor/utils/log"
|
||||
@ -81,3 +89,142 @@ func checkUserExists(name string) int {
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// TriggerReplication triggers the replication according to the policy
|
||||
func TriggerReplication(policyID int64, repository string,
|
||||
tags []string, operation string) error {
|
||||
data := struct {
|
||||
PolicyID int64 `json:"policy_id"`
|
||||
Repo string `json:"repository"`
|
||||
Operation string `json:"operation"`
|
||||
TagList []string `json:"tags"`
|
||||
}{
|
||||
PolicyID: policyID,
|
||||
Repo: repository,
|
||||
TagList: tags,
|
||||
Operation: operation,
|
||||
}
|
||||
|
||||
b, err := json.Marshal(&data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
url := buildReplicationURL()
|
||||
|
||||
resp, err := http.DefaultClient.Post(url, "application/json", bytes.NewBuffer(b))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return nil
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
b, err = ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return fmt.Errorf("%d %s", resp.StatusCode, string(b))
|
||||
}
|
||||
|
||||
// GetPoliciesByRepository returns policies according the repository
|
||||
func GetPoliciesByRepository(repository string) ([]*models.RepPolicy, error) {
|
||||
repository = strings.TrimSpace(repository)
|
||||
repository = strings.TrimRight(repository, "/")
|
||||
projectName := repository[:strings.LastIndex(repository, "/")]
|
||||
|
||||
project, err := dao.GetProjectByName(projectName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
policies, err := dao.GetRepPolicyByProject(project.ProjectID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return policies, nil
|
||||
}
|
||||
|
||||
// TriggerReplicationByRepository triggers the replication according to the repository
|
||||
func TriggerReplicationByRepository(repository string, tags []string, operation string) {
|
||||
policies, err := GetPoliciesByRepository(repository)
|
||||
if err != nil {
|
||||
log.Errorf("failed to get policies for repository %s: %v", repository, err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, policy := range policies {
|
||||
if err := TriggerReplication(policy.ID, repository, tags, operation); err != nil {
|
||||
log.Errorf("failed to trigger replication of policy %d for %s: %v", policy.ID, repository, err)
|
||||
} else {
|
||||
log.Infof("replication of policy %d for %s triggered", policy.ID, repository)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func postReplicationAction(policyID int64, acton string) error {
|
||||
data := struct {
|
||||
PolicyID int64 `json:"policy_id"`
|
||||
Action string `json:"action"`
|
||||
}{
|
||||
PolicyID: policyID,
|
||||
Action: acton,
|
||||
}
|
||||
|
||||
b, err := json.Marshal(&data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
url := buildReplicationActionURL()
|
||||
|
||||
resp, err := http.DefaultClient.Post(url, "application/json", bytes.NewBuffer(b))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return nil
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
b, err = ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return fmt.Errorf("%d %s", resp.StatusCode, string(b))
|
||||
}
|
||||
|
||||
func buildReplicationURL() string {
|
||||
url := getJobServiceURL()
|
||||
return fmt.Sprintf("%s/api/jobs/replication", url)
|
||||
}
|
||||
|
||||
func buildJobLogURL(jobID string) string {
|
||||
url := getJobServiceURL()
|
||||
return fmt.Sprintf("%s/api/jobs/replication/%s/log", url, jobID)
|
||||
}
|
||||
|
||||
func buildReplicationActionURL() string {
|
||||
url := getJobServiceURL()
|
||||
return fmt.Sprintf("%s/api/jobs/replication/actions", url)
|
||||
}
|
||||
|
||||
func getJobServiceURL() string {
|
||||
url := os.Getenv("JOB_SERVICE_URL")
|
||||
url = strings.TrimSpace(url)
|
||||
url = strings.TrimRight(url, "/")
|
||||
|
||||
if len(url) == 0 {
|
||||
url = "http://jobservice"
|
||||
}
|
||||
|
||||
return url
|
||||
}
|
||||
|
@ -65,6 +65,7 @@ func GenerateRandomString() (string, error) {
|
||||
|
||||
//InitDB initializes the database
|
||||
func InitDB() {
|
||||
// orm.Debug = true
|
||||
orm.RegisterDriver("mysql", orm.DRMySQL)
|
||||
addr := os.Getenv("MYSQL_HOST")
|
||||
port := os.Getenv("MYSQL_PORT")
|
||||
|
438
dao/dao_test.go
438
dao/dao_test.go
@ -20,20 +20,18 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/vmware/harbor/utils/log"
|
||||
|
||||
"github.com/vmware/harbor/models"
|
||||
|
||||
"github.com/astaxie/beego/orm"
|
||||
"github.com/vmware/harbor/models"
|
||||
"github.com/vmware/harbor/utils/log"
|
||||
)
|
||||
|
||||
func execUpdate(o orm.Ormer, sql string, params interface{}) error {
|
||||
func execUpdate(o orm.Ormer, sql string, params ...interface{}) error {
|
||||
p, err := o.Raw(sql).Prepare()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer p.Close()
|
||||
_, err = p.Exec(params)
|
||||
_, err = p.Exec(params...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -97,6 +95,19 @@ func clearUp(username string) {
|
||||
o.Rollback()
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
err = execUpdate(o, `delete from replication_job where id < 99`)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
err = execUpdate(o, `delete from replication_policy where id < 99`)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
err = execUpdate(o, `delete from replication_target where id < 99`)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
o.Commit()
|
||||
}
|
||||
|
||||
@ -716,6 +727,421 @@ func TestDeleteUser(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
var targetID, policyID, policyID2, policyID3, jobID, jobID2, jobID3 int64
|
||||
|
||||
func TestAddRepTarget(t *testing.T) {
|
||||
target := models.RepTarget{
|
||||
URL: "127.0.0.1:5000",
|
||||
Username: "admin",
|
||||
Password: "admin",
|
||||
}
|
||||
//_, err := AddRepTarget(target)
|
||||
id, err := AddRepTarget(target)
|
||||
t.Logf("added target, id: %d", id)
|
||||
if err != nil {
|
||||
t.Errorf("Error occurred in AddRepTarget: %v", err)
|
||||
} else {
|
||||
targetID = id
|
||||
}
|
||||
id2 := id + 99
|
||||
tgt, err := GetRepTarget(id2)
|
||||
if err != nil {
|
||||
t.Errorf("Error occurred in GetTarget: %v, id: %d", err, id2)
|
||||
}
|
||||
if tgt != nil {
|
||||
t.Errorf("There should not be a target with id: %d", id2)
|
||||
}
|
||||
tgt, err = GetRepTarget(id)
|
||||
if err != nil {
|
||||
t.Errorf("Error occurred in GetTarget: %v, id: %d", err, id)
|
||||
}
|
||||
if tgt == nil {
|
||||
t.Errorf("Unable to find a target with id: %d", id)
|
||||
}
|
||||
if tgt.URL != "127.0.0.1:5000" {
|
||||
t.Errorf("Unexpected url in target: %s, expected 127.0.0.1:5000", tgt.URL)
|
||||
}
|
||||
if tgt.Username != "admin" {
|
||||
t.Errorf("Unexpected username in target: %s, expected admin", tgt.Username)
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
Enabled: 1,
|
||||
TargetID: targetID,
|
||||
Description: "whatever",
|
||||
Name: "mypolicy",
|
||||
}
|
||||
id, err := AddRepPolicy(policy)
|
||||
t.Logf("added policy, id: %d", id)
|
||||
if err != nil {
|
||||
t.Errorf("Error occurred in AddRepPolicy: %v", err)
|
||||
} else {
|
||||
policyID = id
|
||||
}
|
||||
p, err := GetRepPolicy(id)
|
||||
if err != nil {
|
||||
t.Errorf("Error occurred in GetPolicy: %v, id: %d", err, id)
|
||||
}
|
||||
if p == nil {
|
||||
t.Errorf("Unable to find a policy with id: %d", id)
|
||||
}
|
||||
|
||||
if p.Name != "mypolicy" || p.TargetID != targetID || p.Enabled != 1 || p.Description != "whatever" {
|
||||
t.Errorf("The data does not match, expected: Name: mypolicy, TargetID: %d, Enabled: 1, Description: whatever;\n result: Name: %s, TargetID: %d, Enabled: %d, Description: %s",
|
||||
targetID, p.Name, p.TargetID, p.Enabled, p.Description)
|
||||
}
|
||||
var tm = time.Now().AddDate(0, 0, -1)
|
||||
if !p.StartTime.After(tm) {
|
||||
t.Errorf("Unexpected start_time: %v", p.StartTime)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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 {
|
||||
t.Errorf("Failed to disable policy, id: %d", policyID)
|
||||
}
|
||||
p, err := GetRepPolicy(policyID)
|
||||
if err != nil {
|
||||
t.Errorf("Error occurred in GetPolicy: %v, id: %d", err, policyID)
|
||||
}
|
||||
if p == nil {
|
||||
t.Errorf("Unable to find a policy with id: %d", policyID)
|
||||
}
|
||||
if p.Enabled == 1 {
|
||||
t.Errorf("The Enabled value of replication policy is still 1 after disabled, id: %d", policyID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnableRepPolicy(t *testing.T) {
|
||||
err := EnableRepPolicy(policyID)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to disable policy, id: %d", policyID)
|
||||
}
|
||||
p, err := GetRepPolicy(policyID)
|
||||
if err != nil {
|
||||
t.Errorf("Error occurred in GetPolicy: %v, id: %d", err, policyID)
|
||||
}
|
||||
if p == nil {
|
||||
t.Errorf("Unable to find a policy with id: %d", policyID)
|
||||
}
|
||||
if p.Enabled == 0 {
|
||||
t.Errorf("The Enabled value of replication policy is still 0 after disabled, id: %d", policyID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddRepPolicy2(t *testing.T) {
|
||||
policy2 := models.RepPolicy{
|
||||
ProjectID: 3,
|
||||
Enabled: 0,
|
||||
TargetID: 3,
|
||||
Description: "whatever",
|
||||
Name: "mypolicy",
|
||||
}
|
||||
policyID2, err := AddRepPolicy(policy2)
|
||||
t.Logf("added policy, id: %d", policyID2)
|
||||
if err != nil {
|
||||
t.Errorf("Error occurred in AddRepPolicy: %v", err)
|
||||
}
|
||||
p, err := GetRepPolicy(policyID2)
|
||||
if err != nil {
|
||||
t.Errorf("Error occurred in GetPolicy: %v, id: %d", err, policyID2)
|
||||
}
|
||||
if p == nil {
|
||||
t.Errorf("Unable to find a policy with id: %d", policyID2)
|
||||
}
|
||||
var tm time.Time
|
||||
if p.StartTime.After(tm) {
|
||||
t.Errorf("Unexpected start_time: %v", p.StartTime)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddRepJob(t *testing.T) {
|
||||
job := models.RepJob{
|
||||
Repository: "library/ubuntu",
|
||||
PolicyID: policyID,
|
||||
Operation: "transfer",
|
||||
TagList: []string{"12.01", "14.04", "latest"},
|
||||
}
|
||||
id, err := AddRepJob(job)
|
||||
if err != nil {
|
||||
t.Errorf("Error occurred in AddRepJob: %v", err)
|
||||
return
|
||||
}
|
||||
jobID = id
|
||||
|
||||
j, err := GetRepJob(id)
|
||||
if err != nil {
|
||||
t.Errorf("Error occurred in GetRepJob: %v, id: %d", err, id)
|
||||
return
|
||||
}
|
||||
if j == nil {
|
||||
t.Errorf("Unable to find a job with id: %d", id)
|
||||
return
|
||||
}
|
||||
if j.Status != models.JobPending || j.Repository != "library/ubuntu" || j.PolicyID != policyID || j.Operation != "transfer" || len(j.TagList) != 3 {
|
||||
t.Errorf("Expected data of job, id: %d, Status: %s, Repository: library/ubuntu, PolicyID: %d, Operation: transfer, taglist length 3"+
|
||||
"but in returned data:, Status: %s, Repository: %s, Operation: %s, PolicyID: %d, TagList: %v", id, models.JobPending, policyID, j.Status, j.Repository, j.Operation, j.PolicyID, j.TagList)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateRepJobStatus(t *testing.T) {
|
||||
err := UpdateRepJobStatus(jobID, models.JobFinished)
|
||||
if err != nil {
|
||||
t.Errorf("Error occured in UpdateRepJobStatus, error: %v, id: %d", err, jobID)
|
||||
return
|
||||
}
|
||||
j, err := GetRepJob(jobID)
|
||||
if err != nil {
|
||||
t.Errorf("Error occurred in GetRepJob: %v, id: %d", err, jobID)
|
||||
}
|
||||
if j == nil {
|
||||
t.Errorf("Unable to find a job with id: %d", jobID)
|
||||
}
|
||||
if j.Status != models.JobFinished {
|
||||
t.Errorf("Job's status: %s, expected: %s, id: %d", j.Status, models.JobFinished, jobID)
|
||||
}
|
||||
err = UpdateRepJobStatus(jobID, models.JobPending)
|
||||
if err != nil {
|
||||
t.Errorf("Error occured in UpdateRepJobStatus when update it back to status pending, error: %v, id: %d", err, jobID)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRepPolicyByProject(t *testing.T) {
|
||||
p1, err := GetRepPolicyByProject(99)
|
||||
if err != nil {
|
||||
t.Errorf("Error occured in GetRepPolicyByProject:%v, project ID: %d", err, 99)
|
||||
return
|
||||
}
|
||||
if len(p1) > 0 {
|
||||
t.Errorf("Unexpected length of policy list, expected: 0, in fact: %d, project id: %d", len(p1), 99)
|
||||
return
|
||||
}
|
||||
|
||||
p2, err := GetRepPolicyByProject(1)
|
||||
if err != nil {
|
||||
t.Errorf("Error occuered in GetRepPolicyByProject:%v, project ID: %d", err, 2)
|
||||
return
|
||||
}
|
||||
if len(p2) != 1 {
|
||||
t.Errorf("Unexpected length of policy list, expected: 1, in fact: %d, project id: %d", len(p2), 1)
|
||||
return
|
||||
}
|
||||
if p2[0].ID != policyID {
|
||||
t.Errorf("Unexpecred policy id in result, expected: %d, in fact: %d", policyID, p2[0].ID)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRepJobByPolicy(t *testing.T) {
|
||||
jobs, err := GetRepJobByPolicy(999)
|
||||
if err != nil {
|
||||
log.Errorf("Error occured in GetRepJobByPolicy: %v, policy ID: %d", err, 999)
|
||||
return
|
||||
}
|
||||
if len(jobs) > 0 {
|
||||
log.Errorf("Unexpected length of jobs, expected: 0, in fact: %d", len(jobs))
|
||||
return
|
||||
}
|
||||
jobs, err = GetRepJobByPolicy(policyID)
|
||||
if err != nil {
|
||||
log.Errorf("Error occured in GetRepJobByPolicy: %v, policy ID: %d", err, policyID)
|
||||
return
|
||||
}
|
||||
if len(jobs) != 1 {
|
||||
log.Errorf("Unexpected length of jobs, expected: 1, in fact: %d", len(jobs))
|
||||
return
|
||||
}
|
||||
if jobs[0].ID != jobID {
|
||||
log.Errorf("Unexpected job ID in the result, expected: %d, in fact: %d", jobID, jobs[0].ID)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRepoJobToStop(t *testing.T) {
|
||||
jobs := [...]models.RepJob{
|
||||
models.RepJob{
|
||||
Repository: "library/ubuntu",
|
||||
PolicyID: policyID,
|
||||
Operation: "transfer",
|
||||
Status: models.JobRunning,
|
||||
},
|
||||
models.RepJob{
|
||||
Repository: "library/ubuntu",
|
||||
PolicyID: policyID,
|
||||
Operation: "transfer",
|
||||
Status: models.JobFinished,
|
||||
},
|
||||
models.RepJob{
|
||||
Repository: "library/ubuntu",
|
||||
PolicyID: policyID,
|
||||
Operation: "transfer",
|
||||
Status: models.JobCanceled,
|
||||
},
|
||||
}
|
||||
var err error
|
||||
for _, j := range jobs {
|
||||
_, err = AddRepJob(j)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to add Job: %+v, error: %v", j, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
res, err := GetRepJobToStop(policyID)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to Get Jobs, error: %v", err)
|
||||
return
|
||||
}
|
||||
//time.Sleep(15 * time.Second)
|
||||
if len(res) != 2 {
|
||||
log.Errorf("Expected length of stoppable jobs, expected:2, in fact: %d", len(res))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteRepTarget(t *testing.T) {
|
||||
err := DeleteRepTarget(targetID)
|
||||
if err != nil {
|
||||
t.Errorf("Error occured in DeleteRepTarget: %v, id: %d", err, targetID)
|
||||
return
|
||||
}
|
||||
t.Logf("deleted target, id: %d", targetID)
|
||||
tgt, err := GetRepTarget(targetID)
|
||||
if err != nil {
|
||||
t.Errorf("Error occurred in GetTarget: %v, id: %d", err, targetID)
|
||||
}
|
||||
if tgt != nil {
|
||||
t.Errorf("Able to find target after deletion, id: %d", targetID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteRepPolicy(t *testing.T) {
|
||||
err := DeleteRepPolicy(policyID)
|
||||
if err != nil {
|
||||
t.Errorf("Error occured in DeleteRepPolicy: %v, id: %d", err, policyID)
|
||||
return
|
||||
}
|
||||
t.Logf("delete rep policy, id: %d", policyID)
|
||||
p, err := GetRepPolicy(policyID)
|
||||
if err != nil {
|
||||
t.Errorf("Error occured in GetRepPolicy:%v", err)
|
||||
}
|
||||
if p != nil {
|
||||
t.Errorf("Able to find rep policy after deletion, id: %d", policyID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteRepJob(t *testing.T) {
|
||||
err := DeleteRepJob(jobID)
|
||||
if err != nil {
|
||||
t.Errorf("Error occured in DeleteRepJob: %v, id: %d", err, jobID)
|
||||
return
|
||||
}
|
||||
t.Logf("deleted rep job, id: %d", jobID)
|
||||
j, err := GetRepJob(jobID)
|
||||
if err != nil {
|
||||
t.Errorf("Error occured in GetRepJob:%v", err)
|
||||
}
|
||||
if j != nil {
|
||||
t.Errorf("Able to find rep job after deletion, id: %d", jobID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetOrmer(t *testing.T) {
|
||||
o := GetOrmer()
|
||||
if o == nil {
|
||||
|
215
dao/replication_job.go
Normal file
215
dao/replication_job.go
Normal file
@ -0,0 +1,215 @@
|
||||
package dao
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"strings"
|
||||
|
||||
"github.com/astaxie/beego/orm"
|
||||
"github.com/vmware/harbor/models"
|
||||
)
|
||||
|
||||
// AddRepTarget ...
|
||||
func AddRepTarget(target models.RepTarget) (int64, error) {
|
||||
o := GetOrmer()
|
||||
return o.Insert(&target)
|
||||
}
|
||||
|
||||
// GetRepTarget ...
|
||||
func GetRepTarget(id int64) (*models.RepTarget, error) {
|
||||
o := GetOrmer()
|
||||
t := models.RepTarget{ID: id}
|
||||
err := o.Read(&t)
|
||||
if err == orm.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
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 := GetOrmer()
|
||||
_, err := o.Delete(&models.RepTarget{ID: id})
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateRepTarget ...
|
||||
func UpdateRepTarget(target models.RepTarget) error {
|
||||
o := GetOrmer()
|
||||
_, err := o.Update(&target, "URL", "Name", "Username", "Password")
|
||||
return err
|
||||
}
|
||||
|
||||
// GetAllRepTargets ...
|
||||
func GetAllRepTargets() ([]*models.RepTarget, error) {
|
||||
o := GetOrmer()
|
||||
qs := o.QueryTable(&models.RepTarget{})
|
||||
var targets []*models.RepTarget
|
||||
_, err := qs.All(&targets)
|
||||
return targets, err
|
||||
}
|
||||
|
||||
// AddRepPolicy ...
|
||||
func AddRepPolicy(policy models.RepPolicy) (int64, error) {
|
||||
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 {
|
||||
sql = fmt.Sprintf(sqlTpl, "NOW()")
|
||||
} else {
|
||||
sql = fmt.Sprintf(sqlTpl, "NULL")
|
||||
}
|
||||
p, err := o.Raw(sql).Prepare()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
r, err := p.Exec(policy.Name, policy.ProjectID, policy.TargetID, policy.Enabled, policy.Description, policy.CronStr)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
id, err := r.LastInsertId()
|
||||
return id, err
|
||||
}
|
||||
|
||||
// GetRepPolicy ...
|
||||
func GetRepPolicy(id int64) (*models.RepPolicy, error) {
|
||||
o := GetOrmer()
|
||||
p := models.RepPolicy{ID: id}
|
||||
err := o.Read(&p)
|
||||
if err == orm.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
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 := GetOrmer()
|
||||
_, err := o.QueryTable("replication_policy").Filter("project_id", projectID).All(&res)
|
||||
return res, err
|
||||
}
|
||||
|
||||
// DeleteRepPolicy ...
|
||||
func DeleteRepPolicy(id int64) error {
|
||||
o := GetOrmer()
|
||||
_, err := o.Delete(&models.RepPolicy{ID: id})
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateRepPolicyEnablement ...
|
||||
func UpdateRepPolicyEnablement(id int64, enabled int) error {
|
||||
o := GetOrmer()
|
||||
p := models.RepPolicy{
|
||||
ID: id,
|
||||
Enabled: enabled}
|
||||
_, err := o.Update(&p, "Enabled")
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// EnableRepPolicy ...
|
||||
func EnableRepPolicy(id int64) error {
|
||||
return UpdateRepPolicyEnablement(id, 1)
|
||||
}
|
||||
|
||||
// DisableRepPolicy ...
|
||||
func DisableRepPolicy(id int64) error {
|
||||
return UpdateRepPolicyEnablement(id, 0)
|
||||
}
|
||||
|
||||
// AddRepJob ...
|
||||
func AddRepJob(job models.RepJob) (int64, error) {
|
||||
o := GetOrmer()
|
||||
if len(job.Status) == 0 {
|
||||
job.Status = models.JobPending
|
||||
}
|
||||
if len(job.TagList) > 0 {
|
||||
job.Tags = strings.Join(job.TagList, ",")
|
||||
}
|
||||
return o.Insert(&job)
|
||||
}
|
||||
|
||||
// GetRepJob ...
|
||||
func GetRepJob(id int64) (*models.RepJob, error) {
|
||||
o := GetOrmer()
|
||||
j := models.RepJob{ID: id}
|
||||
err := o.Read(&j)
|
||||
if err == orm.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
genTagListForJob(&j)
|
||||
return &j, nil
|
||||
}
|
||||
|
||||
// GetRepJobByPolicy ...
|
||||
func GetRepJobByPolicy(policyID int64) ([]*models.RepJob, error) {
|
||||
var res []*models.RepJob
|
||||
_, err := repJobPolicyIDQs(policyID).All(&res)
|
||||
genTagListForJob(res...)
|
||||
return res, err
|
||||
}
|
||||
|
||||
// GetRepJobToStop get jobs that are possibly being handled by workers of a certain policy.
|
||||
func GetRepJobToStop(policyID int64) ([]*models.RepJob, error) {
|
||||
var res []*models.RepJob
|
||||
_, err := repJobPolicyIDQs(policyID).Filter("status__in", models.JobPending, models.JobRunning).All(&res)
|
||||
genTagListForJob(res...)
|
||||
return res, err
|
||||
}
|
||||
|
||||
func repJobPolicyIDQs(policyID int64) orm.QuerySeter {
|
||||
o := GetOrmer()
|
||||
return o.QueryTable("replication_job").Filter("policy_id", policyID)
|
||||
}
|
||||
|
||||
// DeleteRepJob ...
|
||||
func DeleteRepJob(id int64) error {
|
||||
o := GetOrmer()
|
||||
_, err := o.Delete(&models.RepJob{ID: id})
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateRepJobStatus ...
|
||||
func UpdateRepJobStatus(id int64, status string) error {
|
||||
o := GetOrmer()
|
||||
j := models.RepJob{
|
||||
ID: id,
|
||||
Status: status,
|
||||
}
|
||||
num, err := o.Update(&j, "Status")
|
||||
if num == 0 {
|
||||
err = fmt.Errorf("Failed to update replication job with id: %d %s", id, err.Error())
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func genTagListForJob(jobs ...*models.RepJob) {
|
||||
for _, j := range jobs {
|
||||
if len(j.Tags) > 0 {
|
||||
j.TagList = strings.Split(j.Tags, ",")
|
||||
}
|
||||
}
|
||||
}
|
79
job/config/config.go
Normal file
79
job/config/config.go
Normal file
@ -0,0 +1,79 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/vmware/harbor/utils/log"
|
||||
)
|
||||
|
||||
const defaultMaxWorkers int = 10
|
||||
|
||||
var maxJobWorkers int
|
||||
var localURL string
|
||||
var logDir string
|
||||
var uiSecret string
|
||||
|
||||
func init() {
|
||||
maxWorkersEnv := os.Getenv("MAX_JOB_WORKERS")
|
||||
maxWorkers64, err := strconv.ParseInt(maxWorkersEnv, 10, 32)
|
||||
maxJobWorkers = int(maxWorkers64)
|
||||
if err != nil {
|
||||
log.Warningf("Failed to parse max works setting, error: %v, the default value: %d will be used", err, defaultMaxWorkers)
|
||||
maxJobWorkers = defaultMaxWorkers
|
||||
}
|
||||
|
||||
localURL = os.Getenv("HARBOR_URL")
|
||||
if len(localURL) == 0 {
|
||||
localURL = "http://registry:5000/"
|
||||
}
|
||||
|
||||
logDir = os.Getenv("LOG_DIR")
|
||||
if len(logDir) == 0 {
|
||||
logDir = "/var/log"
|
||||
}
|
||||
|
||||
f, err := os.Open(logDir)
|
||||
defer f.Close()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
finfo, err := f.Stat()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if !finfo.IsDir() {
|
||||
panic(fmt.Sprintf("%s is not a direcotry", logDir))
|
||||
}
|
||||
|
||||
uiSecret = os.Getenv("UI_SECRET")
|
||||
if len(uiSecret) == 0 {
|
||||
panic("UI Secret is not set")
|
||||
}
|
||||
|
||||
log.Debugf("config: maxJobWorkers: %d", maxJobWorkers)
|
||||
log.Debugf("config: localHarborURL: %s", localURL)
|
||||
log.Debugf("config: logDir: %s", logDir)
|
||||
log.Debugf("config: uiSecret: ******")
|
||||
}
|
||||
|
||||
// MaxJobWorkers ...
|
||||
func MaxJobWorkers() int {
|
||||
return maxJobWorkers
|
||||
}
|
||||
|
||||
// LocalHarborURL returns the local registry url, job service will use this URL to pull manifest and repository.
|
||||
func LocalHarborURL() string {
|
||||
return localURL
|
||||
}
|
||||
|
||||
// LogDir returns the absolute path to which the log file will be written
|
||||
func LogDir() string {
|
||||
return logDir
|
||||
}
|
||||
|
||||
// UISecret will return the value of secret cookie for jobsevice to call UI API.
|
||||
func UISecret() string {
|
||||
return uiSecret
|
||||
}
|
119
job/replication/delete.go
Normal file
119
job/replication/delete.go
Normal file
@ -0,0 +1,119 @@
|
||||
/*
|
||||
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 replication
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/vmware/harbor/models"
|
||||
"github.com/vmware/harbor/utils/log"
|
||||
)
|
||||
|
||||
const (
|
||||
// StateDelete ...
|
||||
StateDelete = "delete"
|
||||
)
|
||||
|
||||
// Deleter deletes repository or tags
|
||||
type Deleter struct {
|
||||
repository string // prject_name/repo_name
|
||||
tags []string
|
||||
|
||||
dstURL string // url of target registry
|
||||
dstUsr string // username ...
|
||||
dstPwd string // username ...
|
||||
|
||||
logger *log.Logger
|
||||
}
|
||||
|
||||
// NewDeleter returns a Deleter
|
||||
func NewDeleter(repository string, tags []string, dstURL, dstUsr, dstPwd string, logger *log.Logger) *Deleter {
|
||||
deleter := &Deleter{
|
||||
repository: repository,
|
||||
tags: tags,
|
||||
dstURL: dstURL,
|
||||
dstUsr: dstUsr,
|
||||
dstPwd: dstPwd,
|
||||
logger: logger,
|
||||
}
|
||||
deleter.logger.Infof("initialization completed: repository: %s, tags: %v, destination URL: %s, destination user: %s",
|
||||
deleter.repository, deleter.tags, deleter.dstURL, deleter.dstUsr)
|
||||
return deleter
|
||||
}
|
||||
|
||||
// Exit ...
|
||||
func (d *Deleter) Exit() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Enter deletes repository or tags
|
||||
func (d *Deleter) Enter() (string, error) {
|
||||
url := strings.TrimRight(d.dstURL, "/") + "/api/repositories/"
|
||||
|
||||
// delete repository
|
||||
if len(d.tags) == 0 {
|
||||
u := url + "?repo_name=" + d.repository
|
||||
if err := del(u, d.dstUsr, d.dstPwd); err != nil {
|
||||
d.logger.Errorf("an error occurred while deleting repository %s on %s with user %s: %v", d.repository, d.dstURL, d.dstUsr, err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
d.logger.Infof("repository %s on %s has been deleted", d.repository, d.dstURL)
|
||||
|
||||
return models.JobFinished, nil
|
||||
}
|
||||
|
||||
// delele tags
|
||||
for _, tag := range d.tags {
|
||||
u := url + "?repo_name=" + d.repository + "&tag=" + tag
|
||||
if err := del(u, d.dstUsr, d.dstPwd); err != nil {
|
||||
d.logger.Errorf("an error occurred while deleting repository %s:%s on %s with user %s: %v", d.repository, tag, d.dstURL, d.dstUsr, err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
d.logger.Infof("repository %s:%s on %s has been deleted", d.repository, tag, d.dstURL)
|
||||
}
|
||||
|
||||
return models.JobFinished, nil
|
||||
}
|
||||
|
||||
func del(url, username, password string) error {
|
||||
req, err := http.NewRequest("DELETE", url, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.SetBasicAuth(username, password)
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return nil
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
b, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return fmt.Errorf("%d %s", resp.StatusCode, string(b))
|
||||
}
|
76
job/replication/runner.go
Normal file
76
job/replication/runner.go
Normal file
@ -0,0 +1,76 @@
|
||||
package replication
|
||||
|
||||
/*
|
||||
import (
|
||||
"encoding/json"
|
||||
//"github.com/vmware/harbor/dao"
|
||||
"github.com/vmware/harbor/job"
|
||||
"github.com/vmware/harbor/models"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
jobType = "transfer_img_out"
|
||||
)
|
||||
|
||||
type Runner struct {
|
||||
job.JobSM
|
||||
Logger job.Logger
|
||||
parm ImgOutParm
|
||||
}
|
||||
|
||||
type ImgPuller struct {
|
||||
job.DummyHandler
|
||||
img string
|
||||
logger job.Logger
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
type ImgPusher struct {
|
||||
job.DummyHandler
|
||||
targetURL string
|
||||
logger job.Logger
|
||||
}
|
||||
|
||||
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 job.JobContinue, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
job.Register(jobType, Runner{})
|
||||
}
|
||||
|
||||
func (r Runner) Run(je models.JobEntry) error {
|
||||
err := r.init(je)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.Start(job.JobRunning)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Runner) init(je models.JobEntry) error {
|
||||
r.JobID = je.ID
|
||||
r.InitJobSM()
|
||||
err := json.Unmarshal([]byte(je.ParmsStr), &r.parm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.Logger = job.Logger{je.ID}
|
||||
r.AddTransition(job.JobRunning, "pull-img", ImgPuller{DummyHandler: job.DummyHandler{JobID: r.JobID}, img: r.parm.Image, logger: r.Logger})
|
||||
//only handle on target for now
|
||||
url := r.parm.Targets[0].URL
|
||||
r.AddTransition("pull-img", "push-img", ImgPusher{DummyHandler: job.DummyHandler{JobID: r.JobID}, targetURL: url, logger: r.Logger})
|
||||
r.AddTransition("push-img", job.JobFinished, job.StatusUpdater{job.DummyHandler{JobID: r.JobID}, job.JobFinished})
|
||||
return nil
|
||||
}
|
||||
*/
|
399
job/replication/transfer.go
Normal file
399
job/replication/transfer.go
Normal file
@ -0,0 +1,399 @@
|
||||
/*
|
||||
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 replication
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/distribution"
|
||||
"github.com/docker/distribution/manifest/schema1"
|
||||
"github.com/docker/distribution/manifest/schema2"
|
||||
"github.com/vmware/harbor/models"
|
||||
"github.com/vmware/harbor/utils/log"
|
||||
"github.com/vmware/harbor/utils/registry"
|
||||
"github.com/vmware/harbor/utils/registry/auth"
|
||||
)
|
||||
|
||||
const (
|
||||
// StateCheck ...
|
||||
StateCheck = "check"
|
||||
// StatePullManifest ...
|
||||
StatePullManifest = "pull_manifest"
|
||||
// StateTransferBlob ...
|
||||
StateTransferBlob = "transfer_blob"
|
||||
// StatePushManifest ...
|
||||
StatePushManifest = "push_manifest"
|
||||
)
|
||||
|
||||
// BaseHandler holds informations shared by other state handlers
|
||||
type BaseHandler struct {
|
||||
project string // project_name
|
||||
repository string // prject_name/repo_name
|
||||
tags []string
|
||||
|
||||
srcURL string // url of source registry
|
||||
|
||||
dstURL string // url of target registry
|
||||
dstUsr string // username ...
|
||||
dstPwd string // password ...
|
||||
|
||||
srcClient *registry.Repository
|
||||
dstClient *registry.Repository
|
||||
|
||||
manifest distribution.Manifest // manifest of tags[0]
|
||||
blobs []string // blobs need to be transferred for tags[0]
|
||||
|
||||
blobsExistence map[string]bool //key: digest of blob, value: existence
|
||||
|
||||
logger *log.Logger
|
||||
}
|
||||
|
||||
// InitBaseHandler initializes a BaseHandler: creating clients for source and destination registry,
|
||||
// listing tags of the repository if parameter tags is nil.
|
||||
func InitBaseHandler(repository, srcURL, srcSecret,
|
||||
dstURL, dstUsr, dstPwd string, tags []string, logger *log.Logger) (*BaseHandler, error) {
|
||||
|
||||
logger.Infof("initializing: repository: %s, tags: %v, source URL: %s, destination URL: %s, destination user: %s",
|
||||
repository, tags, srcURL, dstURL, dstUsr)
|
||||
|
||||
base := &BaseHandler{
|
||||
repository: repository,
|
||||
tags: tags,
|
||||
srcURL: srcURL,
|
||||
dstURL: dstURL,
|
||||
dstUsr: dstUsr,
|
||||
dstPwd: dstPwd,
|
||||
blobsExistence: make(map[string]bool, 10),
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
base.project = getProjectName(base.repository)
|
||||
|
||||
c := &http.Cookie{Name: models.UISecretCookie, Value: srcSecret}
|
||||
srcCred := auth.NewCookieCredential(c)
|
||||
// srcCred := auth.NewBasicAuthCredential("admin", "Harbor12345")
|
||||
srcClient, err := registry.NewRepositoryWithCredential(base.repository, base.srcURL, srcCred)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
base.srcClient = srcClient
|
||||
|
||||
dstCred := auth.NewBasicAuthCredential(base.dstUsr, base.dstPwd)
|
||||
dstClient, err := registry.NewRepositoryWithCredential(base.repository, base.dstURL, dstCred)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
base.dstClient = dstClient
|
||||
|
||||
if len(base.tags) == 0 {
|
||||
tags, err := base.srcClient.ListTag()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
base.tags = tags
|
||||
}
|
||||
|
||||
base.logger.Infof("initialization completed: project: %s, repository: %s, tags: %v, source URL: %s, destination URL: %s, destination user: %s",
|
||||
base.project, base.repository, base.tags, base.srcURL, base.dstURL, base.dstUsr)
|
||||
|
||||
return base, nil
|
||||
}
|
||||
|
||||
// Exit ...
|
||||
func (b *BaseHandler) Exit() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func getProjectName(repository string) string {
|
||||
repository = strings.TrimSpace(repository)
|
||||
repository = strings.TrimRight(repository, "/")
|
||||
return repository[:strings.LastIndex(repository, "/")]
|
||||
}
|
||||
|
||||
// Checker checks the existence of project and the user's privlege to the project
|
||||
type Checker struct {
|
||||
*BaseHandler
|
||||
}
|
||||
|
||||
// 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) {
|
||||
exist, canWrite, err := c.projectExist()
|
||||
if err != nil {
|
||||
c.logger.Errorf("an error occurred while checking existence of project %s on %s with user %s : %v", c.project, c.dstURL, c.dstUsr, err)
|
||||
return "", err
|
||||
}
|
||||
if !exist {
|
||||
if err := c.createProject(); err != nil {
|
||||
c.logger.Errorf("an error occurred while creating project %s on %s with user %s : %v", c.project, c.dstURL, c.dstUsr, err)
|
||||
return "", err
|
||||
}
|
||||
c.logger.Infof("project %s is created on %s with user %s", c.project, c.dstURL, c.dstUsr)
|
||||
return StatePullManifest, nil
|
||||
}
|
||||
|
||||
c.logger.Infof("project %s already exists on %s", c.project, c.dstURL)
|
||||
|
||||
if !canWrite {
|
||||
err = fmt.Errorf("the user %s is unauthorized to write to project %s on %s", c.dstUsr, c.project, c.dstURL)
|
||||
c.logger.Errorf("%v", err)
|
||||
return "", err
|
||||
}
|
||||
c.logger.Infof("the user %s has write privilege to project %s on %s", c.dstUsr, c.project, c.dstURL)
|
||||
|
||||
return StatePullManifest, nil
|
||||
}
|
||||
|
||||
// check the existence of project, if it exists, returning whether the user has write privilege to it
|
||||
func (c *Checker) projectExist() (exist, canWrite bool, err error) {
|
||||
url := strings.TrimRight(c.dstURL, "/") + "/api/projects/?project_name=" + c.project
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
req.SetBasicAuth(c.dstUsr, c.dstPwd)
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return
|
||||
}
|
||||
|
||||
if resp.StatusCode == http.StatusUnauthorized {
|
||||
exist = true
|
||||
return
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
data, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
projects := make([]models.Project, 1)
|
||||
if err = json.Unmarshal(data, &projects); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(projects) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
for _, project := range projects {
|
||||
if project.Name == c.project {
|
||||
exist = true
|
||||
canWrite = (project.Role == models.PROJECTADMIN ||
|
||||
project.Role == models.DEVELOPER)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
err = fmt.Errorf("an error occurred while checking existen of project %s on %s with user %s: %d %s",
|
||||
c.project, c.dstURL, c.dstUsr, resp.StatusCode, string(data))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Checker) createProject() error {
|
||||
// TODO handle publicity of project
|
||||
project := struct {
|
||||
ProjectName string `json:"project_name"`
|
||||
Public bool `json:"public"`
|
||||
}{
|
||||
ProjectName: c.project,
|
||||
}
|
||||
|
||||
data, err := json.Marshal(project)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
url := strings.TrimRight(c.dstURL, "/") + "/api/projects/"
|
||||
req, err := http.NewRequest("POST", url, bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.SetBasicAuth(c.dstUsr, c.dstPwd)
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusCreated {
|
||||
defer resp.Body.Close()
|
||||
message, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
c.logger.Errorf("an error occurred while reading message from response: %v", err)
|
||||
}
|
||||
|
||||
return fmt.Errorf("failed to create project %s on %s with user %s: %d %s",
|
||||
c.project, c.dstURL, c.dstUsr, resp.StatusCode, string(message))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ManifestPuller pulls the manifest of a tag. And if no tag needs to be pulled,
|
||||
// the next state that state machine should enter is "finished".
|
||||
type ManifestPuller struct {
|
||||
*BaseHandler
|
||||
}
|
||||
|
||||
// Enter pulls manifest of a tag and checks if all blobs exist in the destination registry
|
||||
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
|
||||
}
|
||||
|
||||
name := m.repository
|
||||
tag := m.tags[0]
|
||||
|
||||
acceptMediaTypes := []string{schema1.MediaTypeManifest, schema2.MediaTypeManifest}
|
||||
digest, mediaType, payload, err := m.srcClient.PullManifest(tag, acceptMediaTypes)
|
||||
if err != nil {
|
||||
m.logger.Errorf("an error occurred while pulling manifest of %s:%s from %s: %v", name, tag, m.srcURL, err)
|
||||
return "", err
|
||||
}
|
||||
m.logger.Infof("manifest of %s:%s pulled successfully from %s: %s", name, tag, m.srcURL, digest)
|
||||
|
||||
if strings.Contains(mediaType, "application/json") {
|
||||
mediaType = schema1.MediaTypeManifest
|
||||
}
|
||||
|
||||
manifest, _, err := registry.UnMarshal(mediaType, payload)
|
||||
if err != nil {
|
||||
m.logger.Errorf("an error occurred while parsing manifest of %s:%s from %s: %v", name, tag, m.srcURL, err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
m.manifest = manifest
|
||||
|
||||
// all blobs(layers and config)
|
||||
var blobs []string
|
||||
|
||||
for _, discriptor := range manifest.References() {
|
||||
blobs = append(blobs, discriptor.Digest.String())
|
||||
}
|
||||
|
||||
// config is also need to be transferred if the schema of manifest is v2
|
||||
manifest2, ok := manifest.(*schema2.DeserializedManifest)
|
||||
if ok {
|
||||
blobs = append(blobs, manifest2.Target().Digest.String())
|
||||
}
|
||||
|
||||
m.logger.Infof("all blobs of %s:%s from %s: %v", name, tag, m.srcURL, blobs)
|
||||
|
||||
for _, blob := range blobs {
|
||||
exist, ok := m.blobsExistence[blob]
|
||||
if !ok {
|
||||
exist, err = m.dstClient.BlobExist(blob)
|
||||
if err != nil {
|
||||
m.logger.Errorf("an error occurred while checking existence of blob %s of %s:%s on %s: %v", blob, name, tag, m.dstURL, err)
|
||||
return "", err
|
||||
}
|
||||
m.blobsExistence[blob] = exist
|
||||
}
|
||||
|
||||
if !exist {
|
||||
m.blobs = append(m.blobs, blob)
|
||||
} else {
|
||||
m.logger.Infof("blob %s of %s:%s already exists in %s", blob, name, tag, m.dstURL)
|
||||
}
|
||||
}
|
||||
m.logger.Infof("blobs of %s:%s need to be transferred to %s: %v", name, tag, m.dstURL, m.blobs)
|
||||
|
||||
return StateTransferBlob, nil
|
||||
}
|
||||
|
||||
// BlobTransfer transfers blobs of a tag
|
||||
type BlobTransfer struct {
|
||||
*BaseHandler
|
||||
}
|
||||
|
||||
// Enter pulls blobs and then pushs them to destination registry.
|
||||
func (b *BlobTransfer) Enter() (string, error) {
|
||||
name := b.repository
|
||||
tag := b.tags[0]
|
||||
for _, blob := range b.blobs {
|
||||
b.logger.Infof("transferring blob %s of %s:%s to %s ...", blob, name, tag, b.dstURL)
|
||||
size, data, err := b.srcClient.PullBlob(blob)
|
||||
if err != nil {
|
||||
b.logger.Errorf("an error occurred while pulling blob %s of %s:%s from %s: %v", blob, name, tag, b.srcURL, err)
|
||||
return "", err
|
||||
}
|
||||
if err = b.dstClient.PushBlob(blob, size, data); err != nil {
|
||||
b.logger.Errorf("an error occurred while pushing blob %s of %s:%s to %s : %v", blob, name, tag, b.dstURL, err)
|
||||
return "", err
|
||||
}
|
||||
b.logger.Infof("blob %s of %s:%s transferred to %s completed", blob, name, tag, b.dstURL)
|
||||
}
|
||||
|
||||
return StatePushManifest, nil
|
||||
}
|
||||
|
||||
// ManifestPusher pushs the manifest to destination registry
|
||||
type ManifestPusher struct {
|
||||
*BaseHandler
|
||||
}
|
||||
|
||||
// Enter checks the existence of manifest in the source registry first, and if it
|
||||
// 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) {
|
||||
name := m.repository
|
||||
tag := m.tags[0]
|
||||
_, exist, err := m.srcClient.ManifestExist(tag)
|
||||
if err != nil {
|
||||
m.logger.Infof("an error occurred while checking the existence of manifest of %s:%s on %s: %v", name, tag, m.srcURL, err)
|
||||
return "", err
|
||||
}
|
||||
if !exist {
|
||||
m.logger.Infof("manifest of %s:%s does not exist on source registry %s, cancel manifest pushing", name, tag, m.srcURL)
|
||||
} else {
|
||||
m.logger.Infof("manifest of %s:%s exists on source registry %s, continue manifest pushing", name, tag, m.srcURL)
|
||||
mediaType, data, err := m.manifest.Payload()
|
||||
if err != nil {
|
||||
m.logger.Errorf("an error occurred while getting payload of manifest for %s:%s : %v", name, tag, err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
if _, err = m.dstClient.PushManifest(tag, mediaType, data); err != nil {
|
||||
m.logger.Errorf("an error occurred while pushing manifest of %s:%s to %s : %v", name, tag, m.dstURL, err)
|
||||
return "", err
|
||||
}
|
||||
m.logger.Infof("manifest of %s:%s has been pushed to %s", name, tag, m.dstURL)
|
||||
}
|
||||
|
||||
m.tags = m.tags[1:]
|
||||
m.manifest = nil
|
||||
m.blobs = nil
|
||||
|
||||
return StatePullManifest, nil
|
||||
}
|
8
job/scheduler.go
Normal file
8
job/scheduler.go
Normal file
@ -0,0 +1,8 @@
|
||||
package job
|
||||
|
||||
var jobQueue = make(chan int64)
|
||||
|
||||
// Schedule put a job id into job queue.
|
||||
func Schedule(jobID int64) {
|
||||
jobQueue <- jobID
|
||||
}
|
85
job/statehandlers.go
Normal file
85
job/statehandlers.go
Normal file
@ -0,0 +1,85 @@
|
||||
package job
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/vmware/harbor/dao"
|
||||
"github.com/vmware/harbor/models"
|
||||
"github.com/vmware/harbor/utils/log"
|
||||
)
|
||||
|
||||
// StateHandler handles transition, it associates with each state, will be called when
|
||||
// SM enters and exits a state during a transition.
|
||||
type StateHandler interface {
|
||||
// Enter returns the next state, if it returns empty string the SM will hold the current state or
|
||||
// or decide the next state.
|
||||
Enter() (string, error)
|
||||
//Exit should be idempotent
|
||||
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
|
||||
State string
|
||||
}
|
||||
|
||||
// Enter updates the status of a job and returns "_continue" status to tell state machine to move on.
|
||||
// If the status is a final status it returns empty string and the state machine will be stopped.
|
||||
func (su StatusUpdater) Enter() (string, error) {
|
||||
err := dao.UpdateRepJobStatus(su.JobID, su.State)
|
||||
if err != nil {
|
||||
log.Warningf("Failed to update state of job: %d, state: %s, error: %v", su.JobID, su.State, err)
|
||||
}
|
||||
var next = models.JobContinue
|
||||
if su.State == models.JobStopped || su.State == models.JobError || su.State == models.JobFinished {
|
||||
next = ""
|
||||
}
|
||||
return next, err
|
||||
}
|
||||
|
||||
// ImgPuller was for testing
|
||||
type ImgPuller struct {
|
||||
DummyHandler
|
||||
img string
|
||||
logger *log.Logger
|
||||
}
|
||||
|
||||
// Enter ...
|
||||
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
|
||||
}
|
||||
|
||||
// ImgPusher is a statehandler for testing
|
||||
type ImgPusher struct {
|
||||
DummyHandler
|
||||
targetURL string
|
||||
logger *log.Logger
|
||||
}
|
||||
|
||||
// Enter ...
|
||||
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
|
||||
}
|
269
job/statemachine.go
Normal file
269
job/statemachine.go
Normal file
@ -0,0 +1,269 @@
|
||||
package job
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/vmware/harbor/dao"
|
||||
"github.com/vmware/harbor/job/config"
|
||||
"github.com/vmware/harbor/job/replication"
|
||||
"github.com/vmware/harbor/job/utils"
|
||||
"github.com/vmware/harbor/models"
|
||||
uti "github.com/vmware/harbor/utils"
|
||||
"github.com/vmware/harbor/utils/log"
|
||||
)
|
||||
|
||||
// RepJobParm wraps the parm of a job
|
||||
type RepJobParm struct {
|
||||
LocalRegURL string
|
||||
TargetURL string
|
||||
TargetUsername string
|
||||
TargetPassword string
|
||||
Repository string
|
||||
Tags []string
|
||||
Enabled int
|
||||
Operation string
|
||||
}
|
||||
|
||||
// SM is the state machine to handle job, it handles one job at a time.
|
||||
type SM struct {
|
||||
JobID int64
|
||||
CurrentState string
|
||||
PreviousState string
|
||||
//The states that don't have to exist in transition map, such as "Error", "Canceled"
|
||||
ForcedStates map[string]struct{}
|
||||
Transitions map[string]map[string]struct{}
|
||||
Handlers map[string]StateHandler
|
||||
desiredState string
|
||||
Logger *log.Logger
|
||||
Parms *RepJobParm
|
||||
lock *sync.Mutex
|
||||
}
|
||||
|
||||
// EnterState transit the statemachine from the current state to the state in parameter.
|
||||
// It returns the next state the statemachine should tranit to.
|
||||
func (sm *SM) EnterState(s string) (string, error) {
|
||||
log.Debugf("Job id: %d, transiting from State: %s, to State: %s", sm.JobID, sm.CurrentState, s)
|
||||
targets, ok := sm.Transitions[sm.CurrentState]
|
||||
_, exist := targets[s]
|
||||
_, isForced := sm.ForcedStates[s]
|
||||
if !exist && !isForced {
|
||||
return "", fmt.Errorf("Job id: %d, transition from %s to %s does not exist!", sm.JobID, sm.CurrentState, s)
|
||||
}
|
||||
exitHandler, ok := sm.Handlers[sm.CurrentState]
|
||||
if ok {
|
||||
if err := exitHandler.Exit(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
} else {
|
||||
log.Debugf("Job id: %d, no handler found for state:%s, skip", sm.JobID, sm.CurrentState)
|
||||
}
|
||||
enterHandler, ok := sm.Handlers[s]
|
||||
var next = models.JobContinue
|
||||
var err error
|
||||
if ok {
|
||||
if next, err = enterHandler.Enter(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
} else {
|
||||
log.Debugf("Job id: %d, no handler found for state:%s, skip", sm.JobID, s)
|
||||
}
|
||||
sm.PreviousState = sm.CurrentState
|
||||
sm.CurrentState = s
|
||||
log.Debugf("Job id: %d, transition succeeded, current state: %s", sm.JobID, s)
|
||||
return next, nil
|
||||
}
|
||||
|
||||
// Start kicks off the statemachine to transit from current state to s, and moves on
|
||||
// It will search the transit map if the next state is "_continue", and
|
||||
// will enter error state if there's more than one possible path when next state is "_continue"
|
||||
func (sm *SM) Start(s string) {
|
||||
n, err := sm.EnterState(s)
|
||||
log.Debugf("Job id: %d, next state from handler: %s", sm.JobID, n)
|
||||
for len(n) > 0 && err == nil {
|
||||
if d := sm.getDesiredState(); len(d) > 0 {
|
||||
log.Debugf("Job id: %d. Desired state: %s, will ignore the next state from handler", sm.JobID, d)
|
||||
n = d
|
||||
sm.setDesiredState("")
|
||||
continue
|
||||
}
|
||||
if n == models.JobContinue && len(sm.Transitions[sm.CurrentState]) == 1 {
|
||||
for n = range sm.Transitions[sm.CurrentState] {
|
||||
break
|
||||
}
|
||||
log.Debugf("Job id: %d, Continue to state: %s", sm.JobID, n)
|
||||
continue
|
||||
}
|
||||
if n == models.JobContinue && len(sm.Transitions[sm.CurrentState]) != 1 {
|
||||
log.Errorf("Job id: %d, next state is continue but there are %d possible next states in transition table", sm.JobID, len(sm.Transitions[sm.CurrentState]))
|
||||
err = fmt.Errorf("Unable to continue")
|
||||
break
|
||||
}
|
||||
n, err = sm.EnterState(n)
|
||||
log.Debugf("Job id: %d, next state from handler: %s", sm.JobID, n)
|
||||
}
|
||||
if err != nil {
|
||||
log.Warningf("Job id: %d, the statemachin will enter error state due to error: %v", sm.JobID, err)
|
||||
sm.EnterState(models.JobError)
|
||||
}
|
||||
}
|
||||
|
||||
// AddTransition add a transition to the transition table of state machine, the handler is the handler of target state "to"
|
||||
func (sm *SM) AddTransition(from string, to string, h StateHandler) {
|
||||
_, ok := sm.Transitions[from]
|
||||
if !ok {
|
||||
sm.Transitions[from] = make(map[string]struct{})
|
||||
}
|
||||
sm.Transitions[from][to] = struct{}{}
|
||||
sm.Handlers[to] = h
|
||||
}
|
||||
|
||||
// RemoveTransition removes a transition from transition table of the state machine
|
||||
func (sm *SM) RemoveTransition(from string, to string) {
|
||||
_, ok := sm.Transitions[from]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
delete(sm.Transitions[from], to)
|
||||
}
|
||||
|
||||
// Stop will set the desired state as "stopped" such that when next tranisition happen the state machine will stop handling the current job
|
||||
// and the worker can release itself to the workerpool.
|
||||
func (sm *SM) Stop(id int64) {
|
||||
log.Debugf("Trying to stop the job: %d", id)
|
||||
sm.lock.Lock()
|
||||
defer sm.lock.Unlock()
|
||||
//need to check if the sm switched to other job
|
||||
if id == sm.JobID {
|
||||
sm.desiredState = models.JobStopped
|
||||
log.Debugf("Desired state of job %d is set to stopped", id)
|
||||
} else {
|
||||
log.Debugf("State machine has switched to job %d, so the action to stop job %d will be ignored", sm.JobID, id)
|
||||
}
|
||||
}
|
||||
|
||||
func (sm *SM) getDesiredState() string {
|
||||
sm.lock.Lock()
|
||||
defer sm.lock.Unlock()
|
||||
return sm.desiredState
|
||||
}
|
||||
|
||||
func (sm *SM) setDesiredState(s string) {
|
||||
sm.lock.Lock()
|
||||
defer sm.lock.Unlock()
|
||||
sm.desiredState = s
|
||||
}
|
||||
|
||||
// Init initialzie the state machine, it will be called once in the lifecycle of state machine.
|
||||
func (sm *SM) Init() {
|
||||
sm.lock = &sync.Mutex{}
|
||||
sm.Handlers = make(map[string]StateHandler)
|
||||
sm.Transitions = make(map[string]map[string]struct{})
|
||||
sm.ForcedStates = map[string]struct{}{
|
||||
models.JobError: struct{}{},
|
||||
models.JobStopped: struct{}{},
|
||||
models.JobCanceled: struct{}{},
|
||||
}
|
||||
}
|
||||
|
||||
// Reset resets the state machine so it will start handling another job.
|
||||
func (sm *SM) Reset(jid int64) error {
|
||||
//To ensure the new jobID is visible to the thread to stop the SM
|
||||
sm.lock.Lock()
|
||||
sm.JobID = jid
|
||||
sm.desiredState = ""
|
||||
sm.lock.Unlock()
|
||||
|
||||
//init parms
|
||||
job, err := dao.GetRepJob(sm.JobID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to get job, error: %v", err)
|
||||
}
|
||||
if job == nil {
|
||||
return fmt.Errorf("The job doesn't exist in DB, job id: %d", sm.JobID)
|
||||
}
|
||||
policy, err := dao.GetRepPolicy(job.PolicyID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to get policy, error: %v", err)
|
||||
}
|
||||
if policy == nil {
|
||||
return fmt.Errorf("The policy doesn't exist in DB, policy id:%d", job.PolicyID)
|
||||
}
|
||||
sm.Parms = &RepJobParm{
|
||||
LocalRegURL: config.LocalHarborURL(),
|
||||
Repository: job.Repository,
|
||||
Tags: job.TagList,
|
||||
Enabled: policy.Enabled,
|
||||
Operation: job.Operation,
|
||||
}
|
||||
if policy.Enabled == 0 {
|
||||
//worker will cancel this job
|
||||
return nil
|
||||
}
|
||||
target, err := dao.GetRepTarget(policy.TargetID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to get target, error: %v", err)
|
||||
}
|
||||
if target == nil {
|
||||
return fmt.Errorf("The target doesn't exist in DB, target id: %d", policy.TargetID)
|
||||
}
|
||||
sm.Parms.TargetURL = target.URL
|
||||
sm.Parms.TargetUsername = target.Username
|
||||
pwd := target.Password
|
||||
|
||||
if len(pwd) != 0 {
|
||||
pwd, err = uti.ReversibleDecrypt(pwd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decrypt password: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
sm.Parms.TargetPassword = pwd
|
||||
|
||||
//init states handlers
|
||||
sm.Logger = utils.NewLogger(sm.JobID)
|
||||
sm.Handlers = make(map[string]StateHandler)
|
||||
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}
|
||||
|
||||
switch sm.Parms.Operation {
|
||||
case models.RepOpTransfer:
|
||||
err = addImgTransferTransition(sm)
|
||||
case models.RepOpDelete:
|
||||
err = addImgDeleteTransition(sm)
|
||||
default:
|
||||
err = fmt.Errorf("unsupported operation: %s", sm.Parms.Operation)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
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,
|
||||
sm.Parms.Tags, sm.Logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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.StateTransferBlob, replication.StatePushManifest, &replication.ManifestPusher{BaseHandler: base})
|
||||
sm.AddTransition(replication.StatePushManifest, replication.StatePullManifest, &replication.ManifestPuller{BaseHandler: base})
|
||||
return nil
|
||||
}
|
||||
|
||||
func addImgDeleteTransition(sm *SM) error {
|
||||
deleter := replication.NewDeleter(sm.Parms.Repository, sm.Parms.Tags, sm.Parms.TargetURL,
|
||||
sm.Parms.TargetUsername, sm.Parms.TargetPassword, sm.Logger)
|
||||
|
||||
sm.AddTransition(models.JobRunning, replication.StateDelete, deleter)
|
||||
sm.AddTransition(replication.StateDelete, models.JobFinished, &StatusUpdater{DummyHandler{JobID: sm.JobID}, models.JobFinished})
|
||||
|
||||
return nil
|
||||
}
|
27
job/utils/logger.go
Normal file
27
job/utils/logger.go
Normal file
@ -0,0 +1,27 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/vmware/harbor/job/config"
|
||||
"github.com/vmware/harbor/utils/log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// NewLogger create a logger for a speicified job
|
||||
func NewLogger(jobID int64) *log.Logger {
|
||||
logFile := GetJobLogPath(jobID)
|
||||
f, err := os.OpenFile(logFile, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0660)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to open log file %s, the log of job %d will be printed to standard output", logFile, jobID)
|
||||
f = os.Stdout
|
||||
}
|
||||
return log.New(f, log.NewTextFormatter(), log.InfoLevel)
|
||||
}
|
||||
|
||||
// GetJobLogPath returns the absolute path in which the job log file is located.
|
||||
func GetJobLogPath(jobID int64) string {
|
||||
fn := fmt.Sprintf("job_%d.log", jobID)
|
||||
return filepath.Join(config.LogDir(), fn)
|
||||
}
|
123
job/workerpool.go
Normal file
123
job/workerpool.go
Normal file
@ -0,0 +1,123 @@
|
||||
package job
|
||||
|
||||
import (
|
||||
"github.com/vmware/harbor/dao"
|
||||
"github.com/vmware/harbor/job/config"
|
||||
"github.com/vmware/harbor/models"
|
||||
"github.com/vmware/harbor/utils/log"
|
||||
)
|
||||
|
||||
type workerPool struct {
|
||||
workerChan chan *Worker
|
||||
workerList []*Worker
|
||||
}
|
||||
|
||||
// WorkerPool is a set of workers each worker is associate to a statemachine for handling jobs.
|
||||
// it consists of a channel for free workers and a list to all workers
|
||||
var WorkerPool *workerPool
|
||||
|
||||
// StopJobs accepts a list of jobs and will try to stop them if any of them is being executed by the worker.
|
||||
func (wp *workerPool) StopJobs(jobs []int64) {
|
||||
log.Debugf("Works working on jobs: %v will be stopped", jobs)
|
||||
for _, id := range jobs {
|
||||
for _, w := range wp.workerList {
|
||||
if w.SM.JobID == id {
|
||||
log.Debugf("found a worker whose job ID is %d, will try to stop it", id)
|
||||
w.SM.Stop(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Worker consists of a channel for job from which worker gets the next job to handle, and a pointer to a statemachine,
|
||||
// the actual work to handle the job is done via state machine.
|
||||
type Worker struct {
|
||||
ID int
|
||||
RepJobs chan int64
|
||||
SM *SM
|
||||
quit chan bool
|
||||
}
|
||||
|
||||
// Start is a loop worker gets id from its channel and handle it.
|
||||
func (w *Worker) Start() {
|
||||
go func() {
|
||||
for {
|
||||
WorkerPool.workerChan <- w
|
||||
select {
|
||||
case jobID := <-w.RepJobs:
|
||||
log.Debugf("worker: %d, will handle job: %d", w.ID, jobID)
|
||||
w.handleRepJob(jobID)
|
||||
case q := <-w.quit:
|
||||
if q {
|
||||
log.Debugf("worker: %d, will stop.", w.ID)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Stop ...
|
||||
func (w *Worker) Stop() {
|
||||
go func() {
|
||||
w.quit <- true
|
||||
}()
|
||||
}
|
||||
|
||||
func (w *Worker) handleRepJob(id int64) {
|
||||
err := w.SM.Reset(id)
|
||||
if err != nil {
|
||||
log.Errorf("Worker %d, failed to re-initialize statemachine for job: %d, error: %v", w.ID, id, err)
|
||||
err2 := dao.UpdateRepJobStatus(id, models.JobError)
|
||||
if err2 != nil {
|
||||
log.Errorf("Failed to update job status to ERROR, job: %d, error:%v", id, err2)
|
||||
}
|
||||
return
|
||||
}
|
||||
if w.SM.Parms.Enabled == 0 {
|
||||
log.Debugf("The policy of job:%d is disabled, will cancel the job")
|
||||
_ = dao.UpdateRepJobStatus(id, models.JobCanceled)
|
||||
} else {
|
||||
w.SM.Start(models.JobRunning)
|
||||
}
|
||||
}
|
||||
|
||||
// NewWorker returns a pointer to new instance of worker
|
||||
func NewWorker(id int) *Worker {
|
||||
w := &Worker{
|
||||
ID: id,
|
||||
RepJobs: make(chan int64),
|
||||
quit: make(chan bool),
|
||||
SM: &SM{},
|
||||
}
|
||||
w.SM.Init()
|
||||
return w
|
||||
}
|
||||
|
||||
// InitWorkerPool create workers according to configuration.
|
||||
func InitWorkerPool() {
|
||||
WorkerPool = &workerPool{
|
||||
workerChan: make(chan *Worker, config.MaxJobWorkers()),
|
||||
workerList: make([]*Worker, 0, config.MaxJobWorkers()),
|
||||
}
|
||||
for i := 0; i < config.MaxJobWorkers(); i++ {
|
||||
worker := NewWorker(i)
|
||||
WorkerPool.workerList = append(WorkerPool.workerList, worker)
|
||||
worker.Start()
|
||||
log.Debugf("worker %d started", worker.ID)
|
||||
}
|
||||
}
|
||||
|
||||
// Dispatch will listen to the jobQueue of job service and try to pick a free worker from the worker pool and assign the job to it.
|
||||
func Dispatch() {
|
||||
for {
|
||||
select {
|
||||
case job := <-jobQueue:
|
||||
go func(jobID int64) {
|
||||
log.Debugf("Trying to dispatch job: %d", jobID)
|
||||
worker := <-WorkerPool.workerChan
|
||||
worker.RepJobs <- jobID
|
||||
}(job)
|
||||
}
|
||||
}
|
||||
}
|
5
jobservice/conf/app.conf
Normal file
5
jobservice/conf/app.conf
Normal file
@ -0,0 +1,5 @@
|
||||
appname = jobservice
|
||||
runmode = dev
|
||||
|
||||
[dev]
|
||||
httpport = 80
|
10
jobservice/error.json
Normal file
10
jobservice/error.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"job_type": "notexist",
|
||||
"options": {
|
||||
"whatever": "whatever"
|
||||
},
|
||||
"parms": {
|
||||
"test": "test"
|
||||
},
|
||||
"cron_str": ""
|
||||
}
|
15
jobservice/main.go
Normal file
15
jobservice/main.go
Normal file
@ -0,0 +1,15 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/astaxie/beego"
|
||||
"github.com/vmware/harbor/dao"
|
||||
"github.com/vmware/harbor/job"
|
||||
)
|
||||
|
||||
func main() {
|
||||
dao.InitDB()
|
||||
initRouters()
|
||||
job.InitWorkerPool()
|
||||
go job.Dispatch()
|
||||
beego.Run()
|
||||
}
|
12
jobservice/my_start.sh
Executable file
12
jobservice/my_start.sh
Executable file
@ -0,0 +1,12 @@
|
||||
export MYSQL_HOST=127.0.0.1
|
||||
export MYSQL_PORT=3306
|
||||
export MYSQL_USR=root
|
||||
export MYSQL_PWD=root123
|
||||
export LOG_LEVEL=debug
|
||||
export LOCAL_HARBOR_URL=http://127.0.0.1/
|
||||
export UI_SECRET=abcdef
|
||||
#export UI_USR=admin
|
||||
#export UI_PWD=Harbor12345
|
||||
export MAX_JOB_WORKERS=1
|
||||
|
||||
./jobservice
|
3
jobservice/populate.sql
Normal file
3
jobservice/populate.sql
Normal file
@ -0,0 +1,3 @@
|
||||
use registry;
|
||||
insert into replication_target (name, url, username, password) values ('test', 'http://10.117.171.31', 'admin', 'Harbor12345');
|
||||
insert into replication_policy (name, project_id, target_id, enabled, start_time) value ('test_policy', 1, 1, 1, NOW());
|
13
jobservice/router.go
Normal file
13
jobservice/router.go
Normal file
@ -0,0 +1,13 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
api "github.com/vmware/harbor/api/jobs"
|
||||
|
||||
"github.com/astaxie/beego"
|
||||
)
|
||||
|
||||
func initRouters() {
|
||||
beego.Router("/api/jobs/replication", &api.ReplicationJob{})
|
||||
beego.Router("/api/jobs/replication/:id/log", &api.ReplicationJob{}, "get:GetLog")
|
||||
beego.Router("/api/jobs/replication/actions", &api.ReplicationJob{}, "post:HandleAction")
|
||||
}
|
7
jobservice/start_db.sh
Executable file
7
jobservice/start_db.sh
Executable file
@ -0,0 +1,7 @@
|
||||
#export MYQL_ROOT_PASSWORD=root123
|
||||
docker run --name harbor_mysql -d -e MYSQL_ROOT_PASSWORD=root123 -p 3306:3306 -v /devdata/database:/var/lib/mysql harbor/mysql:dev
|
||||
|
||||
echo "sleep 10 seconds..."
|
||||
sleep 10
|
||||
|
||||
mysql -h 127.0.0.1 -uroot -proot123 < ./populate.sql
|
4
jobservice/stop.json
Normal file
4
jobservice/stop.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"policy_id":1,
|
||||
"action":"stop"
|
||||
}
|
1
jobservice/sync_policy.json
Normal file
1
jobservice/sync_policy.json
Normal file
@ -0,0 +1 @@
|
||||
{"policy_id": 1}
|
1
jobservice/sync_repo.json
Normal file
1
jobservice/sync_repo.json
Normal file
@ -0,0 +1 @@
|
||||
{"policy_id": 1, "repository":"library/ubuntu", "tags":["12.04","11.11"]}
|
17
jobservice/test.json
Normal file
17
jobservice/test.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"job_type": "transfer_img_out",
|
||||
"options": {
|
||||
"whatever": "whatever"
|
||||
},
|
||||
"parms": {
|
||||
"secret": "mysecret",
|
||||
"image": "ubuntu",
|
||||
"targets": [{
|
||||
"url": "127.0.0.1:5000",
|
||||
"username": "admin",
|
||||
"password": "admin"
|
||||
}]
|
||||
|
||||
},
|
||||
"cron_str": ""
|
||||
}
|
11
models/base.go
Normal file
11
models/base.go
Normal file
@ -0,0 +1,11 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/astaxie/beego/orm"
|
||||
)
|
||||
|
||||
func init() {
|
||||
orm.RegisterModel(new(RepTarget),
|
||||
new(RepPolicy),
|
||||
new(RepJob))
|
||||
}
|
138
models/replication_job.go
Normal file
138
models/replication_job.go
Normal file
@ -0,0 +1,138 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/astaxie/beego/validation"
|
||||
)
|
||||
|
||||
const (
|
||||
//JobPending ...
|
||||
JobPending string = "pending"
|
||||
//JobRunning ...
|
||||
JobRunning string = "running"
|
||||
//JobError ...
|
||||
JobError string = "error"
|
||||
//JobStopped ...
|
||||
JobStopped string = "stopped"
|
||||
//JobFinished ...
|
||||
JobFinished string = "finished"
|
||||
//JobCanceled ...
|
||||
JobCanceled string = "canceled"
|
||||
//JobContinue is the status returned by statehandler to tell statemachine to move to next possible state based on trasition table.
|
||||
JobContinue string = "_continue"
|
||||
//RepOpTransfer represents the operation of a job to transfer repository to a remote registry/harbor instance.
|
||||
RepOpTransfer string = "transfer"
|
||||
//RepOpDelete represents the operation of a job to remove repository from a remote registry/harbor instance.
|
||||
RepOpDelete string = "delete"
|
||||
//UISecretCookie is the cookie name to contain the UI secret
|
||||
UISecretCookie string = "uisecret"
|
||||
)
|
||||
|
||||
// RepPolicy is the model for a replication policy, which associate to a project and a target (destination)
|
||||
type RepPolicy struct {
|
||||
ID int64 `orm:"column(id)" json:"id"`
|
||||
ProjectID int64 `orm:"column(project_id)" json:"project_id"`
|
||||
TargetID int64 `orm:"column(target_id)" json:"target_id"`
|
||||
Name string `orm:"column(name)" json:"name"`
|
||||
// Target RepTarget `orm:"-" json:"target"`
|
||||
Enabled int `orm:"column(enabled)" json:"enabled"`
|
||||
Description string `orm:"column(description)" json:"description"`
|
||||
CronStr string `orm:"column(cron_str)" json:"cron_str"`
|
||||
StartTime time.Time `orm:"column(start_time)" json:"start_time"`
|
||||
CreationTime time.Time `orm:"column(creation_time);auto_now_add" json:"creation_time"`
|
||||
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 {
|
||||
ID int64 `orm:"column(id)" json:"id"`
|
||||
Status string `orm:"column(status)" json:"status"`
|
||||
Repository string `orm:"column(repository)" json:"repository"`
|
||||
PolicyID int64 `orm:"column(policy_id)" json:"policy_id"`
|
||||
Operation string `orm:"column(operation)" json:"operation"`
|
||||
Tags string `orm:"column(tags)" json:"-"`
|
||||
TagList []string `orm:"-" json:"tags"`
|
||||
// Policy RepPolicy `orm:"-" json:"policy"`
|
||||
CreationTime time.Time `orm:"column(creation_time);auto_now_add" json:"creation_time"`
|
||||
UpdateTime time.Time `orm:"column(update_time);auto_now" json:"update_time"`
|
||||
}
|
||||
|
||||
// RepTarget is the model for a replication targe, i.e. destination, which wraps the endpoint URL and username/password of a remote registry.
|
||||
type RepTarget struct {
|
||||
ID int64 `orm:"column(id)" json:"id"`
|
||||
URL string `orm:"column(url)" json:"endpoint"`
|
||||
Name string `orm:"column(name)" json:"name"`
|
||||
Username string `orm:"column(username)" json:"username"`
|
||||
Password string `orm:"column(password)" json:"password"`
|
||||
CreationTime time.Time `orm:"column(creation_time);auto_now_add" json:"creation_time"`
|
||||
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 (r *RepTarget) TableName() string {
|
||||
return "replication_target"
|
||||
}
|
||||
|
||||
//TableName is required by by beego orm to map RepJob to table replication_job
|
||||
func (r *RepJob) TableName() string {
|
||||
return "replication_job"
|
||||
}
|
||||
|
||||
//TableName is required by by beego orm to map RepPolicy to table replication_policy
|
||||
func (r *RepPolicy) TableName() string {
|
||||
return "replication_policy"
|
||||
}
|
@ -13,7 +13,7 @@
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package utils
|
||||
package cache
|
||||
|
||||
import (
|
||||
"os"
|
@ -20,10 +20,12 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/vmware/harbor/api"
|
||||
"github.com/vmware/harbor/dao"
|
||||
"github.com/vmware/harbor/models"
|
||||
svc_utils "github.com/vmware/harbor/service/utils"
|
||||
"github.com/vmware/harbor/service/cache"
|
||||
"github.com/vmware/harbor/utils/log"
|
||||
"github.com/vmware/harbor/utils/registry"
|
||||
|
||||
"github.com/astaxie/beego"
|
||||
)
|
||||
@ -54,7 +56,8 @@ func (n *NotificationHandler) Post() {
|
||||
log.Errorf("Failed to match the media type against pattern, error: %v", err)
|
||||
matched = false
|
||||
}
|
||||
if matched && strings.HasPrefix(e.Request.UserAgent, "docker") {
|
||||
if matched && (strings.HasPrefix(e.Request.UserAgent, "docker") ||
|
||||
strings.ToLower(strings.TrimSpace(e.Request.UserAgent)) == strings.ToLower(registry.UserAgent)) {
|
||||
username = e.Actor.Name
|
||||
action = e.Action
|
||||
repo = e.Target.Repository
|
||||
@ -67,14 +70,21 @@ func (n *NotificationHandler) Post() {
|
||||
if username == "" {
|
||||
username = "anonymous"
|
||||
}
|
||||
|
||||
if action == "pull" && username == "job-service-user" {
|
||||
return
|
||||
}
|
||||
|
||||
go dao.AccessLog(username, project, repo, repoTag, action)
|
||||
if action == "push" {
|
||||
go func() {
|
||||
err2 := svc_utils.RefreshCatalogCache()
|
||||
err2 := cache.RefreshCatalogCache()
|
||||
if err2 != nil {
|
||||
log.Errorf("Error happens when refreshing cache: %v", err2)
|
||||
}
|
||||
}()
|
||||
|
||||
go api.TriggerReplicationByRepository(repo, []string{repoTag}, models.RepOpTransfer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ import (
|
||||
|
||||
"github.com/vmware/harbor/auth"
|
||||
"github.com/vmware/harbor/models"
|
||||
//svc_utils "github.com/vmware/harbor/service/utils"
|
||||
svc_utils "github.com/vmware/harbor/service/utils"
|
||||
"github.com/vmware/harbor/utils/log"
|
||||
|
||||
"github.com/astaxie/beego"
|
||||
@ -38,20 +38,27 @@ type Handler struct {
|
||||
// checkes the permission agains local DB and generates jwt token.
|
||||
func (h *Handler) Get() {
|
||||
|
||||
var username, password string
|
||||
request := h.Ctx.Request
|
||||
log.Infof("request url: %v", request.URL.String())
|
||||
username, password, _ := request.BasicAuth()
|
||||
authenticated := authenticate(username, password)
|
||||
service := h.GetString("service")
|
||||
scopes := h.GetStrings("scope")
|
||||
|
||||
if len(scopes) == 0 && !authenticated {
|
||||
log.Info("login request with invalid credentials")
|
||||
h.CustomAbort(http.StatusUnauthorized, "")
|
||||
}
|
||||
access := GetResourceActions(scopes)
|
||||
for _, a := range access {
|
||||
FilterAccess(username, authenticated, a)
|
||||
log.Infof("request url: %v", request.URL.String())
|
||||
|
||||
if svc_utils.VerifySecret(request) {
|
||||
log.Debugf("Will grant all access as this request is from job service with legal secret.")
|
||||
username = "job-service-user"
|
||||
} else {
|
||||
username, password, _ = request.BasicAuth()
|
||||
authenticated := authenticate(username, password)
|
||||
|
||||
if len(scopes) == 0 && !authenticated {
|
||||
log.Info("login request with invalid credentials")
|
||||
h.CustomAbort(http.StatusUnauthorized, "")
|
||||
}
|
||||
for _, a := range access {
|
||||
FilterAccess(username, authenticated, a)
|
||||
}
|
||||
}
|
||||
h.serveToken(username, service, access)
|
||||
}
|
||||
|
33
service/utils/utils.go
Normal file
33
service/utils/utils.go
Normal file
@ -0,0 +1,33 @@
|
||||
/*
|
||||
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 utils contains methods to support security, cache, and webhook functions.
|
||||
package utils
|
||||
|
||||
import (
|
||||
"github.com/vmware/harbor/utils/log"
|
||||
"net/http"
|
||||
"os"
|
||||
)
|
||||
|
||||
// VerifySecret verifies the UI_SECRET cookie in a http request.
|
||||
func VerifySecret(r *http.Request) bool {
|
||||
secret := os.Getenv("UI_SECRET")
|
||||
c, err := r.Cookie("uisecret")
|
||||
if err != nil {
|
||||
log.Errorf("Failed to get secret cookie, error: %v", err)
|
||||
}
|
||||
return c != nil && c.Value == secret
|
||||
}
|
@ -51,6 +51,7 @@ nav .container-custom {
|
||||
|
||||
.nav-custom .active {
|
||||
border-bottom: 3px solid #EFEFEF;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
|
@ -28,8 +28,6 @@
|
||||
|
||||
.switch-pane-tabs {
|
||||
width: 265px;
|
||||
min-width: 265px;
|
||||
float: right;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
@ -41,6 +39,7 @@
|
||||
|
||||
.switch-pane-tabs li .active {
|
||||
border-bottom: 2px solid rgb(0, 84, 190);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.switch-pane-drop-down {
|
||||
|
@ -36,7 +36,7 @@
|
||||
|
||||
|
||||
|
||||
.css-form input.ng-invalid.ng-dirty {
|
||||
.css-form input.ng-invalid.ng-touched {
|
||||
border-color: red;
|
||||
}
|
||||
|
||||
|
@ -6,20 +6,22 @@
|
||||
.module('harbor.details')
|
||||
.directive('retrieveProjects', retrieveProjects);
|
||||
|
||||
RetrieveProjectsController.$inject = ['$scope', 'nameFilter', '$filter', 'CurrentProjectMemberService', 'ListProjectService', '$routeParams', '$route', '$location'];
|
||||
RetrieveProjectsController.$inject = ['$scope', 'nameFilter', '$filter', 'ListProjectService', '$location', 'getParameterByName', 'CurrentProjectMemberService'];
|
||||
|
||||
function RetrieveProjectsController($scope, nameFilter, $filter, CurrentProjectMemberService, ListProjectService, $routeParams, $route, $location) {
|
||||
function RetrieveProjectsController($scope, nameFilter, $filter, ListProjectService, $location, getParameterByName, CurrentProjectMemberService) {
|
||||
var vm = this;
|
||||
|
||||
vm.projectName = '';
|
||||
vm.isOpen = false;
|
||||
|
||||
if($route.current.params.is_public) {
|
||||
vm.isPublic = $route.current.params.is_public === 'true' ? 1 : 0;
|
||||
if(getParameterByName('is_public', $location.absUrl())) {
|
||||
vm.isPublic = getParameterByName('is_public', $location.absUrl()) === 'true' ? 1 : 0;
|
||||
vm.publicity = (vm.isPublic === 1) ? true : false;
|
||||
}
|
||||
|
||||
vm.retrieve = retrieve;
|
||||
vm.filterInput = "";
|
||||
vm.selectItem = selectItem;
|
||||
vm.checkProjectMember = checkProjectMember;
|
||||
|
||||
$scope.$watch('vm.selectedProject', function(current, origin) {
|
||||
@ -28,9 +30,6 @@
|
||||
}
|
||||
});
|
||||
|
||||
vm.filterInput = "";
|
||||
vm.selectItem = selectItem;
|
||||
|
||||
$scope.$watch('vm.publicity', function(current, origin) {
|
||||
vm.publicity = current ? true : false;
|
||||
vm.isPublic = vm.publicity ? 1 : 0;
|
||||
@ -56,15 +55,17 @@
|
||||
|
||||
vm.selectedProject = vm.projects[0];
|
||||
|
||||
if($routeParams.project_id){
|
||||
if(getParameterByName('project_id', $location.absUrl())){
|
||||
angular.forEach(vm.projects, function(value, index) {
|
||||
if(value['ProjectId'] === Number($routeParams.project_id)) {
|
||||
if(value['ProjectId'] === Number(getParameterByName('project_id', $location.absUrl()))) {
|
||||
vm.selectedProject = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
vm.checkProjectMember(vm.selectedProject.ProjectId);
|
||||
|
||||
$location.search('project_id', vm.selectedProject.ProjectId);
|
||||
vm.checkProjectMember(vm.selectedProject.ProjectId);
|
||||
|
||||
vm.resultCount = vm.projects.length;
|
||||
|
||||
$scope.$watch('vm.filterInput', function(current, origin) {
|
||||
@ -76,6 +77,17 @@
|
||||
console.log('Failed to list projects:' + response);
|
||||
}
|
||||
|
||||
function selectItem(item) {
|
||||
vm.selectedProject = item;
|
||||
$location.search('project_id', vm.selectedProject.ProjectId);
|
||||
}
|
||||
|
||||
$scope.$on('$locationChangeSuccess', function(e) {
|
||||
var projectId = getParameterByName('project_id', $location.absUrl());
|
||||
vm.checkProjectMember(projectId);
|
||||
vm.isOpen = false;
|
||||
});
|
||||
|
||||
function checkProjectMember(projectId) {
|
||||
CurrentProjectMemberService(projectId)
|
||||
.success(getCurrentProjectMemberSuccess)
|
||||
@ -88,18 +100,10 @@
|
||||
}
|
||||
|
||||
function getCurrentProjectMemberFailed(data, status) {
|
||||
console.log('Failed get current project member:' + status);
|
||||
console.log('Use has no member for current project:' + status);
|
||||
vm.isProjectMember = false;
|
||||
}
|
||||
|
||||
function selectItem(item) {
|
||||
vm.selectedId = item.ProjectId;
|
||||
vm.selectedProject = item;
|
||||
vm.checkProjectMember(vm.selectedProject.ProjectId);
|
||||
vm.isOpen = false;
|
||||
$location.search('project_id', vm.selectedProject.ProjectId);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function retrieveProjects() {
|
||||
@ -113,7 +117,6 @@
|
||||
'isProjectMember': '='
|
||||
},
|
||||
link: link,
|
||||
replace: true,
|
||||
controller: RetrieveProjectsController,
|
||||
bindToController: true,
|
||||
controllerAs: 'vm'
|
||||
@ -126,10 +129,10 @@
|
||||
|
||||
function clickHandler(e) {
|
||||
$('[data-toggle="popover"]').each(function () {
|
||||
//the 'is' for buttons that trigger popups
|
||||
//the 'has' for icons within a button that triggers a popup
|
||||
if (!$(this).is(e.target) && $(this).has(e.target).length === 0 && $('.popover').has(e.target).length === 0) {
|
||||
$(this).parent().popover('hide');
|
||||
if (!$(this).is(e.target) &&
|
||||
$(this).has(e.target).length === 0 &&
|
||||
$('.popover').has(e.target).length === 0) {
|
||||
$(this).parent().popover('hide');
|
||||
}
|
||||
});
|
||||
var targetId = $(e.target).attr('id');
|
||||
|
@ -34,7 +34,6 @@
|
||||
var directive = {
|
||||
restrict: 'E',
|
||||
templateUrl: '/static/ng/resources/js/components/details/switch-pane-projects.directive.html',
|
||||
replace: true,
|
||||
scope: {
|
||||
'isOpen': '=',
|
||||
'selectedProject': '='
|
||||
|
@ -6,9 +6,9 @@
|
||||
.module('harbor.log')
|
||||
.directive('listLog', listLog);
|
||||
|
||||
ListLogController.$inject = ['$scope','ListLogService', '$routeParams'];
|
||||
ListLogController.$inject = ['$scope','ListLogService', 'getParameterByName', '$location'];
|
||||
|
||||
function ListLogController($scope, ListLogService, $routeParams) {
|
||||
function ListLogController($scope, ListLogService, getParameterByName, $location) {
|
||||
var vm = this;
|
||||
vm.isOpen = false;
|
||||
|
||||
@ -22,7 +22,7 @@
|
||||
vm.search = search;
|
||||
vm.showAdvancedSearch = showAdvancedSearch;
|
||||
|
||||
vm.projectId = $routeParams.project_id;
|
||||
vm.projectId = getParameterByName('project_id', $location.absUrl());
|
||||
vm.queryParams = {
|
||||
'beginTimestamp' : vm.beginTimestamp,
|
||||
'endTimestamp' : vm.endTimestamp,
|
||||
@ -32,6 +32,18 @@
|
||||
};
|
||||
retrieve(vm.queryParams);
|
||||
|
||||
$scope.$on('$locationChangeSuccess', function() {
|
||||
vm.projectId = getParameterByName('project_id', $location.absUrl());
|
||||
vm.queryParams = {
|
||||
'beginTimestamp' : vm.beginTimestamp,
|
||||
'endTimestamp' : vm.endTimestamp,
|
||||
'keywords' : vm.keywords,
|
||||
'projectId': vm.projectId,
|
||||
'username' : vm.username
|
||||
};
|
||||
retrieve(vm.queryParams);
|
||||
});
|
||||
|
||||
function search(e) {
|
||||
if(e.op[0] === 'all') {
|
||||
vm.queryParams.keywords = '';
|
||||
@ -91,7 +103,6 @@
|
||||
var directive = {
|
||||
restrict: 'E',
|
||||
templateUrl: '/static/ng/resources/js/components/log/list-log.directive.html',
|
||||
replace: true,
|
||||
scope: true,
|
||||
controller: ListLogController,
|
||||
controllerAs: 'vm',
|
||||
|
@ -6,20 +6,25 @@
|
||||
.module('harbor.project.member')
|
||||
.directive('listProjectMember', listProjectMember);
|
||||
|
||||
ListProjectMemberController.$inject = ['$scope', 'ListProjectMemberService', '$routeParams', 'currentUser'];
|
||||
ListProjectMemberController.$inject = ['$scope', 'ListProjectMemberService', 'getParameterByName', '$location', 'currentUser'];
|
||||
|
||||
function ListProjectMemberController($scope, ListProjectMemberService, $routeParams, currentUser) {
|
||||
function ListProjectMemberController($scope, ListProjectMemberService, getParameterByName, $location, currentUser) {
|
||||
var vm = this;
|
||||
|
||||
vm.isOpen = false;
|
||||
vm.search = search;
|
||||
vm.addProjectMember = addProjectMember;
|
||||
vm.retrieve = retrieve;
|
||||
vm.projectId = $routeParams.project_id;
|
||||
vm.username = "";
|
||||
vm.username = '';
|
||||
|
||||
vm.projectId = getParameterByName('project_id', $location.absUrl());
|
||||
vm.retrieve();
|
||||
|
||||
$scope.$on('$locationChangeSuccess', function() {
|
||||
vm.projectId = getParameterByName('project_id', $location.absUrl());
|
||||
vm.retrieve();
|
||||
});
|
||||
|
||||
function search(e) {
|
||||
vm.projectId = e.projectId;
|
||||
vm.username = e.username;
|
||||
@ -55,7 +60,6 @@
|
||||
var directive = {
|
||||
restrict: 'E',
|
||||
templateUrl: '/static/ng/resources/js/components/project-member/list-project-member.directive.html',
|
||||
replace: true,
|
||||
scope: true,
|
||||
controller: ListProjectMemberController,
|
||||
controllerAs: 'vm',
|
||||
|
@ -5,9 +5,9 @@
|
||||
.module('harbor.repository')
|
||||
.directive('listRepository', listRepository);
|
||||
|
||||
ListRepositoryController.$inject = ['$scope', 'ListRepositoryService', 'DeleteRepositoryService', '$routeParams', '$filter', 'trFilter', '$location'];
|
||||
ListRepositoryController.$inject = ['$scope', 'ListRepositoryService', 'DeleteRepositoryService', '$filter', 'trFilter', '$location', 'getParameterByName'];
|
||||
|
||||
function ListRepositoryController($scope, ListRepositoryService, DeleteRepositoryService, $routeParams, $filter, trFilter, $location) {
|
||||
function ListRepositoryController($scope, ListRepositoryService, DeleteRepositoryService, $filter, trFilter, $location, getParameterByName) {
|
||||
var vm = this;
|
||||
|
||||
vm.filterInput = '';
|
||||
@ -23,10 +23,17 @@
|
||||
}
|
||||
|
||||
vm.retrieve = retrieve;
|
||||
vm.projectId = $routeParams.project_id;
|
||||
vm.tagCount = {};
|
||||
|
||||
vm.projectId = getParameterByName('project_id', $location.absUrl());
|
||||
vm.retrieve();
|
||||
|
||||
$scope.$on('$locationChangeSuccess', function() {
|
||||
vm.projectId = getParameterByName('project_id', $location.absUrl());
|
||||
vm.retrieve();
|
||||
});
|
||||
|
||||
|
||||
$scope.$watch('vm.repositories', function(current) {
|
||||
if(current) {
|
||||
vm.repositories = current || [];
|
||||
@ -99,7 +106,6 @@
|
||||
var directive = {
|
||||
restrict: 'E',
|
||||
templateUrl: '/static/ng/resources/js/components/repository/list-repository.directive.html',
|
||||
replace: true,
|
||||
controller: ListRepositoryController,
|
||||
controllerAs: 'vm',
|
||||
bindToController: true
|
||||
|
@ -11,7 +11,7 @@
|
||||
function ToggleAdminController($scope, ToggleAdminService) {
|
||||
var vm = this;
|
||||
|
||||
vm.isAdmin = (vm.hasAdminRole === 1) ? true : false;
|
||||
vm.isAdmin = (vm.hasAdminRole == 1) ? true : false;
|
||||
vm.toggle = toggle;
|
||||
|
||||
function toggle() {
|
||||
|
@ -15,11 +15,6 @@
|
||||
'harbor.layout.index',
|
||||
'harbor.layout.dashboard',
|
||||
'harbor.layout.project',
|
||||
'harbor.layout.repository',
|
||||
'harbor.layout.project.member',
|
||||
'harbor.layout.user',
|
||||
'harbor.layout.system.management',
|
||||
'harbor.layout.log',
|
||||
'harbor.layout.admin.option',
|
||||
'harbor.layout.search',
|
||||
'harbor.services.i18n',
|
||||
@ -27,6 +22,9 @@
|
||||
'harbor.services.user',
|
||||
'harbor.services.repository',
|
||||
'harbor.services.project.member',
|
||||
'harbor.services.replication.policy',
|
||||
'harbor.services.replication.job',
|
||||
'harbor.services.destination',
|
||||
'harbor.summary',
|
||||
'harbor.optional.menu',
|
||||
'harbor.modal.dialog',
|
||||
@ -38,6 +36,8 @@
|
||||
'harbor.project.member',
|
||||
'harbor.user',
|
||||
'harbor.log',
|
||||
'harbor.validator'
|
||||
'harbor.validator',
|
||||
'harbor.replication',
|
||||
'harbor.system.management'
|
||||
]);
|
||||
})();
|
@ -3,24 +3,6 @@
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('harbor.layout.admin.option')
|
||||
.config(routeConfig);
|
||||
|
||||
function routeConfig($routeProvider) {
|
||||
$routeProvider
|
||||
.when('/all_user', {
|
||||
'templateUrl': '/static/ng/resources/js/layout/user/user.controller.html',
|
||||
'controller': 'UserController',
|
||||
'controllerAs': 'vm'
|
||||
})
|
||||
.when('/system_management', {
|
||||
'templateUrl': '/static/ng/resources/js/layout/system-management/system-management.controller.html',
|
||||
'controller': 'SystemManagementController',
|
||||
'controllerAs': 'vm'
|
||||
})
|
||||
.otherwise({
|
||||
'redirectTo': '/'
|
||||
});
|
||||
}
|
||||
.module('harbor.layout.admin.option');
|
||||
|
||||
})();
|
@ -6,6 +6,8 @@
|
||||
.module('harbor.layout.admin.option')
|
||||
.controller('AdminOptionController', AdminOptionController);
|
||||
|
||||
AdminOptionController.$inject = [];
|
||||
|
||||
function AdminOptionController() {
|
||||
var vm = this;
|
||||
vm.toggle = false;
|
||||
|
@ -4,29 +4,8 @@
|
||||
|
||||
angular
|
||||
.module('harbor.details')
|
||||
.config(routeConfig)
|
||||
.filter('name', nameFilter);
|
||||
|
||||
|
||||
function routeConfig($routeProvider) {
|
||||
$routeProvider
|
||||
.when('/repositories', {
|
||||
templateUrl: '/static/ng/resources/js/layout/repository/repository.controller.html',
|
||||
controller: 'RepositoryController',
|
||||
controllerAs: 'vm'
|
||||
})
|
||||
.when('/users', {
|
||||
templateUrl: '/static/ng/resources/js/layout/project-member/project-member.controller.html',
|
||||
controller: 'ProjectMemberController',
|
||||
controllerAs: 'vm'
|
||||
})
|
||||
.when('/logs', {
|
||||
templateUrl: '/static/ng/resources/js/layout/log/log.controller.html',
|
||||
controller: 'LogController',
|
||||
controllerAs: 'vm'
|
||||
});
|
||||
}
|
||||
|
||||
function nameFilter() {
|
||||
|
||||
return filter;
|
||||
|
@ -13,6 +13,7 @@
|
||||
vm.isProjectMember = false;
|
||||
|
||||
vm.togglePublicity = togglePublicity;
|
||||
vm.target = 'repositories';
|
||||
|
||||
function togglePublicity(e) {
|
||||
vm.publicity = e.publicity;
|
||||
|
@ -1 +0,0 @@
|
||||
<list-log project-id="vm.projectId"></list-log>
|
@ -1,15 +0,0 @@
|
||||
(function() {
|
||||
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('harbor.layout.log')
|
||||
.controller('LogController', LogController);
|
||||
|
||||
LogController.$inject = ['$scope'];
|
||||
|
||||
function LogController($scope) {
|
||||
|
||||
}
|
||||
|
||||
})();
|
@ -1,8 +0,0 @@
|
||||
(function() {
|
||||
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('harbor.layout.log', []);
|
||||
|
||||
})();
|
@ -6,27 +6,27 @@
|
||||
.module('harbor.layout.navigation')
|
||||
.directive('navigationDetails', navigationDetails);
|
||||
|
||||
NavigationDetailsController.$inject = ['$window', '$location', '$scope', '$route'];
|
||||
NavigationDetailsController.$inject = ['$window', '$location', '$scope', 'getParameterByName'];
|
||||
|
||||
function NavigationDetailsController($window, $location, $scope, $route) {
|
||||
function NavigationDetailsController($window, $location, $scope, getParameterByName) {
|
||||
var vm = this;
|
||||
|
||||
$scope.$watch('vm.selectedProject', function(current, origin) {
|
||||
if(current) {
|
||||
vm.projectId = current.ProjectId;
|
||||
}
|
||||
vm.projectId = getParameterByName('project_id', $location.absUrl());
|
||||
|
||||
$scope.$on('$locationChangeSuccess', function() {
|
||||
vm.projectId = getParameterByName('project_id', $location.absUrl());
|
||||
});
|
||||
|
||||
vm.url = $location.url();
|
||||
vm.path = $location.path();
|
||||
}
|
||||
|
||||
function navigationDetails() {
|
||||
var directive = {
|
||||
restrict: 'E',
|
||||
templateUrl: '/static/ng/resources/js/layout/navigation/navigation-details.directive.html',
|
||||
templateUrl: '/ng/navigation_detail',
|
||||
link: link,
|
||||
scope: {
|
||||
'selectedProject': '='
|
||||
'target': '='
|
||||
},
|
||||
replace: true,
|
||||
controller: NavigationDetailsController,
|
||||
@ -38,26 +38,24 @@
|
||||
|
||||
function link(scope, element, attrs, ctrl) {
|
||||
|
||||
var visited = ctrl.url.substring(1);
|
||||
var visited = ctrl.path.substring(1);
|
||||
if(visited.indexOf('?') >= 0) {
|
||||
visited = ctrl.url.substring(1, ctrl.url.indexOf('?'));
|
||||
}
|
||||
scope.$watch('vm.selectedProject', function(current) {
|
||||
if(current) {
|
||||
element.find('a').removeClass('active');
|
||||
if(visited) {
|
||||
element.find('a[tag="' + visited + '"]').addClass('active');
|
||||
}else{
|
||||
element.find('a:first').addClass('active');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if(visited) {
|
||||
element.find('a[tag="' + visited + '"]').addClass('active');
|
||||
}else{
|
||||
element.find('a:first').addClass('active');
|
||||
}
|
||||
|
||||
element.find('a').on('click', click);
|
||||
|
||||
function click(event) {
|
||||
element.find('a').removeClass('active');
|
||||
$(event.target).addClass('active');
|
||||
ctrl.target = $(this).attr('tag');
|
||||
scope.$apply();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1 +0,0 @@
|
||||
<list-project-member project-id="vm.projectId"></list-project-member>
|
@ -1,13 +0,0 @@
|
||||
(function() {
|
||||
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('harbor.layout.project.member')
|
||||
.controller('ProjectMemberController', ProjectMemberController);
|
||||
|
||||
function ProjectMemberController($scope) {
|
||||
|
||||
}
|
||||
|
||||
})();
|
@ -1,9 +0,0 @@
|
||||
(function() {
|
||||
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('harbor.layout.project.member', []);
|
||||
|
||||
|
||||
})();
|
@ -1 +0,0 @@
|
||||
<list-repository></list-repository>
|
@ -1,15 +0,0 @@
|
||||
(function() {
|
||||
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('harbor.layout.repository')
|
||||
.controller('RepositoryController', RepositoryController);
|
||||
|
||||
RepositoryController.$inject = ['$scope'];
|
||||
|
||||
function RepositoryController($scope) {
|
||||
|
||||
}
|
||||
|
||||
})();
|
@ -1,8 +0,0 @@
|
||||
(function() {
|
||||
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('harbor.layout.repository', []);
|
||||
|
||||
})();
|
@ -1,63 +0,0 @@
|
||||
|
||||
<form name="form" class="form-horizontal" ng-submit="form.$valid && vm.changeSettings(system)" >
|
||||
<div class="col-md-12">
|
||||
<h5>System Settings</h5>
|
||||
<hr/>
|
||||
</div>
|
||||
<div class="col-md-12 col-md-off-set-1 main-content">
|
||||
<div class="form-group">
|
||||
<label for="hostName" class="col-sm-3 control-label">Host Name:</label>
|
||||
<div class="col-sm-7">
|
||||
<input type="text" class="form-control" id="hostName" ng-model="system.hostName" ng-model-options="{ updateOn: 'blur' }" ng-value="vm.system.hostName" name="uHostName" required>
|
||||
<div ng-messages="form.$dirty && form.uHostName.$error">
|
||||
<span ng-message="required">Host name is required.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="urlProtocol" class="col-sm-3 control-label">URL Protocol:</label>
|
||||
<div class="col-sm-7">
|
||||
<input type="text" class="form-control" id="urlProtocol" ng-model="system.urlProtocol" ng-model-options="{ updateOn: 'blur' }" ng-value="vm.system.urlProtocol" name="uUrlProtocol" required>
|
||||
<div ng-messages="form.$dirty && form.uUrlProtocol.$error">
|
||||
<span ng-message="required">Url protocol is required.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="emailServer" class="col-sm-3 control-label">Email server:</label>
|
||||
<div class="col-sm-7">
|
||||
<input type="text" class="form-control" id="emailServer" ng-model="system.emailServer" ng-model-options="{ updateOn: 'blur' }" ng-value="vm.system.emailServer" name="uEmailServer" required>
|
||||
<div ng-messages="form.$dirty && form.uEmailServer.$error">
|
||||
<span ng-message="required">Email server is required.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="ldapUrl" class="col-sm-3 control-label">LDAP URL:</label>
|
||||
<div class="col-sm-7">
|
||||
<input type="text" class="form-control" id="ldapUrl" ng-model="system.ldapUrl" ng-model-options="{ updateOn: 'blur' }" ng-value="vm.system.ldapUrl" name="uLdapUrl" required>
|
||||
<div ng-messages="form.$dirty && form.uLdapUrl.$error">
|
||||
<span ng-message="required">LDAP URL is required.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<h5>Registration</h5>
|
||||
<hr/>
|
||||
</div>
|
||||
<div class="col-md-12 col-md-off-set-1 main-content">
|
||||
<div class="form-group">
|
||||
<label for="registration" class="col-sm-3 control-label">Registration:</label>
|
||||
<div class="col-sm-7">
|
||||
<select class="form-control" ng-model="vm.currentRegistration" ng-options="r as r.name for r in vm.registrationOptions track by r.value" ng-click="vm.selectRegistration()"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-md-offset-7 col-md-10">
|
||||
<input type="submit" class="btn btn-primary" ng-disabled="form.$invalid" value="Save">
|
||||
<input type="submit" class="btn btn-default" ng-click="vm.cancel()" value="Cancel">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
@ -1,39 +0,0 @@
|
||||
(function() {
|
||||
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('harbor.layout.system.management')
|
||||
.controller('SystemManagementController', SystemManagementController);
|
||||
|
||||
function SystemManagementController() {
|
||||
var vm = this;
|
||||
vm.registrationOptions = [
|
||||
{
|
||||
'name': 'on',
|
||||
'value': true
|
||||
},
|
||||
{
|
||||
'name': 'off',
|
||||
'value': false
|
||||
}
|
||||
];
|
||||
vm.currentRegistration = {
|
||||
'name': 'on',
|
||||
'value': true
|
||||
};
|
||||
|
||||
vm.changeSettings = changeSettings;
|
||||
|
||||
vm.selectRegistration = selectRegistration;
|
||||
|
||||
function selectRegistration() {
|
||||
|
||||
}
|
||||
|
||||
function changeSettings(system) {
|
||||
console.log(system);
|
||||
}
|
||||
}
|
||||
|
||||
})();
|
@ -1,8 +0,0 @@
|
||||
(function() {
|
||||
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('harbor.layout.system.management', []);
|
||||
|
||||
})();
|
@ -1,37 +0,0 @@
|
||||
<div class="search-pane">
|
||||
<div class="form-inline">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" placeholder="" ng-model="vm.username" size="30">
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-primary" type="button" ng-click="vm.searchUser()"><span class="glyphicon glyphicon-search"></span></button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pane project-pane">
|
||||
<div class="sub-pane">
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<th>// 'username' | tr //</th><th>// 'role' | tr //</th><th>// 'email' | tr //</th><th>// 'registration_time' | tr //</th><th>// 'operation' | tr //</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="u in vm.users">
|
||||
<td>//u.username//</td>
|
||||
<td>N/A</td>
|
||||
<td>//u.email//</td>
|
||||
<td>//u.creation_time | dateL : 'YYYY-MM-DD HH:mm:ss'//</td>
|
||||
<td>
|
||||
<toggle-admin has-admin-role="u.HasAdminRole == 1" user-id="//u.UserId//"></toggle-admin>
|
||||
<a href="javascript:void(0)" ng-click="vm.deleteUser(u.UserId)"><span class="glyphicon glyphicon-trash"></span></a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
<div class="col-xs-4 col-md-12 well well-sm well-custom">
|
||||
<div class="col-md-offset-10">//vm.users ? vm.users.length : 0// items</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,58 +0,0 @@
|
||||
(function() {
|
||||
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('harbor.layout.user')
|
||||
.controller('UserController', UserController);
|
||||
|
||||
UserController.$inject = ['ListUserService', 'DeleteUserService'];
|
||||
|
||||
function UserController(ListUserService, DeleteUserService) {
|
||||
var vm = this;
|
||||
|
||||
vm.username = '';
|
||||
vm.searchUser = searchUser;
|
||||
vm.deleteUser = deleteUser;
|
||||
vm.retrieve = retrieve;
|
||||
|
||||
vm.retrieve();
|
||||
|
||||
function searchUser() {
|
||||
vm.retrieve();
|
||||
}
|
||||
|
||||
function deleteUser(userId) {
|
||||
DeleteUserService(userId)
|
||||
.success(deleteUserSuccess)
|
||||
.error(deleteUserFailed);
|
||||
}
|
||||
|
||||
function retrieve() {
|
||||
ListUserService(vm.username)
|
||||
.success(listUserSuccess)
|
||||
.error(listUserFailed);
|
||||
}
|
||||
|
||||
function deleteUserSuccess(data, status) {
|
||||
console.log('Successful delete user.');
|
||||
vm.retrieve();
|
||||
}
|
||||
|
||||
function deleteUserFailed(data, status) {
|
||||
console.log('Failed delete user.');
|
||||
}
|
||||
|
||||
function listUserSuccess(data, status) {
|
||||
vm.users = data;
|
||||
}
|
||||
|
||||
function listUserFailed(data, status) {
|
||||
console.log('Failed list user:' + data);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
})();
|
@ -1,10 +0,0 @@
|
||||
(function() {
|
||||
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('harbor.layout.user', [
|
||||
'harbor.services.user'
|
||||
]);
|
||||
|
||||
})();
|
@ -25,5 +25,6 @@ func initNgRouters() {
|
||||
|
||||
beego.Router("/ng/optional_menu", &ng.OptionalMenuController{})
|
||||
beego.Router("/ng/navigation_header", &ng.NavigationHeaderController{})
|
||||
beego.Router("/ng/navigation_detail", &ng.NavigationDetailController{})
|
||||
beego.Router("/ng/sign_in", &ng.SignInController{})
|
||||
}
|
||||
|
13
ui/router.go
13
ui/router.go
@ -52,17 +52,22 @@ func initRouters() {
|
||||
|
||||
//API:
|
||||
beego.Router("/api/search", &api.SearchAPI{})
|
||||
beego.Router("/api/projects/:pid/members/?:mid", &api.ProjectMemberAPI{})
|
||||
beego.Router("/api/projects/:pid([0-9]+)/members/?:mid", &api.ProjectMemberAPI{})
|
||||
beego.Router("/api/projects/", &api.ProjectAPI{}, "get:List")
|
||||
beego.Router("/api/projects/?:id", &api.ProjectAPI{})
|
||||
beego.Router("/api/statistics", &api.StatisticAPI{})
|
||||
beego.Router("/api/projects/:id/logs/filter", &api.ProjectAPI{}, "post:FilterAccessLog")
|
||||
beego.Router("/api/users", &api.UserAPI{})
|
||||
beego.Router("/api/projects/:id([0-9]+)/logs/filter", &api.ProjectAPI{}, "post:FilterAccessLog")
|
||||
beego.Router("/api/users/?:id", &api.UserAPI{})
|
||||
beego.Router("/api/users/:id/password", &api.UserAPI{}, "put:ChangePassword")
|
||||
beego.Router("/api/users/:id([0-9]+)/password", &api.UserAPI{}, "put:ChangePassword")
|
||||
beego.Router("/api/repositories", &api.RepositoryAPI{})
|
||||
beego.Router("/api/repositories/tags", &api.RepositoryAPI{}, "get:GetTags")
|
||||
beego.Router("/api/repositories/manifests", &api.RepositoryAPI{}, "get:GetManifests")
|
||||
beego.Router("/api/jobs/replication/?:id([0-9]+)", &api.RepJobAPI{})
|
||||
beego.Router("/api/jobs/replication/:id([0-9]+)/log", &api.RepJobAPI{}, "get:GetLog")
|
||||
beego.Router("/api/policies/replication", &api.RepPolicyAPI{})
|
||||
beego.Router("/api/policies/replication/:id([0-9]+)/enablement", &api.RepPolicyAPI{}, "put:UpdateEnablement")
|
||||
beego.Router("/api/targets/?:id([0-9]+)", &api.TargetAPI{})
|
||||
beego.Router("/api/targets/ping", &api.TargetAPI{}, "post:Ping")
|
||||
|
||||
//external service that hosted on harbor process:
|
||||
beego.Router("/service/notifications", &service.NotificationHandler{})
|
||||
|
@ -17,6 +17,7 @@ package utils
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/crypto/pbkdf2"
|
||||
@ -26,3 +27,14 @@ import (
|
||||
func Encrypt(content string, salt string) string {
|
||||
return fmt.Sprintf("%x", pbkdf2.Key([]byte(content), []byte(salt), 4096, 16, sha1.New))
|
||||
}
|
||||
|
||||
// ReversibleEncrypt encrypts the str with base64
|
||||
func ReversibleEncrypt(str string) string {
|
||||
return base64.StdEncoding.EncodeToString([]byte(str))
|
||||
}
|
||||
|
||||
// ReversibleDecrypt decrypts the str with base64
|
||||
func ReversibleDecrypt(str string) (string, error) {
|
||||
b, err := base64.StdEncoding.DecodeString(str)
|
||||
return string(b), err
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ import (
|
||||
var logger = New(os.Stdout, NewTextFormatter(), WarningLevel)
|
||||
|
||||
func init() {
|
||||
logger.callDepth = 3
|
||||
logger.callDepth = 4
|
||||
|
||||
// TODO add item in configuaration file
|
||||
lvl := os.Getenv("LOG_LEVEL")
|
||||
@ -52,6 +52,7 @@ type Logger struct {
|
||||
fmtter Formatter
|
||||
lvl Level
|
||||
callDepth int
|
||||
skipLine bool
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
@ -61,7 +62,7 @@ func New(out io.Writer, fmtter Formatter, lvl Level) *Logger {
|
||||
out: out,
|
||||
fmtter: fmtter,
|
||||
lvl: lvl,
|
||||
callDepth: 2,
|
||||
callDepth: 3,
|
||||
}
|
||||
}
|
||||
|
||||
@ -121,8 +122,7 @@ func (l *Logger) output(record *Record) (err error) {
|
||||
// Debug ...
|
||||
func (l *Logger) Debug(v ...interface{}) {
|
||||
if l.lvl <= DebugLevel {
|
||||
line := line(l.callDepth)
|
||||
record := NewRecord(time.Now(), fmt.Sprint(v...), line, DebugLevel)
|
||||
record := NewRecord(time.Now(), fmt.Sprint(v...), l.getLine(), DebugLevel)
|
||||
l.output(record)
|
||||
}
|
||||
}
|
||||
@ -130,8 +130,7 @@ func (l *Logger) Debug(v ...interface{}) {
|
||||
// Debugf ...
|
||||
func (l *Logger) Debugf(format string, v ...interface{}) {
|
||||
if l.lvl <= DebugLevel {
|
||||
line := line(l.callDepth)
|
||||
record := NewRecord(time.Now(), fmt.Sprintf(format, v...), line, DebugLevel)
|
||||
record := NewRecord(time.Now(), fmt.Sprintf(format, v...), l.getLine(), DebugLevel)
|
||||
l.output(record)
|
||||
}
|
||||
}
|
||||
@ -171,8 +170,7 @@ func (l *Logger) Warningf(format string, v ...interface{}) {
|
||||
// Error ...
|
||||
func (l *Logger) Error(v ...interface{}) {
|
||||
if l.lvl <= ErrorLevel {
|
||||
line := line(l.callDepth)
|
||||
record := NewRecord(time.Now(), fmt.Sprint(v...), line, ErrorLevel)
|
||||
record := NewRecord(time.Now(), fmt.Sprint(v...), l.getLine(), ErrorLevel)
|
||||
l.output(record)
|
||||
}
|
||||
}
|
||||
@ -180,8 +178,7 @@ func (l *Logger) Error(v ...interface{}) {
|
||||
// Errorf ...
|
||||
func (l *Logger) Errorf(format string, v ...interface{}) {
|
||||
if l.lvl <= ErrorLevel {
|
||||
line := line(l.callDepth)
|
||||
record := NewRecord(time.Now(), fmt.Sprintf(format, v...), line, ErrorLevel)
|
||||
record := NewRecord(time.Now(), fmt.Sprintf(format, v...), l.getLine(), ErrorLevel)
|
||||
l.output(record)
|
||||
}
|
||||
}
|
||||
@ -189,8 +186,7 @@ func (l *Logger) Errorf(format string, v ...interface{}) {
|
||||
// Fatal ...
|
||||
func (l *Logger) Fatal(v ...interface{}) {
|
||||
if l.lvl <= FatalLevel {
|
||||
line := line(l.callDepth)
|
||||
record := NewRecord(time.Now(), fmt.Sprint(v...), line, FatalLevel)
|
||||
record := NewRecord(time.Now(), fmt.Sprint(v...), l.getLine(), FatalLevel)
|
||||
l.output(record)
|
||||
}
|
||||
os.Exit(1)
|
||||
@ -199,13 +195,19 @@ func (l *Logger) Fatal(v ...interface{}) {
|
||||
// Fatalf ...
|
||||
func (l *Logger) Fatalf(format string, v ...interface{}) {
|
||||
if l.lvl <= FatalLevel {
|
||||
line := line(l.callDepth)
|
||||
record := NewRecord(time.Now(), fmt.Sprintf(format, v...), line, FatalLevel)
|
||||
record := NewRecord(time.Now(), fmt.Sprintf(format, v...), l.getLine(), FatalLevel)
|
||||
l.output(record)
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func (l *Logger) getLine() string {
|
||||
if l.skipLine {
|
||||
return ""
|
||||
}
|
||||
return line(l.callDepth)
|
||||
}
|
||||
|
||||
// Debug ...
|
||||
func Debug(v ...interface{}) {
|
||||
logger.Debug(v...)
|
||||
|
@ -42,3 +42,19 @@ func NewBasicAuthCredential(username, password string) Credential {
|
||||
func (b *basicAuthCredential) AddAuthorization(req *http.Request) {
|
||||
req.SetBasicAuth(b.username, b.password)
|
||||
}
|
||||
|
||||
type cookieCredential struct {
|
||||
cookie *http.Cookie
|
||||
}
|
||||
|
||||
// NewCookieCredential initialize a cookie based crendential handler, the cookie in parameter will be added to request to registry
|
||||
// if this crendential is attached to a registry client.
|
||||
func NewCookieCredential(c *http.Cookie) Credential {
|
||||
return &cookieCredential{
|
||||
cookie: c,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *cookieCredential) AddAuthorization(req *http.Request) {
|
||||
req.AddCookie(c.cookie)
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ import (
|
||||
|
||||
token_util "github.com/vmware/harbor/service/token"
|
||||
"github.com/vmware/harbor/utils/log"
|
||||
registry_errors "github.com/vmware/harbor/utils/registry/errors"
|
||||
registry_error "github.com/vmware/harbor/utils/registry/error"
|
||||
)
|
||||
|
||||
type scope struct {
|
||||
@ -75,7 +75,9 @@ func (t *tokenHandler) AuthorizeRequest(req *http.Request, params map[string]str
|
||||
hasFrom = true
|
||||
}
|
||||
|
||||
scopes = append(scopes, t.scope)
|
||||
if t.scope != nil {
|
||||
scopes = append(scopes, t.scope)
|
||||
}
|
||||
|
||||
expired := true
|
||||
|
||||
@ -143,11 +145,14 @@ func NewStandardTokenHandler(credential Credential, scopeType, scopeName string,
|
||||
credential: credential,
|
||||
}
|
||||
|
||||
handler.scope = &scope{
|
||||
Type: scopeType,
|
||||
Name: scopeName,
|
||||
Actions: scopeActions,
|
||||
if len(scopeType) != 0 || len(scopeName) != 0 {
|
||||
handler.scope = &scope{
|
||||
Type: scopeType,
|
||||
Name: scopeName,
|
||||
Actions: scopeActions,
|
||||
}
|
||||
}
|
||||
|
||||
handler.tg = handler.generateToken
|
||||
|
||||
return handler
|
||||
@ -182,10 +187,9 @@ func (s *standardTokenHandler) generateToken(realm, service string, scopes []str
|
||||
return
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
err = registry_errors.Error{
|
||||
err = ®istry_error.Error{
|
||||
StatusCode: resp.StatusCode,
|
||||
StatusText: resp.Status,
|
||||
Message: string(b),
|
||||
Detail: string(b),
|
||||
}
|
||||
return
|
||||
}
|
||||
@ -203,12 +207,14 @@ func (s *standardTokenHandler) generateToken(realm, service string, scopes []str
|
||||
|
||||
expiresIn = tk.ExpiresIn
|
||||
|
||||
t, err := time.Parse(time.RFC3339, tk.IssuedAt)
|
||||
if err != nil {
|
||||
log.Errorf("error occurred while parsing issued_at: %v", err)
|
||||
err = nil
|
||||
} else {
|
||||
issuedAt = &t
|
||||
if len(tk.IssuedAt) != 0 {
|
||||
t, err := time.Parse(time.RFC3339, tk.IssuedAt)
|
||||
if err != nil {
|
||||
log.Errorf("error occurred while parsing issued_at: %v", err)
|
||||
err = nil
|
||||
} else {
|
||||
issuedAt = &t
|
||||
}
|
||||
}
|
||||
|
||||
log.Debug("get token from token server")
|
||||
|
@ -13,27 +13,19 @@
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package errors
|
||||
package error
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Error : if response's status code is not 200 or does not meet requirement,
|
||||
// an Error instance will be returned
|
||||
// Error : if response is returned but the status code is not 200, an Error instance will be returned
|
||||
type Error struct {
|
||||
StatusCode int
|
||||
StatusText string
|
||||
Message string
|
||||
Detail string
|
||||
}
|
||||
|
||||
// Error ...
|
||||
func (e Error) Error() string {
|
||||
return fmt.Sprintf("%d %s %s", e.StatusCode, e.StatusText, e.Message)
|
||||
}
|
||||
|
||||
// ParseError parses err, if err is type Error, convert it to Error
|
||||
func ParseError(err error) (Error, bool) {
|
||||
e, ok := err.(Error)
|
||||
return e, ok
|
||||
// Error returns the details as string
|
||||
func (e *Error) Error() string {
|
||||
return fmt.Sprintf("%d %s", e.StatusCode, e.Detail)
|
||||
}
|
@ -25,7 +25,12 @@ import (
|
||||
|
||||
"github.com/vmware/harbor/utils/log"
|
||||
"github.com/vmware/harbor/utils/registry/auth"
|
||||
"github.com/vmware/harbor/utils/registry/errors"
|
||||
registry_error "github.com/vmware/harbor/utils/registry/error"
|
||||
)
|
||||
|
||||
const (
|
||||
// UserAgent is used to decorate the request so it can be identified by webhook.
|
||||
UserAgent string = "registry-client"
|
||||
)
|
||||
|
||||
// Registry holds information of a registry entity
|
||||
@ -78,6 +83,36 @@ func NewRegistryWithUsername(endpoint, username string) (*Registry, error) {
|
||||
return registry, nil
|
||||
}
|
||||
|
||||
// NewRegistryWithCredential returns a Registry instance which associate to a crendential.
|
||||
// And Credential is essentially a decorator for client to docorate the request before sending it to the registry.
|
||||
func NewRegistryWithCredential(endpoint string, credential auth.Credential) (*Registry, error) {
|
||||
endpoint = strings.TrimSpace(endpoint)
|
||||
endpoint = strings.TrimRight(endpoint, "/")
|
||||
if !strings.HasPrefix(endpoint, "http://") &&
|
||||
!strings.HasPrefix(endpoint, "https://") {
|
||||
endpoint = "http://" + endpoint
|
||||
}
|
||||
|
||||
u, err := url.Parse(endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client, err := newClient(endpoint, "", credential, "", "", "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
registry := &Registry{
|
||||
Endpoint: u,
|
||||
client: client,
|
||||
}
|
||||
|
||||
log.Debugf("initialized a registry client with credential: %s", endpoint)
|
||||
|
||||
return registry, nil
|
||||
}
|
||||
|
||||
// Catalog ...
|
||||
func (r *Registry) Catalog() ([]string, error) {
|
||||
repos := []string{}
|
||||
@ -89,11 +124,7 @@ func (r *Registry) Catalog() ([]string, error) {
|
||||
|
||||
resp, err := r.client.Do(req)
|
||||
if err != nil {
|
||||
ok, e := isUnauthorizedError(err)
|
||||
if ok {
|
||||
return repos, e
|
||||
}
|
||||
return repos, err
|
||||
return repos, parseError(err)
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
@ -117,10 +148,48 @@ func (r *Registry) Catalog() ([]string, error) {
|
||||
return repos, nil
|
||||
}
|
||||
|
||||
return repos, errors.Error{
|
||||
return repos, ®istry_error.Error{
|
||||
StatusCode: resp.StatusCode,
|
||||
StatusText: resp.Status,
|
||||
Message: string(b),
|
||||
Detail: string(b),
|
||||
}
|
||||
}
|
||||
|
||||
// Ping ...
|
||||
func (r *Registry) Ping() error {
|
||||
req, err := http.NewRequest("GET", buildPingURL(r.Endpoint.String()), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := r.client.Do(req)
|
||||
if err != nil {
|
||||
// if urlErr, ok := err.(*url.Error); ok {
|
||||
// if regErr, ok := urlErr.Err.(*registry_error.Error); ok {
|
||||
// return ®istry_error.Error{
|
||||
// StatusCode: regErr.StatusCode,
|
||||
// Detail: regErr.Detail,
|
||||
// }
|
||||
// }
|
||||
// return urlErr.Err
|
||||
// }
|
||||
|
||||
return parseError(err)
|
||||
}
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return nil
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
b, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return ®istry_error.Error{
|
||||
StatusCode: resp.StatusCode,
|
||||
Detail: string(b),
|
||||
}
|
||||
}
|
||||
|
||||
@ -149,8 +218,9 @@ func newClient(endpoint, username string, credential auth.Credential,
|
||||
|
||||
challenges := auth.ParseChallengeFromResponse(resp)
|
||||
authorizer := auth.NewRequestAuthorizer(handlers, challenges)
|
||||
headerModifier := NewHeaderModifier(map[string]string{http.CanonicalHeaderKey("User-Agent"): UserAgent})
|
||||
|
||||
transport := NewTransport(http.DefaultTransport, []RequestModifier{authorizer})
|
||||
transport := NewTransport(http.DefaultTransport, []RequestModifier{authorizer, headerModifier})
|
||||
return &http.Client{
|
||||
Transport: transport,
|
||||
}, nil
|
||||
|
@ -19,6 +19,7 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@ -29,7 +30,7 @@ import (
|
||||
"github.com/docker/distribution/manifest/schema2"
|
||||
"github.com/vmware/harbor/utils/log"
|
||||
"github.com/vmware/harbor/utils/registry/auth"
|
||||
"github.com/vmware/harbor/utils/registry/errors"
|
||||
registry_error "github.com/vmware/harbor/utils/registry/error"
|
||||
)
|
||||
|
||||
// Repository holds information of a repository entity
|
||||
@ -111,15 +112,14 @@ func NewRepositoryWithUsername(name, endpoint, username string) (*Repository, er
|
||||
return repository, nil
|
||||
}
|
||||
|
||||
// try to convert err to errors.Error if it is
|
||||
func isUnauthorizedError(err error) (bool, error) {
|
||||
if strings.Contains(err.Error(), http.StatusText(http.StatusUnauthorized)) {
|
||||
return true, errors.Error{
|
||||
StatusCode: http.StatusUnauthorized,
|
||||
StatusText: http.StatusText(http.StatusUnauthorized),
|
||||
func parseError(err error) error {
|
||||
if urlErr, ok := err.(*url.Error); ok {
|
||||
if regErr, ok := urlErr.Err.(*registry_error.Error); ok {
|
||||
return regErr
|
||||
}
|
||||
return urlErr.Err
|
||||
}
|
||||
return false, err
|
||||
return err
|
||||
}
|
||||
|
||||
// ListTag ...
|
||||
@ -132,11 +132,7 @@ func (r *Repository) ListTag() ([]string, error) {
|
||||
|
||||
resp, err := r.client.Do(req)
|
||||
if err != nil {
|
||||
ok, e := isUnauthorizedError(err)
|
||||
if ok {
|
||||
return tags, e
|
||||
}
|
||||
return tags, err
|
||||
return tags, parseError(err)
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
@ -159,10 +155,9 @@ func (r *Repository) ListTag() ([]string, error) {
|
||||
|
||||
return tags, nil
|
||||
}
|
||||
return tags, errors.Error{
|
||||
return tags, ®istry_error.Error{
|
||||
StatusCode: resp.StatusCode,
|
||||
StatusText: resp.Status,
|
||||
Message: string(b),
|
||||
Detail: string(b),
|
||||
}
|
||||
|
||||
}
|
||||
@ -179,11 +174,7 @@ func (r *Repository) ManifestExist(reference string) (digest string, exist bool,
|
||||
|
||||
resp, err := r.client.Do(req)
|
||||
if err != nil {
|
||||
ok, e := isUnauthorizedError(err)
|
||||
if ok {
|
||||
err = e
|
||||
return
|
||||
}
|
||||
err = parseError(err)
|
||||
return
|
||||
}
|
||||
|
||||
@ -204,10 +195,9 @@ func (r *Repository) ManifestExist(reference string) (digest string, exist bool,
|
||||
return
|
||||
}
|
||||
|
||||
err = errors.Error{
|
||||
err = ®istry_error.Error{
|
||||
StatusCode: resp.StatusCode,
|
||||
StatusText: resp.Status,
|
||||
Message: string(b),
|
||||
Detail: string(b),
|
||||
}
|
||||
return
|
||||
}
|
||||
@ -225,11 +215,7 @@ func (r *Repository) PullManifest(reference string, acceptMediaTypes []string) (
|
||||
|
||||
resp, err := r.client.Do(req)
|
||||
if err != nil {
|
||||
ok, e := isUnauthorizedError(err)
|
||||
if ok {
|
||||
err = e
|
||||
return
|
||||
}
|
||||
err = parseError(err)
|
||||
return
|
||||
}
|
||||
|
||||
@ -246,10 +232,9 @@ func (r *Repository) PullManifest(reference string, acceptMediaTypes []string) (
|
||||
return
|
||||
}
|
||||
|
||||
err = errors.Error{
|
||||
err = ®istry_error.Error{
|
||||
StatusCode: resp.StatusCode,
|
||||
StatusText: resp.Status,
|
||||
Message: string(b),
|
||||
Detail: string(b),
|
||||
}
|
||||
|
||||
return
|
||||
@ -266,11 +251,7 @@ func (r *Repository) PushManifest(reference, mediaType string, payload []byte) (
|
||||
|
||||
resp, err := r.client.Do(req)
|
||||
if err != nil {
|
||||
ok, e := isUnauthorizedError(err)
|
||||
if ok {
|
||||
err = e
|
||||
return
|
||||
}
|
||||
err = parseError(err)
|
||||
return
|
||||
}
|
||||
|
||||
@ -286,10 +267,9 @@ func (r *Repository) PushManifest(reference, mediaType string, payload []byte) (
|
||||
return
|
||||
}
|
||||
|
||||
err = errors.Error{
|
||||
err = ®istry_error.Error{
|
||||
StatusCode: resp.StatusCode,
|
||||
StatusText: resp.Status,
|
||||
Message: string(b),
|
||||
Detail: string(b),
|
||||
}
|
||||
|
||||
return
|
||||
@ -304,11 +284,7 @@ func (r *Repository) DeleteManifest(digest string) error {
|
||||
|
||||
resp, err := r.client.Do(req)
|
||||
if err != nil {
|
||||
ok, e := isUnauthorizedError(err)
|
||||
if ok {
|
||||
return e
|
||||
}
|
||||
return err
|
||||
return parseError(err)
|
||||
}
|
||||
|
||||
if resp.StatusCode == http.StatusAccepted {
|
||||
@ -322,10 +298,9 @@ func (r *Repository) DeleteManifest(digest string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return errors.Error{
|
||||
return ®istry_error.Error{
|
||||
StatusCode: resp.StatusCode,
|
||||
StatusText: resp.Status,
|
||||
Message: string(b),
|
||||
Detail: string(b),
|
||||
}
|
||||
}
|
||||
|
||||
@ -337,9 +312,8 @@ func (r *Repository) DeleteTag(tag string) error {
|
||||
}
|
||||
|
||||
if !exist {
|
||||
return errors.Error{
|
||||
return ®istry_error.Error{
|
||||
StatusCode: http.StatusNotFound,
|
||||
StatusText: http.StatusText(http.StatusNotFound),
|
||||
}
|
||||
}
|
||||
|
||||
@ -355,11 +329,7 @@ func (r *Repository) BlobExist(digest string) (bool, error) {
|
||||
|
||||
resp, err := r.client.Do(req)
|
||||
if err != nil {
|
||||
ok, e := isUnauthorizedError(err)
|
||||
if ok {
|
||||
return false, e
|
||||
}
|
||||
return false, err
|
||||
return false, parseError(err)
|
||||
}
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
@ -377,15 +347,14 @@ func (r *Repository) BlobExist(digest string) (bool, error) {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return false, errors.Error{
|
||||
return false, ®istry_error.Error{
|
||||
StatusCode: resp.StatusCode,
|
||||
StatusText: resp.Status,
|
||||
Message: string(b),
|
||||
Detail: string(b),
|
||||
}
|
||||
}
|
||||
|
||||
// PullBlob ...
|
||||
func (r *Repository) PullBlob(digest string) (size int64, data []byte, err error) {
|
||||
func (r *Repository) PullBlob(digest string) (size int64, data io.ReadCloser, err error) {
|
||||
req, err := http.NewRequest("GET", buildBlobURL(r.Endpoint.String(), r.Name, digest), nil)
|
||||
if err != nil {
|
||||
return
|
||||
@ -393,17 +362,7 @@ func (r *Repository) PullBlob(digest string) (size int64, data []byte, err error
|
||||
|
||||
resp, err := r.client.Do(req)
|
||||
if err != nil {
|
||||
ok, e := isUnauthorizedError(err)
|
||||
if ok {
|
||||
err = e
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
b, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
err = parseError(err)
|
||||
return
|
||||
}
|
||||
|
||||
@ -413,14 +372,19 @@ func (r *Repository) PullBlob(digest string) (size int64, data []byte, err error
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
data = b
|
||||
data = resp.Body
|
||||
return
|
||||
}
|
||||
|
||||
err = errors.Error{
|
||||
defer resp.Body.Close()
|
||||
b, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = ®istry_error.Error{
|
||||
StatusCode: resp.StatusCode,
|
||||
StatusText: resp.Status,
|
||||
Message: string(b),
|
||||
Detail: string(b),
|
||||
}
|
||||
|
||||
return
|
||||
@ -432,11 +396,7 @@ func (r *Repository) initiateBlobUpload(name string) (location, uploadUUID strin
|
||||
|
||||
resp, err := r.client.Do(req)
|
||||
if err != nil {
|
||||
ok, e := isUnauthorizedError(err)
|
||||
if ok {
|
||||
err = e
|
||||
return
|
||||
}
|
||||
err = parseError(err)
|
||||
return
|
||||
}
|
||||
|
||||
@ -453,28 +413,23 @@ func (r *Repository) initiateBlobUpload(name string) (location, uploadUUID strin
|
||||
return
|
||||
}
|
||||
|
||||
err = errors.Error{
|
||||
err = ®istry_error.Error{
|
||||
StatusCode: resp.StatusCode,
|
||||
StatusText: resp.Status,
|
||||
Message: string(b),
|
||||
Detail: string(b),
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (r *Repository) monolithicBlobUpload(location, digest string, size int64, data []byte) error {
|
||||
req, err := http.NewRequest("PUT", buildMonolithicBlobUploadURL(location, digest), bytes.NewReader(data))
|
||||
func (r *Repository) monolithicBlobUpload(location, digest string, size int64, data io.Reader) error {
|
||||
req, err := http.NewRequest("PUT", buildMonolithicBlobUploadURL(location, digest), data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := r.client.Do(req)
|
||||
if err != nil {
|
||||
ok, e := isUnauthorizedError(err)
|
||||
if ok {
|
||||
return e
|
||||
}
|
||||
return err
|
||||
return parseError(err)
|
||||
}
|
||||
|
||||
if resp.StatusCode == http.StatusCreated {
|
||||
@ -488,25 +443,14 @@ func (r *Repository) monolithicBlobUpload(location, digest string, size int64, d
|
||||
return err
|
||||
}
|
||||
|
||||
return errors.Error{
|
||||
return ®istry_error.Error{
|
||||
StatusCode: resp.StatusCode,
|
||||
StatusText: resp.Status,
|
||||
Message: string(b),
|
||||
Detail: string(b),
|
||||
}
|
||||
}
|
||||
|
||||
// PushBlob ...
|
||||
func (r *Repository) PushBlob(digest string, size int64, data []byte) error {
|
||||
exist, err := r.BlobExist(digest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if exist {
|
||||
log.Infof("blob already exists, skip pushing: %s %s", r.Name, digest)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repository) PushBlob(digest string, size int64, data io.Reader) error {
|
||||
location, _, err := r.initiateBlobUpload(r.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -524,11 +468,7 @@ func (r *Repository) DeleteBlob(digest string) error {
|
||||
|
||||
resp, err := r.client.Do(req)
|
||||
if err != nil {
|
||||
ok, e := isUnauthorizedError(err)
|
||||
if ok {
|
||||
return e
|
||||
}
|
||||
return err
|
||||
return parseError(err)
|
||||
}
|
||||
|
||||
if resp.StatusCode == http.StatusAccepted {
|
||||
@ -542,10 +482,9 @@ func (r *Repository) DeleteBlob(digest string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return errors.Error{
|
||||
return ®istry_error.Error{
|
||||
StatusCode: resp.StatusCode,
|
||||
StatusText: resp.Status,
|
||||
Message: string(b),
|
||||
Detail: string(b),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -25,9 +25,8 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
//"github.com/vmware/harbor/utils/log"
|
||||
"github.com/vmware/harbor/utils/registry/auth"
|
||||
"github.com/vmware/harbor/utils/registry/errors"
|
||||
"github.com/vmware/harbor/utils/registry/error"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -164,12 +163,12 @@ func TestListTagWithInvalidCredential(t *testing.T) {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
_, err = client.ListTag()
|
||||
if err != nil {
|
||||
e, ok := errors.ParseError(err)
|
||||
if _, err = client.ListTag(); err != nil {
|
||||
e, ok := err.(*error.Error)
|
||||
if ok && e.StatusCode == http.StatusUnauthorized {
|
||||
return
|
||||
}
|
||||
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
@ -26,6 +26,26 @@ type RequestModifier interface {
|
||||
ModifyRequest(*http.Request) error
|
||||
}
|
||||
|
||||
// HeaderModifier adds headers to request
|
||||
type HeaderModifier struct {
|
||||
headers map[string]string
|
||||
}
|
||||
|
||||
// NewHeaderModifier ...
|
||||
func NewHeaderModifier(headers map[string]string) *HeaderModifier {
|
||||
return &HeaderModifier{
|
||||
headers: headers,
|
||||
}
|
||||
}
|
||||
|
||||
// ModifyRequest adds headers to the request
|
||||
func (h *HeaderModifier) ModifyRequest(req *http.Request) error {
|
||||
for key, value := range h.headers {
|
||||
req.Header.Add(key, value)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Transport holds information about base transport and modifiers
|
||||
type Transport struct {
|
||||
transport http.RoundTripper
|
||||
|
@ -3,17 +3,71 @@
|
||||
<div class="row extend-height">
|
||||
<div class="section">
|
||||
<h1 class="col-md-12 col-md-offset-2 main-title title-color">// 'account_setting' | tr //</h1>
|
||||
|
||||
<div class="col-md-12 col-md-offset-2 main-content">
|
||||
<form name="form" class="form-horizontal" ng-submit="form.$valid && vm.changeProile(user)" >
|
||||
<div class="col-md-12 col-md-offset-2 main-content">
|
||||
<form name="form" class="form-horizontal" ng-submit="form.$valid && vm.changeProile(user)" >
|
||||
<div class="form-group">
|
||||
<label for="username" class="col-sm-3 control-label">// 'username' | tr //:</label>
|
||||
<div class="col-sm-7">
|
||||
<input type="text" class="form-control" id="username" ng-model="user.username" ng-value="vm.user.username" name="uUsername" ng-model-options="{ updateOn: 'blur' }" ng-disabled="true" required maxlength="20" invalid-chars>
|
||||
<div ng-messages="form.$submitted && form.uUsername.$error">
|
||||
<span ng-message="required">// 'username_is_required' | tr //</span>
|
||||
<span ng-message="maxlength">// 'username_is_too_long' | tr //</span>
|
||||
<span ng-message="invalidChars">// 'username_contains_illegal_chars' | tr //</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-2">
|
||||
<span class="asterisk">*</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="email" class="col-sm-3 control-label">// 'email' | tr //:</label>
|
||||
<div class="col-sm-7">
|
||||
<input type="email" class="form-control" id="email" ng-model="user.email" ng-value="vm.user.email" ng-model-options="{ updateOn: 'blur' }" ng-disabled="true" name="uEmail" required>
|
||||
<div ng-messages="form.$submitted && form.uEmail.$error">
|
||||
<span ng-message="required">// 'email_is_required' | tr //</span>
|
||||
<span ng-message="email">// 'email_content_illegal' | tr //</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-2">
|
||||
<span class="asterisk">*</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="fullName" class="col-sm-3 control-label">// 'full_name' | tr //:</label>
|
||||
<div class="col-sm-7">
|
||||
<input type="text" class="form-control" id="fullName" ng-model="user.fullName" name="uFullName" ng-model-options="{ updateOn: 'blur' }" ng-value="vm.user.realname" ng-disabled="true" required maxlength="20" invalid-chars>
|
||||
<div ng-messages="form.$submitted && form.uFullName.$error">
|
||||
<span ng-message="required">// 'full_name_is_required' | tr //</span>
|
||||
<span ng-message="invalidChars">// 'full_name_contains_illegal_chars' | tr //</span>
|
||||
<span ng-message="maxlength">// 'full_name_is_too_long' | tr //</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-2">
|
||||
<span class="asterisk">*</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="comments" class="col-sm-3 control-label">// 'comments' | tr //:</label>
|
||||
<div class="col-sm-7">
|
||||
<input type="text" class="form-control" id="comments" ng-model="user.comment" name="uComments" ng-model-options="{ updateOn: 'blur' }" ng-value="vm.user.comment" ng-disabled="true" ng-model-options="{ updateOn: 'blur' }" maxlength="20">
|
||||
<div ng-messages="form.$submitted && form.uComments.$error">
|
||||
<span ng-show="maxlength">// 'comment_is_too_long' | tr //</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<form name="form" class="form-horizontal css-form" ng-submit="form.$valid && vm.changePassword(user)" novalidate>
|
||||
<div class="form-group">
|
||||
<label for="toggleChangePassword" class="col-sm-3 control-label"><a id="toggleChangePassword" href="#" ng-click="vm.toggleChangePassword()">// 'change_password' | tr //</a></label>
|
||||
</div>
|
||||
<div ng-show="vm.isOpen">
|
||||
<hr/>
|
||||
<div class="form-group">
|
||||
<label for="username" class="col-sm-3 control-label">// 'username' | tr //:</label>
|
||||
<label for="oldPassword" class="col-sm-3 control-label">// 'old_password' | tr //:</label>
|
||||
<div class="col-sm-7">
|
||||
<input type="text" class="form-control" id="username" ng-model="user.username" ng-value="vm.user.username" name="uUsername" ng-model-options="{ updateOn: 'blur' }" ng-disabled="true" required maxlength="20" invalid-chars>
|
||||
<div ng-messages="form.$submitted && form.uUsername.$error">
|
||||
<span ng-message="required">// 'username_is_required' | tr //</span>
|
||||
<span ng-message="maxlength">// 'username_is_too_long' | tr //</span>
|
||||
<span ng-message="invalidChars">// 'username_contains_illegal_chars' | tr //</span>
|
||||
<input type="password" class="form-control" id="oldPassword" ng-model="user.oldPassword" ng-change="vm.reset()" name="uOldPassword" ng-model-options="{ updateOn: 'blur' }" required>
|
||||
<div class="error-message" ng-messages="form.uOldPassword.$error" ng-if="form.uOldPassword.$touched">
|
||||
<span ng-message="required">// 'old_password_is_required' | tr //</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-2">
|
||||
@ -21,99 +75,43 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="email" class="col-sm-3 control-label">// 'email' | tr //:</label>
|
||||
<label for="password" class="col-sm-3 control-label">// 'new_password' | tr //:</label>
|
||||
<div class="col-sm-7">
|
||||
<input type="email" class="form-control" id="email" ng-model="user.email" ng-value="vm.user.email" ng-model-options="{ updateOn: 'blur' }" ng-disabled="true" name="uEmail" required>
|
||||
<div ng-messages="form.$submitted && form.uEmail.$error">
|
||||
<span ng-message="required">// 'email_is_required' | tr //</span>
|
||||
<span ng-message="email">// 'email_content_illegal' | tr //</span>
|
||||
<input type="password" class="form-control" id="password" ng-model="user.password" ng-change="vm.reset()" name="uPassword" ng-model-options="{ updateOn: 'blur' }" required password>
|
||||
<div class="error-message" ng-messages="form.uPassword.$error" ng-if="form.uPassword.$touched">
|
||||
<span ng-message="required">// 'password_is_required' | tr //</span>
|
||||
<span ng-message="password">// 'password_is_invalid' | tr //</span>
|
||||
</div>
|
||||
<p class="help-block small-size-fonts">// 'password_desc' | tr //</p>
|
||||
</div>
|
||||
<div class="col-sm-2">
|
||||
<span class="asterisk">*</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="fullName" class="col-sm-3 control-label">// 'full_name' | tr //:</label>
|
||||
<label for="confirmPassword" class="col-sm-3 control-label">// 'confirm_password' | tr //:</label>
|
||||
<div class="col-sm-7">
|
||||
<input type="text" class="form-control" id="fullName" ng-model="user.fullName" name="uFullName" ng-model-options="{ updateOn: 'blur' }" ng-value="vm.user.realname" ng-disabled="true" required maxlength="20" invalid-chars>
|
||||
<div ng-messages="form.$submitted && form.uFullName.$error">
|
||||
<span ng-message="required">// 'full_name_is_required' | tr //</span>
|
||||
<span ng-message="invalidChars">// 'full_name_contains_illegal_chars' | tr //</span>
|
||||
<span ng-message="maxlength">// 'full_name_is_too_long' | tr //</span>
|
||||
<input type="password" class="form-control" id="confirmPassword" ng-model="user.confirmPassword" ng-change="vm.reset()" name="uConfirmPassword" ng-model-options="{ updateOn: 'blur' }" compare-to="user.password">
|
||||
<div class="error-message" ng-messages="form.uConfirmPassword.$error" ng-if="form.uConfirmPassword.$touched">
|
||||
<span ng-message="compareTo">// 'password_does_not_match' | tr //</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-2">
|
||||
<span class="asterisk">*</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="comments" class="col-sm-3 control-label">// 'comments' | tr //:</label>
|
||||
<div class="col-sm-7">
|
||||
<input type="text" class="form-control" id="comments" ng-model="user.comment" name="uComments" ng-model-options="{ updateOn: 'blur' }" ng-value="vm.user.comment" ng-disabled="true" ng-model-options="{ updateOn: 'blur' }" maxlength="20">
|
||||
<div ng-messages="form.$submitted && form.uComments.$error">
|
||||
<span ng-show="maxlength">// 'comment_is_too_long' | tr //</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-md-offset-7 col-md-10">
|
||||
<input type="submit" class="btn btn-primary" ng-disabled="form.$invalid" value="// 'save' | tr //">
|
||||
<input type="submit" class="btn btn-default" ng-click="vm.cancel(form)" value="// 'cancel' | tr //">
|
||||
</div>
|
||||
</form>
|
||||
<form name="form" class="form-horizontal css-form" ng-submit="form.$valid && vm.changePassword(user)" novalidate>
|
||||
<div class="form-group">
|
||||
<label for="toggleChangePassword" class="col-sm-3 control-label"><a id="toggleChangePassword" href="#" ng-click="vm.toggleChangePassword()">// 'change_password' | tr //</a></label>
|
||||
</div>
|
||||
<div ng-show="vm.isOpen">
|
||||
<hr/>
|
||||
<div class="form-group">
|
||||
<label for="oldPassword" class="col-sm-3 control-label">// 'old_password' | tr //:</label>
|
||||
<div class="col-sm-7">
|
||||
<input type="password" class="form-control" id="oldPassword" ng-model="user.oldPassword" ng-change="vm.reset()" name="uOldPassword" ng-model-options="{ updateOn: 'blur' }" required>
|
||||
<div class="error-message" ng-messages="form.uOldPassword.$error" ng-if="form.uOldPassword.$touched">
|
||||
<span ng-message="required">// 'old_password_is_required' | tr //</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-2">
|
||||
<span class="asterisk">*</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password" class="col-sm-3 control-label">// 'new_password' | tr //:</label>
|
||||
<div class="col-sm-7">
|
||||
<input type="password" class="form-control" id="password" ng-model="user.password" ng-change="vm.reset()" name="uPassword" ng-model-options="{ updateOn: 'blur' }" required password>
|
||||
<div class="error-message" ng-messages="form.uPassword.$error" ng-if="form.uPassword.$touched">
|
||||
<span ng-message="required">// 'password_is_required' | tr //</span>
|
||||
<span ng-message="password">// 'password_is_invalid' | tr //</span>
|
||||
</div>
|
||||
<p class="help-block small-size-fonts">// 'password_desc' | tr //</p>
|
||||
</div>
|
||||
<div class="col-sm-2">
|
||||
<span class="asterisk">*</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="confirmPassword" class="col-sm-3 control-label">// 'confirm_password' | tr //:</label>
|
||||
<div class="col-sm-7">
|
||||
<input type="password" class="form-control" id="confirmPassword" ng-model="user.confirmPassword" ng-change="vm.reset()" name="uConfirmPassword" ng-model-options="{ updateOn: 'blur' }" compare-to="user.password">
|
||||
<div class="error-message" ng-messages="form.uConfirmPassword.$error" ng-if="form.uConfirmPassword.$touched">
|
||||
<span ng-message="compareTo">// 'password_does_not_match' | tr //</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-2">
|
||||
<span class="asterisk">*</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-md-offset-7 col-md-10">
|
||||
<input type="submit" class="btn btn-primary" ng-disabled="form.$invalid" value="// 'save' | tr //">
|
||||
<input type="submit" class="btn btn-default" ng-click="vm.cancel(form)" value="// 'cancel' | tr //">
|
||||
</div>
|
||||
</div>
|
||||
<div class="error-message">
|
||||
<span ng-show="vm.hasError">// vm.errorMessage | tr //</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="error-message">
|
||||
<span ng-show="vm.hasError">// vm.errorMessage | tr //</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -2,17 +2,16 @@
|
||||
<div class="container container-custom">
|
||||
<div class="row extend-height">
|
||||
<div class="col-xs-12 col-md-12">
|
||||
<div class="section">
|
||||
<h4 class="page-header">
|
||||
<span ng-show="!vm.toggle">// 'users' | tr //</span>
|
||||
<a ng-show="vm.toggle" href="#/all_user" ng-click="vm.toggleAdminOption()">// 'users' | tr //</a>
|
||||
<span class="gutter">|</span>
|
||||
<span ng-show="vm.toggle">// 'system_management' | tr //</span>
|
||||
<a ng-show="!vm.toggle" href="#/system_management" class="title-color" ng-click="vm.toggleAdminOption()">// 'system_management' | tr //</a>
|
||||
<div class="section">
|
||||
<h4 class="page-header">
|
||||
<span ng-if="!vm.toggle">// 'users' | tr //</span>
|
||||
<a ng-if="vm.toggle" href="#" ng-click="vm.toggleAdminOption()">// 'users' | tr //</a>
|
||||
<span class="gutter">|</span>
|
||||
<span ng-if="vm.toggle">// 'system_management' | tr //</span>
|
||||
<a ng-if="!vm.toggle" href="#/destinations" class="title-color" ng-click="vm.toggleAdminOption()">// 'system_management' | tr //</a>
|
||||
</h4>
|
||||
<div class="pane project-pane">
|
||||
<ng-view></ng-view>
|
||||
</div>
|
||||
<list-user ng-if="!vm.toggle"></list-user>
|
||||
<system-management ng-if="vm.toggle"></system-management>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -3,7 +3,7 @@
|
||||
<li><a tag="dashboard" href="/ng/dashboard">// 'dashboard' | tr //</a></li>
|
||||
<li><a tag="project" href="/ng/project">// 'projects' | tr //</a></li>
|
||||
{{ if eq .IsAdmin 1 }}
|
||||
<li><a tag="admin_option" href="/ng/admin_option#/all_user">// 'admin_options' | tr //</a></li>
|
||||
<li><a tag="admin_option" href="/ng/admin_option">// 'admin_options' | tr //</a></li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
{{ end }}
|
@ -12,14 +12,17 @@
|
||||
<div class="switch-pane">
|
||||
<switch-pane-projects is-open="vm.isOpen" selected-project="vm.selectedProject"></switch-pane-projects>
|
||||
<span>
|
||||
<navigation-details selected-project="vm.selectedProject" ng-show="vm.isProjectMember"></navigation-details>
|
||||
<navigation-details target="vm.target" ng-show="vm.isProjectMember"></navigation-details>
|
||||
</span>
|
||||
</div>
|
||||
<retrieve-projects is-open="vm.isOpen" selected-project="vm.selectedProject" is-project-member="vm.isProjectMember" publicity="vm.publicity"></retrieve-projects>
|
||||
<!-- Tab panes -->
|
||||
<div class="tab-content" ng-click="vm.closeRetrievePane()">
|
||||
<input type="hidden" id="HarborRegUrl" value="{{.HarborRegUrl}}">
|
||||
<ng-view></ng-view>
|
||||
<input type="hidden" id="HarborRegUrl" value="{{.HarborRegUrl}}">
|
||||
<list-repository ng-if="vm.target === 'repositories'"></list-repository>
|
||||
<list-replication ng-if="vm.target === 'replication'"></list-replication>
|
||||
<list-project-member ng-if="vm.target === 'users'"></list-project-member>
|
||||
<list-log ng-if="vm.target === 'logs'"></list-log>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -31,6 +31,9 @@
|
||||
<link rel="stylesheet" href="/static/ng/resources/css/repository.css">
|
||||
<link rel="stylesheet" href="/static/ng/resources/css/sign-up.css">
|
||||
<link rel="stylesheet" href="/static/ng/resources/css/search.css">
|
||||
<link rel="stylesheet" href="/static/ng/resources/css/replication.css">
|
||||
<link rel="stylesheet" href="/static/ng/resources/css/admin-options.css">
|
||||
<link rel="stylesheet" href="/static/ng/resources/css/destination.css">
|
||||
|
||||
<script src="/static/ng/vendors/angularjs/angular.js"></script>
|
||||
<script src="/static/ng/vendors/angularjs/angular-route.js"></script>
|
||||
@ -48,6 +51,7 @@
|
||||
<script src="/static/ng/resources/js/layout/navigation/navigation.module.js"></script>
|
||||
<script src="/static/ng/resources/js/layout/navigation/navigation-header.directive.js"></script>
|
||||
<script src="/static/ng/resources/js/layout/navigation/navigation-details.directive.js"></script>
|
||||
<script src="/static/ng/resources/js/layout/navigation/navigation-admin-options.directive.js"></script>
|
||||
|
||||
<script src="/static/ng/resources/js/layout/sign-up/sign-up.module.js"></script>
|
||||
<script src="/static/ng/resources/js/layout/sign-up/sign-up.controller.js"></script>
|
||||
@ -74,21 +78,6 @@
|
||||
<script src="/static/ng/resources/js/layout/details/details.config.js"></script>
|
||||
<script src="/static/ng/resources/js/layout/details/details.controller.js"></script>
|
||||
|
||||
<script src="/static/ng/resources/js/layout/repository/repository.module.js"></script>
|
||||
<script src="/static/ng/resources/js/layout/repository/repository.controller.js"></script>
|
||||
|
||||
<script src="/static/ng/resources/js/layout/project-member/project-member.module.js"></script>
|
||||
<script src="/static/ng/resources/js/layout/project-member/project-member.controller.js"></script>
|
||||
|
||||
<script src="/static/ng/resources/js/layout/user/user.module.js"></script>
|
||||
<script src="/static/ng/resources/js/layout/user/user.controller.js"></script>
|
||||
|
||||
<script src="/static/ng/resources/js/layout/system-management/system-management.module.js"></script>
|
||||
<script src="/static/ng/resources/js/layout/system-management/system-management.controller.js"></script>
|
||||
|
||||
<script src="/static/ng/resources/js/layout/log/log.module.js"></script>
|
||||
<script src="/static/ng/resources/js/layout/log/log.controller.js"></script>
|
||||
|
||||
<script src="/static/ng/resources/js/layout/admin-option/admin-option.module.js"></script>
|
||||
<script src="/static/ng/resources/js/layout/admin-option/admin-option.controller.js"></script>
|
||||
<script src="/static/ng/resources/js/layout/admin-option/admin-option.config.js"></script>
|
||||
@ -146,6 +135,16 @@
|
||||
<script src="/static/ng/resources/js/services/log/services.list-log.js"></script>
|
||||
<script src="/static/ng/resources/js/services/log/services.list-integrated-log.js"></script>
|
||||
|
||||
<script src="/static/ng/resources/js/services/replication-policy/services.replication-policy.module.js"></script>
|
||||
<script src="/static/ng/resources/js/services/replication-policy/services.list-replication-policy.js"></script>
|
||||
|
||||
<script src="/static/ng/resources/js/services/replication-job/services.replication-job.module.js"></script>
|
||||
<script src="/static/ng/resources/js/services/replication-job/services.list-replication-job.js"></script>
|
||||
|
||||
<script src="/static/ng/resources/js/services/destination/services.destination.module.js"></script>
|
||||
<script src="/static/ng/resources/js/services/destination/services.create-destination.js"></script>
|
||||
<script src="/static/ng/resources/js/services/destination/services.list-destination.js"></script>
|
||||
|
||||
<script src="/static/ng/resources/js/session/session.module.js"></script>
|
||||
<script src="/static/ng/resources/js/session/session.current-user.js"></script>
|
||||
|
||||
@ -191,6 +190,7 @@
|
||||
<script src="/static/ng/resources/js/components/project-member/edit-project-member.directive.js"></script>
|
||||
|
||||
<script src="/static/ng/resources/js/components/user/user.module.js"></script>
|
||||
<script src="/static/ng/resources/js/components/user/list-user.directive.js"></script>
|
||||
<script src="/static/ng/resources/js/components/user/toggle-admin.directive.js"></script>
|
||||
|
||||
<script src="/static/ng/resources/js/components/log/log.module.js"></script>
|
||||
@ -198,5 +198,19 @@
|
||||
<script src="/static/ng/resources/js/components/log/list-log.directive.js"></script>
|
||||
<script src="/static/ng/resources/js/components/log/advanced-search.directive.js"></script>
|
||||
|
||||
|
||||
<script src="/static/ng/resources/js/components/replication/replication.module.js"></script>
|
||||
<script src="/static/ng/resources/js/components/replication/list-replication.directive.js"></script>
|
||||
<script src="/static/ng/resources/js/components/replication/create-policy.directive.js"></script>
|
||||
|
||||
|
||||
<script src="/static/ng/resources/js/components/system-management/system-management.module.js"></script>
|
||||
<script src="/static/ng/resources/js/components/system-management/system-management.directive.js"></script>
|
||||
<script src="/static/ng/resources/js/components/system-management/destination.directive.js"></script>
|
||||
<script src="/static/ng/resources/js/components/system-management/create-destination.directive.js"></script>
|
||||
<script src="/static/ng/resources/js/components/system-management/replication.directive.js"></script>
|
||||
<script src="/static/ng/resources/js/components/system-management/configuration.directive.js"></script>
|
||||
|
||||
|
||||
<script src="/static/ng/resources/js/components/summary/summary.module.js"></script>
|
||||
<script src="/static/ng/resources/js/components/summary/summary.directive.js"></script>
|
||||
|
@ -9,7 +9,7 @@
|
||||
<form name="form" class="form-horizontal css-form" ng-submit="form.$valid" novalidate>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-offset-1 col-sm-10">
|
||||
<input id="username" type="text" class="form-control" placeholder="// 'username_email' | tr //" name="uPrincipal" ng-change="vm.reset()" ng-model="user.principal" required>
|
||||
<input id="username" type="text" class="form-control" placeholder="// 'username_email' | tr //" name="uPrincipal" ng-change="vm.reset()" ng-model="user.principal" ng-model-options="{ debounce: { default: 500, blur: 0 } }" required>
|
||||
<div class="error-message">
|
||||
<div ng-messages="form.uPrincipal.$error" ng-if="form.uPrincipal.$touched || form.$submitted">
|
||||
<span ng-message="required">// 'username_is_required' | tr //</span>
|
||||
@ -19,7 +19,7 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-offset-1 col-sm-10">
|
||||
<input type="password" class="form-control" placeholder="// 'password' | tr //" name="uPassword" ng-change="vm.reset()" ng-model="user.password" required>
|
||||
<input type="password" class="form-control" placeholder="// 'password' | tr //" name="uPassword" ng-change="vm.reset()" ng-model-options="{ debounce: { default: 500, blur: 0 } }" ng-model="user.password" required>
|
||||
<div class="error-message">
|
||||
<div ng-messages="form.uPassword.$error" ng-if="form.uPassword.$touched || form.$submitted">
|
||||
<span ng-message="required">// 'password_is_required' | tr //</span>
|
||||
|
Loading…
Reference in New Issue
Block a user