Merge remote-tracking branch 'upstream/master'

This commit is contained in:
xiahaoshawn 2016-04-19 18:44:52 +08:00
commit 24b6d87012
41 changed files with 1256 additions and 194 deletions

1
.gitignore vendored
View File

@ -4,3 +4,4 @@ Deploy/config/ui/env
Deploy/config/ui/app.conf Deploy/config/ui/app.conf
Deploy/config/db/env Deploy/config/db/env
Deploy/harbor.cfg Deploy/harbor.cfg
ui/ui

View File

@ -0,0 +1,3 @@
FROM library/nginx:1.9
ADD ./config/nginx /etc/nginx

View File

@ -0,0 +1,33 @@
version: 0.1
log:
level: debug
fields:
service: registry
storage:
cache:
layerinfo: inmemory
filesystem:
rootdirectory: /storage
maintenance:
uploadpurging:
enabled: false
http:
addr: :5000
secret: placeholder
debug:
addr: localhost:5001
auth:
token:
issuer: registry-token-issuer
realm: http://harbor.caicloud.io/service/token
rootcertbundle: /etc/registry/root.crt
service: token-service
notifications:
endpoints:
- name: harbor
disabled: false
url: http://harbor.caicloud.io/service/notifications
timeout: 500
threshold: 5
backoff: 1000

View File

@ -0,0 +1,6 @@
FROM library/registry:2.3.0
ADD ./config/registry/ /etc/registry/
ADD ./kubernetes/dockerfiles/registry-config.yml /etc/registry/config.yml
CMD ["/etc/registry/config.yml"]

View File

@ -0,0 +1,4 @@
FROM deploy_ui
ADD ./config/ui/app.conf /etc/ui/app.conf
ADD ./config/ui/private_key.pem /etc/ui/private_key.pem

View File

@ -0,0 +1,30 @@
apiVersion: v1
kind: ReplicationController
metadata:
name: mysql
labels:
name: mysql
spec:
replicas: 1
selector:
name: mysql
template:
metadata:
labels:
name: mysql
spec:
containers:
- name: mysql
image: caicloud/harbor_deploy_mysql:latest
imagePullPolicy: Always
ports:
- containerPort: 3306
env:
- name: MYSQL_ROOT_PASSWORD
value: root123
volumeMounts:
- name: mysql-storage
mountPath: /var/lib/mysql
volumes:
- name: mysql-storage
emptyDir: {}

View File

@ -0,0 +1,11 @@
apiVersion: v1
kind: Service
metadata:
name: mysql
labels:
name: mysql
spec:
ports:
- port: 3306
selector:
name: mysql

View File

@ -0,0 +1,22 @@
apiVersion: v1
kind: ReplicationController
metadata:
name: proxy
labels:
name: proxy
spec:
replicas: 1
selector:
name: proxy
template:
metadata:
labels:
name: proxy
spec:
containers:
- name: proxy
image: caicloud/harbor_proxy:latest
imagePullPolicy: Always
ports:
- containerPort: 80
- containerPort: 443

View File

@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: proxy
labels:
name: proxy
spec:
type: LoadBalancer
ports:
- name: http
port: 80
- name: https
port: 443
selector:
name: proxy

View File

@ -0,0 +1,28 @@
apiVersion: v1
kind: ReplicationController
metadata:
name: registry
labels:
name: registry
spec:
replicas: 1
selector:
name: registry
template:
metadata:
labels:
name: registry
spec:
containers:
- name: registry
image: caicloud/harbor_registry:2.3.0
imagePullPolicy: Always
ports:
- containerPort: 5000
- containerPort: 5001
volumeMounts:
- name: storage
mountPath: /storage
volumes:
- name: storage
emptyDir: {}

View File

@ -0,0 +1,14 @@
apiVersion: v1
kind: Service
metadata:
name: registry
labels:
name: registry
spec:
ports:
- name: internal
port: 5000
- name: external
port: 5001
selector:
name: registry

View File

@ -0,0 +1,49 @@
apiVersion: v1
kind: ReplicationController
metadata:
name: ui
labels:
name: ui
spec:
replicas: 1
selector:
name: ui
template:
metadata:
labels:
name: ui
spec:
containers:
- name: ui
image: caicloud/harbor_deploy_ui:latest
imagePullPolicy: Always
env:
- name: MYSQL_HOST
value: mysql
- name: MYSQL_PORT
value: "3306"
- name: MYSQL_USR
value: root
- name: MYSQL_PWD
value: root123
- name: REGISTRY_URL
value: http://registry:5000
- name: CONFIG_PATH
value: /etc/ui/app.conf
- name: HARBOR_REG_URL
value: localhost
- name: HARBOR_ADMIN_PASSWORD
value: Harbor12345
- name: HARBOR_URL
value: http://localhost
- name: AUTH_MODE
value: db_auth
- name: LDAP_URL
value: ldaps://ldap.mydomain.com
- name: LDAP_BASE_DN
value: uid=%s,ou=people,dc=mydomain,dc=com
- name: LOG_LEVEL
value: debug
ports:
- containerPort: 80

View File

@ -0,0 +1,11 @@
apiVersion: v1
kind: Service
metadata:
name: ui
labels:
name: ui
spec:
ports:
- port: 80
selector:
name: ui

View File

@ -1,31 +1,40 @@
#!/usr/bin/python #!/usr/bin/python
# -*- coding: utf-8 -*-
import ConfigParser from __future__ import print_function, unicode_literals # We require Python 2.6 or later
import StringIO
import os
from string import Template from string import Template
import os
import sys
from io import open
if sys.version_info[:3][0] == 2:
import ConfigParser as ConfigParser
import StringIO as StringIO
if sys.version_info[:3][0] == 3:
import configparser as ConfigParser
import io as StringIO
#Read configurations #Read configurations
conf = StringIO.StringIO() conf = StringIO.StringIO()
conf.write("[configuration]\n") conf.write("[configuration]\n")
conf.write(open("harbor.cfg").read()) conf.write(open("harbor.cfg").read())
conf.seek(0, os.SEEK_SET) conf.seek(0, os.SEEK_SET)
cp = ConfigParser.RawConfigParser() rcp = ConfigParser.RawConfigParser()
cp.readfp(conf) rcp.readfp(conf)
hostname = cp.get("configuration", "hostname") hostname = rcp.get("configuration", "hostname")
ui_url = cp.get("configuration", "ui_url_protocol") + "://" + hostname ui_url = rcp.get("configuration", "ui_url_protocol") + "://" + hostname
email_server = cp.get("configuration", "email_server") email_server = rcp.get("configuration", "email_server")
email_server_port = cp.get("configuration", "email_server_port") email_server_port = rcp.get("configuration", "email_server_port")
email_username = cp.get("configuration", "email_username") email_username = rcp.get("configuration", "email_username")
email_password = cp.get("configuration", "email_password") email_password = rcp.get("configuration", "email_password")
email_from = cp.get("configuration", "email_from") email_from = rcp.get("configuration", "email_from")
harbor_admin_password = cp.get("configuration", "harbor_admin_password") harbor_admin_password = rcp.get("configuration", "harbor_admin_password")
auth_mode = cp.get("configuration", "auth_mode") auth_mode = rcp.get("configuration", "auth_mode")
ldap_url = cp.get("configuration", "ldap_url") ldap_url = rcp.get("configuration", "ldap_url")
ldap_basedn = cp.get("configuration", "ldap_basedn") ldap_basedn = rcp.get("configuration", "ldap_basedn")
db_password = cp.get("configuration", "db_password") db_password = rcp.get("configuration", "db_password")
self_registration = cp.get("configuration", "self_registration") self_registration = rcp.get("configuration", "self_registration")
######## ########
base_dir = os.path.dirname(__file__) base_dir = os.path.dirname(__file__)
@ -45,7 +54,7 @@ def render(src, dest, **kw):
t = Template(open(src, 'r').read()) t = Template(open(src, 'r').read())
with open(dest, 'w') as f: with open(dest, 'w') as f:
f.write(t.substitute(**kw)) f.write(t.substitute(**kw))
print "Generated configuration file: %s" % dest print("Generated configuration file: %s" % dest)
ui_conf_env = os.path.join(config_dir, "ui", "env") ui_conf_env = os.path.join(config_dir, "ui", "env")
ui_conf = os.path.join(config_dir, "ui", "app.conf") ui_conf = os.path.join(config_dir, "ui", "app.conf")
@ -55,7 +64,7 @@ db_conf_env = os.path.join(config_dir, "db", "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 ]
for f in conf_files: for f in conf_files:
if os.path.exists(f): if os.path.exists(f):
print "Clearing the configuration file: %s" % f print("Clearing the configuration file: %s" % f)
os.remove(f) os.remove(f)
render(os.path.join(templates_dir, "ui", "env"), render(os.path.join(templates_dir, "ui", "env"),
@ -86,4 +95,4 @@ render(os.path.join(templates_dir, "db", "env"),
db_conf_env, db_conf_env,
db_password=db_password) db_password=db_password)
print "The configuration files are ready, please use docker-compose to start the service." print("The configuration files are ready, please use docker-compose to start the service.")

View File

@ -11,6 +11,8 @@ storage:
maintenance: maintenance:
uploadpurging: uploadpurging:
enabled: false enabled: false
delete:
enabled: true
http: http:
addr: :5000 addr: :5000
secret: placeholder secret: placeholder

View File

@ -57,6 +57,9 @@ To simplify the installation process, a pre-built installation package of Harbor
For information on how to use Harbor, please see [User Guide](docs/user_guide.md) . For information on how to use Harbor, please see [User Guide](docs/user_guide.md) .
### Deploy harbor on Kubernetes
Detailed instruction about deploying harbor on Kubernetes is described [here](https://github.com/vmware/harbor/blob/master/kubernetes_deployment.md).
### Contribution ### Contribution
We welcome contributions from the community. If you wish to contribute code and you have not signed our contributor license agreement (CLA), our bot will update the issue when you open a pull request. For any questions about the CLA process, please refer to our [FAQ](https://cla.vmware.com/faq). We welcome contributions from the community. If you wish to contribute code and you have not signed our contributor license agreement (CLA), our bot will update the issue when you open a pull request. For any questions about the CLA process, please refer to our [FAQ](https://cla.vmware.com/faq).

View File

@ -57,7 +57,10 @@ func (b *BaseAPI) ValidateUser() int {
username, password, ok := b.Ctx.Request.BasicAuth() username, password, ok := b.Ctx.Request.BasicAuth()
if ok { if ok {
log.Infof("Requst with Basic Authentication header, username: %s", username) log.Infof("Requst with Basic Authentication header, username: %s", username)
user, err := auth.Login(models.AuthModel{username, password}) user, err := auth.Login(models.AuthModel{
Principal: username,
Password: password,
})
if err != nil { if err != nil {
log.Errorf("Error while trying to login, username: %s, error: %v", username, err) log.Errorf("Error while trying to login, username: %s, error: %v", username, err)
user = nil user = nil

View File

@ -18,6 +18,7 @@ package api
import ( import (
"encoding/json" "encoding/json"
"net/http" "net/http"
"os"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@ -26,6 +27,9 @@ import (
"github.com/vmware/harbor/models" "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/vmware/harbor/utils/log"
"github.com/vmware/harbor/utils/registry"
"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 // RepositoryAPI handles request to /api/repositories /api/repositories/tags /api/repositories/manifests, the parm has to be put
@ -36,6 +40,7 @@ type RepositoryAPI struct {
BaseAPI BaseAPI
userID int userID int
username string username string
registry *registry.Registry
} }
// Prepare will set a non existent user ID in case the request tries to view repositories under a project he doesn't has permission. // Prepare will set a non existent user ID in case the request tries to view repositories under a project he doesn't has permission.
@ -53,6 +58,43 @@ func (ra *RepositoryAPI) Prepare() {
} else { } else {
ra.username = username ra.username = username
} }
var client *http.Client
//no session, initialize a standard auth handler
if ra.userID == dao.NonExistUserID && len(ra.username) == 0 {
username, password, _ := ra.Ctx.Request.BasicAuth()
credential := auth.NewBasicAuthCredential(username, password)
client = registry.NewClientStandardAuthHandlerEmbeded(credential)
log.Debug("initializing standard auth handler")
} else {
// session works, initialize a username auth handler
username := ra.username
if len(username) == 0 {
user, err := dao.GetUser(models.User{
UserID: ra.userID,
})
if err != nil {
log.Errorf("error occurred whiling geting user for initializing a username auth handler: %v", err)
return
}
username = user.Username
}
client = registry.NewClientUsernameAuthHandlerEmbeded(username)
log.Debug("initializing username auth handler: %s", username)
}
endpoint := os.Getenv("REGISTRY_URL")
r, err := registry.New(endpoint, client)
if err != nil {
log.Fatalf("error occurred while initializing auth handler for repository API: %v", err)
}
ra.registry = r
} }
// Get ... // Get ...
@ -77,11 +119,13 @@ func (ra *RepositoryAPI) Get() {
ra.RenderError(http.StatusForbidden, "") ra.RenderError(http.StatusForbidden, "")
return return
} }
repoList, err := svc_utils.GetRepoFromCache() repoList, err := svc_utils.GetRepoFromCache()
if err != nil { if err != nil {
log.Errorf("Failed to get repo from cache, error: %v", err) log.Errorf("Failed to get repo from cache, error: %v", err)
ra.RenderError(http.StatusInternalServerError, "internal sever error") ra.RenderError(http.StatusInternalServerError, "internal sever error")
} }
projectName := p.Name projectName := p.Name
q := ra.GetString("q") q := ra.GetString("q")
var resp []string var resp []string
@ -105,6 +149,56 @@ func (ra *RepositoryAPI) Get() {
ra.ServeJSON() ra.ServeJSON()
} }
// Delete ...
func (ra *RepositoryAPI) Delete() {
repoName := ra.GetString("repo_name")
if len(repoName) == 0 {
ra.CustomAbort(http.StatusBadRequest, "repo_name is nil")
}
tags := []string{}
tag := ra.GetString("tag")
if len(tag) == 0 {
tagList, err := ra.registry.ListTag(repoName)
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")
}
}
tags = append(tags, tagList...)
} else {
tags = append(tags, tag)
}
for _, t := range tags {
if err := ra.registry.DeleteTag(repoName, 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")
}
}
log.Infof("delete tag: %s %s", repoName, t)
}
go func() {
log.Debug("refreshing catalog cache")
if err := svc_utils.RefreshCatalogCache(); err != nil {
log.Errorf("error occurred while refresh catalog cache: %v", err)
}
}()
}
type tag struct { type tag struct {
Name string `json:"name"` Name string `json:"name"`
Tags []string `json:"tags"` Tags []string `json:"tags"`
@ -128,15 +222,19 @@ func (ra *RepositoryAPI) GetTags() {
var tags []string var tags []string
repoName := ra.GetString("repo_name") repoName := ra.GetString("repo_name")
result, err := svc_utils.RegistryAPIGet(svc_utils.BuildRegistryURL(repoName, "tags", "list"), ra.username)
tags, err := ra.registry.ListTag(repoName)
if err != nil { if err != nil {
log.Errorf("Failed to get repo tags, repo name: %s, error: %v", repoName, err) e, ok := errors.ParseError(err)
ra.RenderError(http.StatusInternalServerError, "Failed to get repo tags") if ok {
} else { log.Info(e)
t := tag{} ra.CustomAbort(e.StatusCode, e.Message)
json.Unmarshal(result, &t) } else {
tags = t.Tags log.Error(err)
ra.CustomAbort(http.StatusInternalServerError, "internal error")
}
} }
ra.Data["json"] = tags ra.Data["json"] = tags
ra.ServeJSON() ra.ServeJSON()
} }
@ -148,14 +246,20 @@ func (ra *RepositoryAPI) GetManifests() {
item := models.RepoItem{} item := models.RepoItem{}
result, err := svc_utils.RegistryAPIGet(svc_utils.BuildRegistryURL(repoName, "manifests", tag), ra.username) _, _, payload, err := ra.registry.PullManifest(repoName, tag, registry.ManifestVersion1)
if err != nil { if err != nil {
log.Errorf("Failed to get manifests for repo, repo name: %s, tag: %s, error: %v", repoName, tag, err) e, ok := errors.ParseError(err)
ra.RenderError(http.StatusInternalServerError, "Internal Server Error") if ok {
return log.Info(e)
ra.CustomAbort(e.StatusCode, e.Message)
} else {
log.Error(err)
ra.CustomAbort(http.StatusInternalServerError, "internal error")
}
} }
mani := manifest{} mani := manifest{}
err = json.Unmarshal(result, &mani) err = json.Unmarshal(payload, &mani)
if err != nil { if err != nil {
log.Errorf("Failed to decode json from response for manifests, repo name: %s, tag: %s, error: %v", repoName, tag, err) log.Errorf("Failed to decode json from response for manifests, repo name: %s, tag: %s, error: %v", repoName, tag, err)
ra.RenderError(http.StatusInternalServerError, "Internal Server Error") ra.RenderError(http.StatusInternalServerError, "Internal Server Error")
@ -169,7 +273,6 @@ func (ra *RepositoryAPI) GetManifests() {
ra.RenderError(http.StatusInternalServerError, "Internal Server Error") ra.RenderError(http.StatusInternalServerError, "Internal Server Error")
return return
} }
item.CreatedStr = item.Created.Format("2006-01-02 15:04:05")
item.DurationDays = strconv.Itoa(int(time.Since(item.Created).Hours()/24)) + " days" item.DurationDays = strconv.Itoa(int(time.Since(item.Created).Hours()/24)) + " days"
ra.Data["json"] = item ra.Data["json"] = item

View File

@ -49,7 +49,10 @@ func (c *CommonController) Login() {
principal := c.GetString("principal") principal := c.GetString("principal")
password := c.GetString("password") password := c.GetString("password")
user, err := auth.Login(models.AuthModel{principal, password}) user, err := auth.Login(models.AuthModel{
Principal: principal,
Password: password,
})
if err != nil { if err != nil {
log.Errorf("Error occurred in UserLogin: %v", err) log.Errorf("Error occurred in UserLogin: %v", err)
c.CustomAbort(http.StatusUnauthorized, "") c.CustomAbort(http.StatusUnauthorized, "")

View File

@ -82,7 +82,7 @@ func InitDB() {
c.Close() c.Close()
ch <- 1 ch <- 1
} else { } else {
log.Info("failed to connect to db, retry after 2 seconds...") log.Errorf("failed to connect to db, retry after 2 seconds :%v", err)
time.Sleep(2 * time.Second) time.Sleep(2 * time.Second)
} }
} }

View File

@ -206,7 +206,10 @@ func TestLoginByUserName(t *testing.T) {
Password: "Abc12345", Password: "Abc12345",
} }
loginUser, err := LoginByDb(models.AuthModel{userQuery.Username, userQuery.Password}) loginUser, err := LoginByDb(models.AuthModel{
Principal: userQuery.Username,
Password: userQuery.Password,
})
if err != nil { if err != nil {
t.Errorf("Error occurred in LoginByDb: %v", err) t.Errorf("Error occurred in LoginByDb: %v", err)
} }
@ -226,7 +229,10 @@ func TestLoginByEmail(t *testing.T) {
Password: "Abc12345", Password: "Abc12345",
} }
loginUser, err := LoginByDb(models.AuthModel{userQuery.Email, userQuery.Password}) loginUser, err := LoginByDb(models.AuthModel{
Principal: userQuery.Email,
Password: userQuery.Password,
})
if err != nil { if err != nil {
t.Errorf("Error occurred in LoginByDb: %v", err) t.Errorf("Error occurred in LoginByDb: %v", err)
} }

View File

@ -58,6 +58,10 @@ func IsAdminRole(userIDOrUsername interface{}) (bool, error) {
return false, fmt.Errorf("invalid parameter, only int and string are supported: %v", userIDOrUsername) return false, fmt.Errorf("invalid parameter, only int and string are supported: %v", userIDOrUsername)
} }
if u.UserID == NonExistUserID && len(u.Username) == 0 {
return false, nil
}
user, err := GetUser(u) user, err := GetUser(u)
if err != nil { if err != nil {
return false, err return false, err

View File

@ -109,7 +109,7 @@ func ListUsers(query models.User) ([]models.User, error) {
return u, err return u, err
} }
// ToggleUserAdminRole gives a user admim role. // ToggleUserAdminRole gives a user admin role.
func ToggleUserAdminRole(u models.User) error { func ToggleUserAdminRole(u models.User) error {
o := orm.NewOrm() o := orm.NewOrm()

65
kubernetes_deployment.md Normal file
View File

@ -0,0 +1,65 @@
## Deploy harbor on kubernetes.
For now, it's a little tricky to start harbor on kubernetes because
1. Registry uses https, so we need cert or workaround to avoid errors like this:
```
Error response from daemon: invalid registry endpoint https://{HOST}/v0/: unable to ping registry endpoint https://{HOST}/v0/
v2 ping attempt failed with error: Get https://{HOST}/v2/: EOF
v1 ping attempt failed with error: Get https://{HOST}/v1/_ping: EOF. If this private registry supports only HTTP or HTTPS with an unknown CA certificate, please add `--insecure-registry {HOST}` to the daemon's arguments. In the case of HTTPS, if you have access to the registry's CA certificate, no need for the flag; simply place the CA certificate at /etc/docker/certs.d/{HOST}/ca.crt
```
There is a workaround if you don't have a cert. The workaround is to add the host into the list of insecure registry by editting the ```/etc/default/docker``` file:
```
sudo vi /etc/default/docker
```
add the line at the end of file:
```
DOCKER_OPTS="$DOCKER_OPTS --insecure-registry={HOST}"
```
restart docker service
```
sudo service docker restart
```
2. The registry config file need to know the IP (or DNS name) of the registry, but on kubernetes, you won't know the IP before the service is created. There are several workarounds to solve this problem for now:
- Use DNS name and link th DNS name with the IP after the service is created.
- Rebuild the registry image with the service IP after the service is created and use ```kubectl rolling-update``` to update to the new image.
To start harbor on kubernetes, you first need to build the docker images. The docker images for deploying Harbor on Kubernetes depends on the docker images to deploy Harbor with docker-compose. So the first step is to build docker images with docker-compose. Before actually building the images, you need to first adjust the [configuration](https://github.com/vmware/harbor/blob/master/Deploy/harbor.cfg):
- Change the [hostname](https://github.com/vmware/harbor/blob/master/Deploy/harbor.cfg#L5) to ```localhost```
- Adjust the [email settings](https://github.com/vmware/harbor/blob/master/Deploy/harbor.cfg#L11) according to your needs.
Then you can run the following commends to build docker images:
```
cd Deploy
./prepare
docker-compose build
docker build -f kubernetes/dockerfiles/proxy-dockerfile -t {your_account}/proxy .
docker build -f kubernetes/dockerfiles/registry-dockerfile -t {your_account}/registry .
docker build -f kubernetes/dockerfiles/ui-dockerfile -t {your_account}/deploy_ui .
docker tag deploy_mysql {your_account}/deploy_mysql
docker push {your_account}/proxy
docker push {your_account}/registry
docker push {your_account}/deploy_ui
docker push {your_account}/deploy_mysql
```
where "your_account" is your own registry. Then you need to update the "image" field in the ```*-rc.yaml``` files at:
```
Deploy/kubernetes/mysql-rc.yaml
Deploy/kubernetes/proxy-rc.yaml
Deploy/kubernetes/registry-rc.yaml
Deploy/kubernetes/ui-rc.yaml
```
Further more, the following configuration could be changed according to your need:
- **harbor_admin_password**: The password for the administrator of Harbor, by default the password is Harbor12345. You can changed it [here](https://github.com/vmware/harbor/blob/master/Deploy/kubernetes/ui-rc.yaml#L36).
- **auth_mode**: The authentication mode of Harbor. By default it is *db_auth*, i.e. the credentials are stored in a database. Please set it to *ldap_auth* if you want to verify user's credentials against an LDAP server. You can change the configuration [here](https://github.com/vmware/harbor/blob/master/Deploy/kubernetes/ui-rc.yaml#L40).
- **ldap_url**: The URL for LDAP endpoint, for example ldaps://ldap.mydomain.com. It is only used when **auth_mode** is set to *ldap_auth*. It could be changed [here](https://github.com/vmware/harbor/blob/master/Deploy/kubernetes/ui-rc.yaml#L42).
- **ldap_basedn**: The basedn template for verifying the user's credentials against LDAP, for example uid=%s,ou=people,dc=mydomain,dc=com. It is only used when **auth_mode** is set to *ldap_auth*. It could be changed [here](https://github.com/vmware/harbor/blob/master/Deploy/kubernetes/ui-rc.yaml#L44).
- **db_password**: The password of root user of mySQL database. Change this password for any production use. You need to change both [here](https://github.com/vmware/harbor/blob/master/Deploy/kubernetes/ui-rc.yaml#L28) and [here](https://github.com/vmware/harbor/blob/master/Deploy/harbor.cfg#L32) to make the change. Please note, you need to change the ```harbor.cfg``` before building the docker images.
Finally you can start the jobs by running:
```
kubectl create -f Deploy/kubernetes
```

View File

@ -29,7 +29,6 @@ type RepoItem struct {
ID string `json:"Id"` ID string `json:"Id"`
Parent string `json:"Parent"` Parent string `json:"Parent"`
Created time.Time `json:"Created"` Created time.Time `json:"Created"`
CreatedStr string `json:"CreatedStr"`
DurationDays string `json:"Duration Days"` DurationDays string `json:"Duration Days"`
Author string `json:"Author"` Author string `json:"Author"`
Architecture string `json:"Architecture"` Architecture string `json:"Architecture"`

View File

@ -38,7 +38,7 @@ const manifestPattern = `^application/vnd.docker.distribution.manifest.v\d\+json
// Post handles POST request, and records audit log or refreshes cache based on event. // Post handles POST request, and records audit log or refreshes cache based on event.
func (n *NotificationHandler) Post() { func (n *NotificationHandler) Post() {
var notification models.Notification var notification models.Notification
// log.Info("Notification Handler triggered!\n") //log.Info("Notification Handler triggered!\n")
// log.Infof("request body in string: %s", string(n.Ctx.Input.CopyBody())) // log.Infof("request body in string: %s", string(n.Ctx.Input.CopyBody()))
err := json.Unmarshal(n.Ctx.Input.CopyBody(1<<32), &notification) err := json.Unmarshal(n.Ctx.Input.CopyBody(1<<32), &notification)

View File

@ -13,7 +13,7 @@
limitations under the License. limitations under the License.
*/ */
package utils package token
import ( import (
"crypto" "crypto"
@ -80,7 +80,7 @@ func FilterAccess(username string, authenticated bool, a *token.ResourceActions)
return return
} }
if exist { if exist {
permission = "RW" permission = "RWM"
} else { } else {
permission = "" permission = ""
log.Infof("project %s does not exist, set empty permission for admin\n", projectName) log.Infof("project %s does not exist, set empty permission for admin\n", projectName)
@ -96,6 +96,9 @@ func FilterAccess(username string, authenticated bool, a *token.ResourceActions)
if strings.Contains(permission, "W") { if strings.Contains(permission, "W") {
a.Actions = append(a.Actions, "push") a.Actions = append(a.Actions, "push")
} }
if strings.Contains(permission, "M") {
a.Actions = append(a.Actions, "*")
}
if strings.Contains(permission, "R") || dao.IsProjectPublic(projectName) { if strings.Contains(permission, "R") || dao.IsProjectPublic(projectName) {
a.Actions = append(a.Actions, "pull") a.Actions = append(a.Actions, "pull")
} }

View File

@ -13,53 +13,53 @@
limitations under the License. limitations under the License.
*/ */
package service package token
import ( import (
"net/http" "net/http"
"github.com/vmware/harbor/auth" "github.com/vmware/harbor/auth"
"github.com/vmware/harbor/models" "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/vmware/harbor/utils/log"
"github.com/astaxie/beego" "github.com/astaxie/beego"
"github.com/docker/distribution/registry/auth/token" "github.com/docker/distribution/registry/auth/token"
) )
// TokenHandler handles request on /service/token, which is the auth provider for registry. // Handler handles request on /service/token, which is the auth provider for registry.
type TokenHandler struct { type Handler struct {
beego.Controller beego.Controller
} }
// Get handles GET request, it checks the http header for user credentials // Get handles GET request, it checks the http header for user credentials
// and parse service and scope based on docker registry v2 standard, // and parse service and scope based on docker registry v2 standard,
// checkes the permission agains local DB and generates jwt token. // checkes the permission agains local DB and generates jwt token.
func (a *TokenHandler) Get() { func (h *Handler) Get() {
request := a.Ctx.Request request := h.Ctx.Request
log.Infof("request url: %v", request.URL.String()) log.Infof("request url: %v", request.URL.String())
username, password, _ := request.BasicAuth() username, password, _ := request.BasicAuth()
authenticated := authenticate(username, password) authenticated := authenticate(username, password)
service := a.GetString("service") service := h.GetString("service")
scopes := a.GetStrings("scope") scopes := h.GetStrings("scope")
log.Debugf("scopes: %+v", scopes) log.Debugf("scopes: %+v", scopes)
if len(scopes) == 0 && !authenticated { if len(scopes) == 0 && !authenticated {
log.Info("login request with invalid credentials") log.Info("login request with invalid credentials")
a.CustomAbort(http.StatusUnauthorized, "") h.CustomAbort(http.StatusUnauthorized, "")
} }
access := svc_utils.GetResourceActions(scopes) access := GetResourceActions(scopes)
for _, a := range access { for _, a := range access {
svc_utils.FilterAccess(username, authenticated, a) FilterAccess(username, authenticated, a)
} }
a.serveToken(username, service, access) h.serveToken(username, service, access)
} }
func (a *TokenHandler) serveToken(username, service string, access []*token.ResourceActions) { func (h *Handler) serveToken(username, service string, access []*token.ResourceActions) {
writer := a.Ctx.ResponseWriter writer := h.Ctx.ResponseWriter
//create token //create token
rawToken, err := svc_utils.MakeToken(username, service, access) rawToken, err := MakeToken(username, service, access)
if err != nil { if err != nil {
log.Errorf("Failed to make token, error: %v", err) log.Errorf("Failed to make token, error: %v", err)
writer.WriteHeader(http.StatusInternalServerError) writer.WriteHeader(http.StatusInternalServerError)
@ -67,12 +67,15 @@ func (a *TokenHandler) serveToken(username, service string, access []*token.Reso
} }
tk := make(map[string]string) tk := make(map[string]string)
tk["token"] = rawToken tk["token"] = rawToken
a.Data["json"] = tk h.Data["json"] = tk
a.ServeJSON() h.ServeJSON()
} }
func authenticate(principal, password string) bool { func authenticate(principal, password string) bool {
user, err := auth.Login(models.AuthModel{principal, password}) user, err := auth.Login(models.AuthModel{
Principal: principal,
Password: password,
})
if err != nil { if err != nil {
log.Errorf("Error occurred in UserLogin: %v", err) log.Errorf("Error occurred in UserLogin: %v", err)
return false return false

View File

@ -16,11 +16,11 @@
package utils package utils
import ( import (
"encoding/json" "os"
"time" "time"
"github.com/vmware/harbor/models"
"github.com/vmware/harbor/utils/log" "github.com/vmware/harbor/utils/log"
"github.com/vmware/harbor/utils/registry"
"github.com/astaxie/beego/cache" "github.com/astaxie/beego/cache"
) )
@ -28,6 +28,8 @@ import (
// Cache is the global cache in system. // Cache is the global cache in system.
var Cache cache.Cache var Cache cache.Cache
var registryClient *registry.Registry
const catalogKey string = "catalog" const catalogKey string = "catalog"
func init() { func init() {
@ -36,20 +38,39 @@ func init() {
if err != nil { if err != nil {
log.Errorf("Failed to initialize cache, error:%v", err) log.Errorf("Failed to initialize cache, error:%v", err)
} }
endpoint := os.Getenv("REGISTRY_URL")
client := registry.NewClientUsernameAuthHandlerEmbeded("admin")
registryClient, err = registry.New(endpoint, client)
if err != nil {
log.Fatalf("error occurred while initializing authentication handler used by cache: %v", err)
}
} }
// RefreshCatalogCache calls registry's API to get repository list and write it to cache. // RefreshCatalogCache calls registry's API to get repository list and write it to cache.
func RefreshCatalogCache() error { func RefreshCatalogCache() error {
result, err := RegistryAPIGet(BuildRegistryURL("_catalog"), "") log.Debug("refreshing catalog cache...")
rs, err := registryClient.Catalog()
if err != nil { if err != nil {
return err return err
} }
repoResp := models.Repo{}
err = json.Unmarshal(result, &repoResp) repos := []string{}
if err != nil {
return err for _, repo := range rs {
tags, err := registryClient.ListTag(repo)
if err != nil {
log.Errorf("error occurred while list tag for %s: %v", repo, err)
return err
}
if len(tags) != 0 {
repos = append(repos, repo)
log.Debugf("add %s to catalog cache", repo)
}
} }
Cache.Put(catalogKey, repoResp.Repositories, 600*time.Second)
Cache.Put(catalogKey, repos, 600*time.Second)
return nil return nil
} }

View File

@ -1,114 +0,0 @@
/*
Copyright (c) 2016 VMware, Inc. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package utils
import (
"errors"
"fmt"
"io/ioutil"
"net/http"
"os"
"regexp"
"github.com/vmware/harbor/utils/log"
)
// BuildRegistryURL ...
func BuildRegistryURL(segments ...string) string {
registryURL := os.Getenv("REGISTRY_URL")
if registryURL == "" {
registryURL = "http://localhost:5000"
}
url := registryURL + "/v2"
for _, s := range segments {
if s == "v2" {
log.Debugf("unnecessary v2 in %v", segments)
continue
}
url += "/" + s
}
return url
}
// RegistryAPIGet triggers GET request to the URL which is the endpoint of registry and returns the response body.
// It will attach a valid jwt token to the request if registry requires.
func RegistryAPIGet(url, username string) ([]byte, error) {
log.Debugf("Registry API url: %s", url)
response, err := http.Get(url)
if err != nil {
return nil, err
}
result, err := ioutil.ReadAll(response.Body)
if err != nil {
return nil, err
}
defer response.Body.Close()
if response.StatusCode == http.StatusOK {
return result, nil
} else if response.StatusCode == http.StatusUnauthorized {
authenticate := response.Header.Get("WWW-Authenticate")
log.Debugf("authenticate header: %s", authenticate)
var service string
var scopes []string
//Disregard the case for hanlding multiple scopes for http call initiated from UI, as there's refactor planned.
re := regexp.MustCompile(`service=\"(.*?)\".*scope=\"(.*?)\"`)
res := re.FindStringSubmatch(authenticate)
if len(res) > 2 {
service = res[1]
scopes = append(scopes, res[2])
}
token, err := GenTokenForUI(username, service, scopes)
if err != nil {
return nil, err
}
request, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
request.Header.Add("Authorization", "Bearer "+token)
client := &http.Client{}
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
// log.Infof("via length: %d\n", len(via))
if len(via) >= 10 {
return fmt.Errorf("too many redirects")
}
for k, v := range via[0].Header {
if _, ok := req.Header[k]; !ok {
req.Header[k] = v
}
}
return nil
}
response, err = client.Do(request)
if err != nil {
return nil, err
}
if response.StatusCode != http.StatusOK {
errMsg := fmt.Sprintf("Unexpected return code from registry: %d", response.StatusCode)
log.Error(errMsg)
return nil, fmt.Errorf(errMsg)
}
result, err = ioutil.ReadAll(response.Body)
if err != nil {
return nil, err
}
defer response.Body.Close()
return result, nil
} else {
return nil, errors.New(string(result))
}
}

View File

@ -82,4 +82,4 @@ index_desc_2 = 2. Efficiency: A private registry server is set up within the org
index_desc_3 = 3. Access Control: RBAC (Role Based Access Control) is provided. User management can be integrated with existing enterprise identity services like AD/LDAP. index_desc_3 = 3. Access Control: RBAC (Role Based Access Control) is provided. User management can be integrated with existing enterprise identity services like AD/LDAP.
index_desc_4 = 4. Audit: All access to the registry are logged and can be used for audit purpose. index_desc_4 = 4. Audit: All access to the registry are logged and can be used for audit purpose.
index_desc_5 = 5. GUI: User friendly single-pane-of-glass management console. index_desc_5 = 5. GUI: User friendly single-pane-of-glass management console.
index_title = An enterprise-class registry server

View File

@ -82,3 +82,4 @@ index_desc_2 = 2. 效率: 搭建组织内部的私有容器Registry服务
index_desc_3 = 3. 访问控制: 提供基于角色的访问控制,可集成企业目前拥有的用户管理系统(如:AD/LDAP index_desc_3 = 3. 访问控制: 提供基于角色的访问控制,可集成企业目前拥有的用户管理系统(如:AD/LDAP
index_desc_4 = 4. 审计: 所有访问Registry服务的操作均被记录便于日后审计。 index_desc_4 = 4. 审计: 所有访问Registry服务的操作均被记录便于日后审计。
index_desc_5 = 5. 管理界面: 具有友好易用图形管理界面。 index_desc_5 = 5. 管理界面: 具有友好易用图形管理界面。
index_title = 企业级 Registry 服务

View File

@ -130,8 +130,7 @@ jQuery(function(){
data[i] = "N/A"; data[i] = "N/A";
} }
} }
data.Created = data.CreatedStr; data.Created = moment(new Date(data.Created)).format("YYYY-MM-DD HH:mm:ss");
delete data.CreatedStr;
$("#dlgModal").dialogModal({"title": i18n.getMessage("image_details"), "content": data}); $("#dlgModal").dialogModal({"title": i18n.getMessage("image_details"), "content": data});
} }
@ -246,7 +245,7 @@ jQuery(function(){
var userId = userList[i].UserId; var userId = userList[i].UserId;
var roleId = userList[i].RoleId; var roleId = userList[i].RoleId;
var username = userList[i].Username; var username = userList[i].username;
var roleNameList = []; var roleNameList = [];
for(var j = i; j < userList.length; i++, j++){ for(var j = i; j < userList.length; i++, j++){

View File

@ -19,6 +19,7 @@ import (
"github.com/vmware/harbor/api" "github.com/vmware/harbor/api"
"github.com/vmware/harbor/controllers" "github.com/vmware/harbor/controllers"
"github.com/vmware/harbor/service" "github.com/vmware/harbor/service"
"github.com/vmware/harbor/service/token"
"github.com/astaxie/beego" "github.com/astaxie/beego"
) )
@ -63,5 +64,5 @@ func initRouters() {
//external service that hosted on harbor process: //external service that hosted on harbor process:
beego.Router("/service/notifications", &service.NotificationHandler{}) beego.Router("/service/notifications", &service.NotificationHandler{})
beego.Router("/service/token", &service.TokenHandler{}) beego.Router("/service/token", &token.Handler{})
} }

View File

@ -0,0 +1,32 @@
/*
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 auth
import (
"net/http"
au "github.com/docker/distribution/registry/client/auth"
"github.com/vmware/harbor/utils/log"
)
// ParseChallengeFromResponse ...
func ParseChallengeFromResponse(resp *http.Response) []au.Challenge {
challenges := au.ResponseChallenges(resp)
log.Debugf("challenges: %v", challenges)
return challenges
}

View File

@ -0,0 +1,197 @@
/*
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 auth
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strings"
token_util "github.com/vmware/harbor/service/token"
"github.com/vmware/harbor/utils/log"
registry_errors "github.com/vmware/harbor/utils/registry/errors"
)
const (
// credential type
basicAuth string = "basic_auth"
secretKey string = "secret_key"
)
// Handler authorizes the request when encounters a 401 error
type Handler interface {
// Schema : basic, bearer
Schema() string
//AuthorizeRequest adds basic auth or token auth to the header of request
AuthorizeRequest(req *http.Request, params map[string]string) error
}
// Credential ...
type Credential interface {
// AddAuthorization adds authorization information to request
AddAuthorization(req *http.Request)
}
type basicAuthCredential struct {
username string
password string
}
// NewBasicAuthCredential ...
func NewBasicAuthCredential(username, password string) Credential {
return &basicAuthCredential{
username: username,
password: password,
}
}
func (b *basicAuthCredential) AddAuthorization(req *http.Request) {
req.SetBasicAuth(b.username, b.password)
}
type token struct {
Token string `json:"token"`
}
type standardTokenHandler struct {
client *http.Client
credential Credential
}
// NewStandardTokenHandler returns a standard token handler. The handler will request a token
// from token server whose URL is specified in the "WWW-authentication" header and add it to
// the origin request
// TODO deal with https
func NewStandardTokenHandler(credential Credential) Handler {
return &standardTokenHandler{
client: &http.Client{
Transport: http.DefaultTransport,
},
credential: credential,
}
}
// Schema implements the corresponding method in interface AuthHandler
func (t *standardTokenHandler) Schema() string {
return "bearer"
}
// AuthorizeRequest implements the corresponding method in interface AuthHandler
func (t *standardTokenHandler) AuthorizeRequest(req *http.Request, params map[string]string) error {
realm, ok := params["realm"]
if !ok {
return errors.New("no realm")
}
service := params["service"]
scope := params["scope"]
u, err := url.Parse(realm)
if err != nil {
return err
}
q := u.Query()
q.Add("service", service)
for _, s := range strings.Split(scope, " ") {
q.Add("scope", s)
}
u.RawQuery = q.Encode()
r, err := http.NewRequest("GET", u.String(), nil)
if err != nil {
return err
}
t.credential.AddAuthorization(r)
resp, err := t.client.Do(r)
if err != nil {
return err
}
defer resp.Body.Close()
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
return registry_errors.Error{
StatusCode: resp.StatusCode,
Message: string(b),
}
}
decoder := json.NewDecoder(resp.Body)
tk := &token{}
if err = decoder.Decode(tk); err != nil {
return err
}
req.Header.Add(http.CanonicalHeaderKey("Authorization"), fmt.Sprintf("Bearer %s", tk.Token))
log.Debugf("standardTokenHandler generated token successfully | %s %s", req.Method, req.URL)
return nil
}
type usernameTokenHandler struct {
username string
}
// NewUsernameTokenHandler returns a handler which will generate
// a token according the user's privileges
func NewUsernameTokenHandler(username string) Handler {
return &usernameTokenHandler{
username: username,
}
}
// Schema implements the corresponding method in interface AuthHandler
func (u *usernameTokenHandler) Schema() string {
return "bearer"
}
// AuthorizeRequest implements the corresponding method in interface AuthHandler
func (u *usernameTokenHandler) AuthorizeRequest(req *http.Request, params map[string]string) error {
service := params["service"]
scopes := []string{}
scope := params["scope"]
if len(scope) != 0 {
scopes = strings.Split(scope, " ")
}
token, err := token_util.GenTokenForUI(u.username, service, scopes)
if err != nil {
return err
}
req.Header.Add(http.CanonicalHeaderKey("Authorization"), fmt.Sprintf("Bearer %s", token))
log.Debugf("usernameTokenHandler generated token successfully | %s %s", req.Method, req.URL)
return nil
}

View File

@ -0,0 +1,38 @@
/*
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 errors
import (
"fmt"
)
// Error : if response's status code is not 200 or does not meet requirement,
// an Error instance will be returned
type Error struct {
StatusCode int
Message string
}
// Error ...
func (e Error) Error() string {
return fmt.Sprintf("%d %s", e.StatusCode, 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
}

View File

@ -0,0 +1,116 @@
/*
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 registry
import (
"net/http"
"github.com/vmware/harbor/utils/log"
"github.com/vmware/harbor/utils/registry/auth"
)
// NewClient returns a http.Client according to the handlers provided
func NewClient(handlers []auth.Handler) *http.Client {
transport := NewAuthTransport(http.DefaultTransport, handlers)
return &http.Client{
Transport: transport,
}
}
// NewClientStandardAuthHandlerEmbeded return a http.Client which will authorize the request
// according to the credential provided and send it again when encounters a 401 error
func NewClientStandardAuthHandlerEmbeded(credential auth.Credential) *http.Client {
handlers := []auth.Handler{}
tokenHandler := auth.NewStandardTokenHandler(credential)
handlers = append(handlers, tokenHandler)
return NewClient(handlers)
}
// NewClientUsernameAuthHandlerEmbeded return a http.Client which will authorize the request
// according to the user's privileges and send it again when encounters a 401 error
func NewClientUsernameAuthHandlerEmbeded(username string) *http.Client {
handlers := []auth.Handler{}
tokenHandler := auth.NewUsernameTokenHandler(username)
handlers = append(handlers, tokenHandler)
return NewClient(handlers)
}
type authTransport struct {
transport http.RoundTripper
handlers []auth.Handler
}
// NewAuthTransport wraps the AuthHandlers to be http.RounTripper
func NewAuthTransport(transport http.RoundTripper, handlers []auth.Handler) http.RoundTripper {
return &authTransport{
transport: transport,
handlers: handlers,
}
}
// RoundTrip ...
func (a *authTransport) RoundTrip(req *http.Request) (*http.Response, error) {
originResp, originErr := a.transport.RoundTrip(req)
if originErr != nil {
return originResp, originErr
}
log.Debugf("%d | %s %s", originResp.StatusCode, req.Method, req.URL)
if originResp.StatusCode != http.StatusUnauthorized {
return originResp, nil
}
challenges := auth.ParseChallengeFromResponse(originResp)
reqChanged := false
for _, challenge := range challenges {
scheme := challenge.Scheme
for _, handler := range a.handlers {
if scheme != handler.Schema() {
log.Debugf("scheme not match: %s %s, skip", scheme, handler.Schema())
continue
}
if err := handler.AuthorizeRequest(req, challenge.Parameters); err != nil {
return nil, err
}
reqChanged = true
}
}
if !reqChanged {
log.Warning("no handler match scheme")
return originResp, nil
}
resp, err := a.transport.RoundTrip(req)
if err == nil {
log.Debugf("%d | %s %s", resp.StatusCode, req.Method, req.URL)
}
return resp, err
}

View File

@ -0,0 +1,25 @@
/*
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 registry
import (
"github.com/docker/distribution"
)
// UnMarshal converts []byte to be distribution.Manifest
func UnMarshal(mediaType string, data []byte) (distribution.Manifest, distribution.Descriptor, error) {
return distribution.UnmarshalManifest(mediaType, data)
}

316
utils/registry/registry.go Normal file
View File

@ -0,0 +1,316 @@
/*
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 registry
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"github.com/docker/distribution/manifest"
"github.com/docker/distribution/manifest/schema1"
"github.com/docker/distribution/manifest/schema2"
"github.com/vmware/harbor/utils/registry/errors"
)
// Registry holds information of a registry entiry
type Registry struct {
Endpoint *url.URL
client *http.Client
ub *uRLBuilder
}
type uRLBuilder struct {
root *url.URL
}
var (
// ManifestVersion1 : schema 1
ManifestVersion1 = manifest.Versioned{
SchemaVersion: 1,
MediaType: schema1.MediaTypeManifest,
}
// ManifestVersion2 : schema 2
ManifestVersion2 = manifest.Versioned{
SchemaVersion: 2,
MediaType: schema2.MediaTypeManifest,
}
)
// New returns an instance of Registry
func New(endpoint string, client *http.Client) (*Registry, error) {
u, err := url.Parse(endpoint)
if err != nil {
return nil, err
}
return &Registry{
Endpoint: u,
client: client,
ub: &uRLBuilder{
root: u,
},
}, nil
}
// Catalog ...
func (r *Registry) Catalog() ([]string, error) {
repos := []string{}
req, err := http.NewRequest("GET", r.ub.buildCatalogURL(), nil)
if err != nil {
return repos, err
}
resp, err := r.client.Do(req)
if err != nil {
return repos, err
}
defer resp.Body.Close()
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return repos, err
}
if resp.StatusCode == http.StatusOK {
catalogResp := struct {
Repositories []string `json:"repositories"`
}{}
if err := json.Unmarshal(b, &catalogResp); err != nil {
return repos, err
}
repos = catalogResp.Repositories
return repos, nil
}
return repos, errors.Error{
StatusCode: resp.StatusCode,
Message: string(b),
}
}
// ListTag ...
func (r *Registry) ListTag(name string) ([]string, error) {
tags := []string{}
req, err := http.NewRequest("GET", r.ub.buildTagListURL(name), nil)
if err != nil {
return tags, err
}
resp, err := r.client.Do(req)
if err != nil {
return tags, err
}
defer resp.Body.Close()
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return tags, err
}
if resp.StatusCode == http.StatusOK {
tagsResp := struct {
Tags []string `json:"tags"`
}{}
if err := json.Unmarshal(b, &tagsResp); err != nil {
return tags, err
}
tags = tagsResp.Tags
return tags, nil
}
return tags, errors.Error{
StatusCode: resp.StatusCode,
Message: string(b),
}
}
// ManifestExist ...
func (r *Registry) ManifestExist(name, reference string) (digest string, exist bool, err error) {
req, err := http.NewRequest("HEAD", r.ub.buildManifestURL(name, reference), nil)
if err != nil {
return
}
// request Schema 2 manifest, if the registry does not support it,
// Schema 1 manifest will be returned
req.Header.Set(http.CanonicalHeaderKey("Accept"), schema2.MediaTypeManifest)
resp, err := r.client.Do(req)
if err != nil {
return
}
if resp.StatusCode == http.StatusOK {
exist = true
digest = resp.Header.Get(http.CanonicalHeaderKey("Docker-Content-Digest"))
return
}
if resp.StatusCode == http.StatusNotFound {
return
}
defer resp.Body.Close()
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return
}
err = errors.Error{
StatusCode: resp.StatusCode,
Message: string(b),
}
return
}
// PullManifest ...
func (r *Registry) PullManifest(name, reference string, version manifest.Versioned) (digest, mediaType string, payload []byte, err error) {
req, err := http.NewRequest("GET", r.ub.buildManifestURL(name, reference), nil)
if err != nil {
return
}
// if the registry does not support schema 2, schema 1 manifest will be returned
req.Header.Set(http.CanonicalHeaderKey("Accept"), version.MediaType)
resp, err := r.client.Do(req)
if err != nil {
return
}
defer resp.Body.Close()
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return
}
if resp.StatusCode == http.StatusOK {
digest = resp.Header.Get(http.CanonicalHeaderKey("Docker-Content-Digest"))
mediaType = resp.Header.Get(http.CanonicalHeaderKey("Content-Type"))
payload = b
return
}
err = errors.Error{
StatusCode: resp.StatusCode,
Message: string(b),
}
return
}
// DeleteManifest ...
func (r *Registry) DeleteManifest(name, digest string) error {
req, err := http.NewRequest("DELETE", r.ub.buildManifestURL(name, digest), nil)
if err != nil {
return err
}
resp, err := r.client.Do(req)
if err != nil {
return err
}
if resp.StatusCode == http.StatusAccepted {
return nil
}
defer resp.Body.Close()
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
return errors.Error{
StatusCode: resp.StatusCode,
Message: string(b),
}
}
// DeleteTag ...
func (r *Registry) DeleteTag(name, tag string) error {
digest, exist, err := r.ManifestExist(name, tag)
if err != nil {
return err
}
if !exist {
return errors.Error{
StatusCode: http.StatusNotFound,
}
}
return r.DeleteManifest(name, digest)
}
// DeleteBlob ...
func (r *Registry) DeleteBlob(name, digest string) error {
req, err := http.NewRequest("DELETE", r.ub.buildBlobURL(name, digest), nil)
if err != nil {
return err
}
resp, err := r.client.Do(req)
if err != nil {
return err
}
if resp.StatusCode == http.StatusAccepted {
return nil
}
defer resp.Body.Close()
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
return errors.Error{
StatusCode: resp.StatusCode,
Message: string(b),
}
}
func (u *uRLBuilder) buildCatalogURL() string {
return fmt.Sprintf("%s/v2/_catalog", u.root.String())
}
func (u *uRLBuilder) buildTagListURL(name string) string {
return fmt.Sprintf("%s/v2/%s/tags/list", u.root.String(), name)
}
func (u *uRLBuilder) buildManifestURL(name, reference string) string {
return fmt.Sprintf("%s/v2/%s/manifests/%s", u.root.String(), name, reference)
}
func (u *uRLBuilder) buildBlobURL(name, reference string) string {
return fmt.Sprintf("%s/v2/%s/blobs/%s", u.root.String(), name, reference)
}

View File

@ -16,7 +16,7 @@
<div class="jumbotron"> <div class="jumbotron">
<div class="container"> <div class="container">
<img class="pull-left" src="static/resources/image/Harbor_Logo_rec.png" alt="Harbor's Logo" height="180" width="360"/> <img class="pull-left" src="static/resources/image/Harbor_Logo_rec.png" alt="Harbor's Logo" height="180" width="360"/>
<p class="pull-left" style="margin-top: 85px; color: #245580; font-size: 30pt;">An enterprise-class registry server</p> <p class="pull-left" style="margin-top: 85px; color: #245580; font-size: 30pt; text-align: center; width: 60%;">{{i18n .Lang "index_title"}}</p>
</div> </div>
</div> </div>