Merge remote-tracking branch 'upstream/dev' into dev-revised

This commit is contained in:
kunw 2017-03-13 18:23:09 +08:00
commit e80840c2d3
22 changed files with 405 additions and 142 deletions

View File

@ -78,7 +78,7 @@ COMPILETAG=compile_normal
REGISTRYSERVER= REGISTRYSERVER=
REGISTRYPROJECTNAME=vmware REGISTRYPROJECTNAME=vmware
DEVFLAG=true DEVFLAG=true
NORTARYFLAG=false NOTARYFLAG=false
#clarity parameters #clarity parameters
CLARITYIMAGE=danieljt/harbor-clarity-base[:tag] CLARITYIMAGE=danieljt/harbor-clarity-base[:tag]
@ -209,7 +209,7 @@ compile_clarity:
@$(DOCKERCMD) run --rm -v $(UIPATH)/static/new-ui:$(CLARITYSEEDPATH)/dist -v $(UINGPATH)/src:$(CLARITYSEEDPATH)/src -v $(UINGPATH)/src/app:$(CLARITYSEEDPATH)/src/app $(CLARITYIMAGE) $(SHELL) $(CLARITYBUILDSCRIPT) @$(DOCKERCMD) run --rm -v $(UIPATH)/static/new-ui:$(CLARITYSEEDPATH)/dist -v $(UINGPATH)/src:$(CLARITYSEEDPATH)/src -v $(UINGPATH)/src/app:$(CLARITYSEEDPATH)/src/app $(CLARITYIMAGE) $(SHELL) $(CLARITYBUILDSCRIPT)
@echo "Done." @echo "Done."
compile_normal: compile_clarity, compile_adminserver compile_ui compile_jobservice compile_normal: compile_clarity compile_adminserver compile_ui compile_jobservice
compile_golangimage: compile_clarity compile_golangimage: compile_clarity
@echo "compiling binary for adminserver (golang image)..." @echo "compiling binary for adminserver (golang image)..."
@ -264,18 +264,24 @@ package_online: modify_composefile
fi fi
@cp LICENSE $(HARBORPKG)/LICENSE @cp LICENSE $(HARBORPKG)/LICENSE
@cp NOTICE $(HARBORPKG)/NOTICE @cp NOTICE $(HARBORPKG)/NOTICE
@$(TARCMD) -zcvf harbor-online-installer-$(VERSIONTAG).tgz \
--exclude=$(HARBORPKG)/common/db --exclude=$(HARBORPKG)/common/config\ @if [ "$(NOTARYFLAG)" = "true" ] ; then \
--exclude=$(HARBORPKG)/photon --exclude=$(HARBORPKG)/kubernetes \ $(TARCMD) -zcvf harbor-online-installer-$(VERSIONTAG).tgz \
--exclude=$(HARBORPKG)/dev --exclude=$(DOCKERCOMPOSETPLFILENAME) \ $(HARBORPKG)/common/templates $(HARBORPKG)/prepare \
--exclude=$(HARBORPKG)/checkenv.sh \ $(HARBORPKG)/LICENSE $(HARBORPKG)/NOTICE \
--exclude=$(HARBORPKG)/jsminify.sh \ $(HARBORPKG)/install.sh $(HARBORPKG)/$(DOCKERCOMPOSEFILENAME) \
--exclude=$(HARBORPKG)/pushimage.sh \ $(HARBORPKG)/harbor.cfg $(HARBORPKG)/$(DOCKERCOMPOSENOTARYFILENAME); \
$(HARBORPKG) else \
$(TARCMD) -zcvf harbor-online-installer-$(VERSIONTAG).tgz \
$(HARBORPKG)/common/templates $(HARBORPKG)/prepare \
$(HARBORPKG)/LICENSE $(HARBORPKG)/NOTICE \
$(HARBORPKG)/install.sh $(HARBORPKG)/$(DOCKERCOMPOSEFILENAME) \
$(HARBORPKG)/harbor.cfg ; \
fi
@rm -rf $(HARBORPKG) @rm -rf $(HARBORPKG)
@echo "Done." @echo "Done."
package_offline: compile build modify_composefile package_offline: compile build modify_composefile
@echo "packing offline package ..." @echo "packing offline package ..."
@cp -r make $(HARBORPKG) @cp -r make $(HARBORPKG)
@ -313,6 +319,21 @@ package_offline: compile build modify_composefile
nginx:1.11.5 registry:2.5.1 photon:1.0 ; \ nginx:1.11.5 registry:2.5.1 photon:1.0 ; \
fi fi
@if [ "$(NOTARYFLAG)" = "true" ] ; then \
$(TARCMD) -zcvf harbor-offline-installer-$(VERSIONTAG).tgz \
$(HARBORPKG)/common/templates $(HARBORPKG)/$(DOCKERIMGFILE).$(VERSIONTAG).tgz \
$(HARBORPKG)/prepare $(HARBORPKG)/NOTICE \
$(HARBORPKG)/LICENSE $(HARBORPKG)/install.sh \
$(HARBORPKG)/harbor.cfg $(HARBORPKG)/$(DOCKERCOMPOSEFILENAME) ; \
else \
$(TARCMD) -zcvf harbor-offline-installer-$(VERSIONTAG).tgz \
$(HARBORPKG)/common/templates $(HARBORPKG)/$(DOCKERIMGFILE).$(VERSIONTAG).tgz \
$(HARBORPKG)/prepare $(HARBORPKG)/NOTICE \
$(HARBORPKG)/LICENSE $(HARBORPKG)/install.sh \
$(HARBORPKG)/harbor.cfg $(HARBORPKG)/$(DOCKERCOMPOSEFILENAME) \
$(HARBORPKG)/$(DOCKERCOMPOSENOTARYFILENAME) ; \
fi
@rm -rf $(HARBORPKG) @rm -rf $(HARBORPKG)
@echo "Done." @echo "Done."
@ -353,6 +374,11 @@ start:
@echo "Start complete. You can visit harbor now." @echo "Start complete. You can visit harbor now."
down: down:
@echo "Please make sure to set -e NOTARYFLAG=true if you are using Notary in Harbor, otherwise the Notary containers cannot be stop automaticlly."
@while [ -z "$$CONTINUE" ]; do \
read -r -p "Type anything but Y or y to exit. [Y/N]: " CONTINUE; \
done ; \
[ $$CONTINUE = "y" ] || [ $$CONTINUE = "Y" ] || (echo "Exiting."; exit 1;)
@echo "stoping harbor instance..." @echo "stoping harbor instance..."
@if [ "$(NOTARYFLAG)" = "true" ] ; then \ @if [ "$(NOTARYFLAG)" = "true" ] ; then \
$(DOCKERCOMPOSECMD) -f $(DOCKERCOMPOSEFILEPATH)/$(DOCKERCOMPOSEFILENAME) -f $(DOCKERCOMPOSEFILEPATH)/$(DOCKERCOMPOSENOTARYFILENAME) down ; \ $(DOCKERCOMPOSECMD) -f $(DOCKERCOMPOSEFILEPATH)/$(DOCKERCOMPOSEFILENAME) -f $(DOCKERCOMPOSEFILEPATH)/$(DOCKERCOMPOSENOTARYFILENAME) down ; \

View File

@ -827,15 +827,20 @@ paths:
format: int32 format: int32
required: false required: false
description: The number of the requested public repositories, default is 10 if not provided. description: The number of the requested public repositories, default is 10 if not provided.
- name: detail
in: query
type: boolean
required: false
description: Get detail info or not.
tags: tags:
- Products - Products
responses: responses:
200: 200:
description: Retrieved top repositories successfully. description: If detail is true, the response is described as the schema, or the response contains a TopRepo array.
schema: schema:
type: array type: array
items: items:
$ref: '#/definitions/TopRepo' $ref: '#/definitions/Repository'
400: 400:
description: Bad request because of invalid count. description: Bad request because of invalid count.
500: 500:
@ -1343,6 +1348,22 @@ paths:
description: User does not have permission of admin role. description: User does not have permission of admin role.
500: 500:
description: Unexpected internal errors. description: Unexpected internal errors.
/systeminfo:
get:
summary: Get general system info
description: |
This API is for retrieving general system info, this can be called by anonymous request.
tags:
- Products
responses:
200:
description: Get general info successfully.
schema:
type: object
items:
$ref: "#/definitions/GeneralInfo"
500:
description: Unexpected internal error.
/systeminfo/volumes: /systeminfo/volumes:
get: get:
summary: Get system volume info (total/free size). summary: Get system volume info (total/free size).
@ -1737,7 +1758,7 @@ definitions:
TopRepo: TopRepo:
type: object type: object
properties: properties:
repo_name: name:
type: string type: string
description: The name of the repo description: The name of the repo
count: count:
@ -1967,6 +1988,27 @@ definitions:
type: integer type: integer
format: int64 format: int64
description: Free volume size. description: Free volume size.
GeneralInfo:
type: object
properties:
with_notary:
type: boolean
description: If the Harbor instance is deployed with nested notary.
with_admiral:
type: boolean
description: If the Harbor instance is deployed with Admiral.
admiral_endpoint:
type: string
description: The url of the endpoint of admiral instance.
auth_mode:
type: string
description: The auth mode of current Harbor instance.
project_creation_restriction:
type: string
description: Indicate who can create projects, it could be 'adminonly' or 'everyone'.
self_registration:
type: boolean
description: Indicate whether the Harbor instance enable user to register himself.
SystemInfo: SystemInfo:
type: object type: object
properties: properties:

View File

@ -34,4 +34,6 @@ JOBSERVICE_SECRET=$jobservice_secret
TOKEN_EXPIRATION=$token_expiration TOKEN_EXPIRATION=$token_expiration
CFG_EXPIRATION=5 CFG_EXPIRATION=5
USE_COMPRESSED_JS=$use_compressed_js USE_COMPRESSED_JS=$use_compressed_js
GODEBUG=netdns=cgo GODEBUG=netdns=cgo
ADMIRAL_URL=$admiral_url
WITH_NOTARY=$with_notary

View File

@ -40,6 +40,9 @@ ssl_cert_key = /data/cert/server.key
#The path of secretkey storage #The path of secretkey storage
secretkey_path = /data secretkey_path = /data
#Admiral's url, comment this attribute, or set its value to to NA when Harbor is standalone
admiral_url = NA
#NOTES: The properties between BEGIN INITIAL PROPERTIES and END INITIAL PROPERTIES #NOTES: The properties between BEGIN INITIAL PROPERTIES and END INITIAL PROPERTIES
#only take effect in the first boot, the subsequent changes of these properties #only take effect in the first boot, the subsequent changes of these properties
#should be performed on web ui #should be performed on web ui

View File

@ -49,14 +49,20 @@ note() { printf "\n${underline}${bold}${blue}Note:${reset} ${blue}%s${reset}\n"
set -e set -e
set +o noglob set +o noglob
usage=$'Please set hostname and other necessary attributes in harbor.cfg first. DO NOT use localhost or 127.0.0.1 for hostname, because Harbor needs to be accessed by external clients.' usage=$'Please set hostname and other necessary attributes in harbor.cfg first. DO NOT use localhost or 127.0.0.1 for hostname, because Harbor needs to be accessed by external clients.
Please set --with-notary if needs enable Notary in Harbor, and set ui_url_protocol/ssl_cert/ssl_cert_key in harbor.cfg bacause notary must run under https.'
item=0 item=0
# notary is not enabled by default
with_notary=$false
while [ $# -gt 0 ]; do while [ $# -gt 0 ]; do
case $1 in case $1 in
--help) --help)
note "$usage" note "$usage"
exit 0;; exit 0;;
--with-notary)
with_notary=true;;
*) *)
note "$usage" note "$usage"
exit 1;; exit 1;;
@ -146,19 +152,38 @@ if [ -n "$host" ]
then then
sed "s/^hostname = .*/hostname = $host/g" -i ./harbor.cfg sed "s/^hostname = .*/hostname = $host/g" -i ./harbor.cfg
fi fi
./prepare if [ $with_notary ]
then
./prepare --with-notary
else
./prepare
fi
echo "" echo ""
h2 "[Step $item]: checking existing instance of Harbor ..."; let item+=1 h2 "[Step $item]: checking existing instance of Harbor ..."; let item+=1
if [ -n "$(docker-compose -f docker-compose*.yml ps -q)" ] if [ $with_notary ]
then then
note "stopping existing Harbor instance ..." if [ -n "$(docker-compose -f docker-compose.yml -f docker-compose.notary.yml ps -q)" ]
docker-compose -f docker-compose*.yml down then
note "stopping existing Harbor instance ..."
docker-compose -f docker-compose.yml -f docker-compose.notary.yml down
fi
else
if [ -n "$(docker-compose -f docker-compose.yml ps -q)" ]
then
note "stopping existing Harbor instance ..."
docker-compose -f docker-compose.yml down
fi
fi fi
echo "" echo ""
h2 "[Step $item]: starting Harbor ..." h2 "[Step $item]: starting Harbor ..."
docker-compose -f docker-compose*.yml up -d if [ $with_notary ]
then
docker-compose -f docker-compose.yml -f docker-compose.notary.yml up -d
else
docker-compose -f docker-compose.yml up -d
fi
protocol=http protocol=http
hostname=reg.mydomain.com hostname=reg.mydomain.com

View File

@ -147,6 +147,7 @@ token_expiration = rcp.get("configuration", "token_expiration")
verify_remote_cert = rcp.get("configuration", "verify_remote_cert") verify_remote_cert = rcp.get("configuration", "verify_remote_cert")
proj_cre_restriction = rcp.get("configuration", "project_creation_restriction") proj_cre_restriction = rcp.get("configuration", "project_creation_restriction")
secretkey_path = rcp.get("configuration", "secretkey_path") secretkey_path = rcp.get("configuration", "secretkey_path")
admiral_url = rcp.get("configuration", "admiral_url")
secret_key = get_secret_key(secretkey_path) secret_key = get_secret_key(secretkey_path)
######## ########
@ -190,10 +191,10 @@ else:
nginx_conf) nginx_conf)
render(os.path.join(templates_dir, "adminserver", "env"), render(os.path.join(templates_dir, "adminserver", "env"),
adminserver_conf_env, adminserver_conf_env,
ui_url=ui_url, ui_url=ui_url,
auth_mode=auth_mode, auth_mode=auth_mode,
self_registration=self_registration, self_registration=self_registration,
ldap_url=ldap_url, ldap_url=ldap_url,
ldap_searchdn =ldap_searchdn, ldap_searchdn =ldap_searchdn,
ldap_search_pwd =ldap_search_pwd, ldap_search_pwd =ldap_search_pwd,
@ -203,27 +204,29 @@ render(os.path.join(templates_dir, "adminserver", "env"),
ldap_scope=ldap_scope, ldap_scope=ldap_scope,
ldap_timeout=ldap_timeout, ldap_timeout=ldap_timeout,
db_password=db_password, db_password=db_password,
email_host=email_host, email_host=email_host,
email_port=email_port, email_port=email_port,
email_usr=email_usr, email_usr=email_usr,
email_pwd=email_pwd, email_pwd=email_pwd,
email_ssl=email_ssl, email_ssl=email_ssl,
email_from=email_from, email_from=email_from,
email_identity=email_identity, email_identity=email_identity,
harbor_admin_password=harbor_admin_password, harbor_admin_password=harbor_admin_password,
project_creation_restriction=proj_cre_restriction, project_creation_restriction=proj_cre_restriction,
verify_remote_cert=verify_remote_cert, verify_remote_cert=verify_remote_cert,
max_job_workers=max_job_workers, max_job_workers=max_job_workers,
ui_secret=ui_secret, ui_secret=ui_secret,
jobservice_secret=jobservice_secret, jobservice_secret=jobservice_secret,
token_expiration=token_expiration, token_expiration=token_expiration,
use_compressed_js=use_compressed_js admiral_url=admiral_url,
with_notary=args.notary_mode,
use_compressed_js=use_compressed_js
) )
render(os.path.join(templates_dir, "ui", "env"), render(os.path.join(templates_dir, "ui", "env"),
ui_conf_env, ui_conf_env,
ui_secret=ui_secret, ui_secret=ui_secret,
jobservice_secret=jobservice_secret,) jobservice_secret=jobservice_secret,)
render(os.path.join(templates_dir, "registry", render(os.path.join(templates_dir, "registry",
"config.yml"), "config.yml"),
@ -237,8 +240,8 @@ render(os.path.join(templates_dir, "db", "env"),
render(os.path.join(templates_dir, "jobservice", "env"), render(os.path.join(templates_dir, "jobservice", "env"),
job_conf_env, job_conf_env,
ui_secret=ui_secret, ui_secret=ui_secret,
jobservice_secret=jobservice_secret) jobservice_secret=jobservice_secret)
print("Generated configuration file: %s" % jobservice_conf) print("Generated configuration file: %s" % jobservice_conf)
shutil.copyfile(os.path.join(templates_dir, "jobservice", "app.conf"), jobservice_conf) shutil.copyfile(os.path.join(templates_dir, "jobservice", "app.conf"), jobservice_conf)
@ -328,5 +331,6 @@ if args.notary_mode:
default_alias = ''.join(random.choice(string.ascii_letters) for i in range(8)) default_alias = ''.join(random.choice(string.ascii_letters) for i in range(8))
render(os.path.join(notary_temp_dir, "signer_env"), os.path.join(notary_config_dir, "signer_env"), alias = default_alias) render(os.path.join(notary_temp_dir, "signer_env"), os.path.join(notary_config_dir, "signer_env"), alias = default_alias)
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

@ -114,6 +114,11 @@ var (
}, },
comcfg.ProjectCreationRestriction: "PROJECT_CREATION_RESTRICTION", comcfg.ProjectCreationRestriction: "PROJECT_CREATION_RESTRICTION",
comcfg.AdminInitialPassword: "HARBOR_ADMIN_PASSWORD", comcfg.AdminInitialPassword: "HARBOR_ADMIN_PASSWORD",
comcfg.AdmiralEndpoint: "ADMIRAL_URL",
comcfg.WithNotary: &parser{
env: "WITH_NOTARY",
parse: parseStringToBool,
},
} }
// configurations need read from environment variables // configurations need read from environment variables
@ -134,6 +139,11 @@ var (
env: "CFG_EXPIRATION", env: "CFG_EXPIRATION",
parse: parseStringToInt, parse: parseStringToInt,
}, },
comcfg.AdmiralEndpoint: "ADMIRAL_URL",
comcfg.WithNotary: &parser{
env: "WITH_NOTARY",
parse: parseStringToBool,
},
} }
) )

View File

@ -75,6 +75,8 @@ const (
JobLogDir = "job_log_dir" JobLogDir = "job_log_dir"
UseCompressedJS = "use_compressed_js" UseCompressedJS = "use_compressed_js"
AdminInitialPassword = "admin_initial_password" AdminInitialPassword = "admin_initial_password"
AdmiralEndpoint = "admiral_url"
WithNotary = "with_notary"
) )
// Manager manages configurations // Manager manages configurations

View File

@ -103,10 +103,12 @@ func GetRepositoryByProjectName(name string) ([]*models.RepoRecord, error) {
} }
//GetTopRepos returns the most popular repositories //GetTopRepos returns the most popular repositories
func GetTopRepos(userID int, count int) ([]models.TopRepo, error) { func GetTopRepos(userID int, count int) ([]*models.RepoRecord, error) {
topRepos := []models.TopRepo{} sql :=
`select r.repository_id, r.name, r.owner_id,
sql := `select r.name, r.pull_count from repository r r.project_id, r.description, r.pull_count,
r.star_count, r.creation_time, r.update_time
from repository r
inner join project p on r.project_id = p.project_id inner join project p on r.project_id = p.project_id
where ( where (
p.deleted = 0 and ( p.deleted = 0 and (
@ -122,18 +124,8 @@ func GetTopRepos(userID int, count int) ([]models.TopRepo, error) {
order by r.pull_count desc, r.name limit ?` order by r.pull_count desc, r.name limit ?`
repositories := []*models.RepoRecord{} repositories := []*models.RepoRecord{}
_, err := GetOrmer().Raw(sql, userID, NonExistUserID, userID, userID, count).QueryRows(&repositories) _, err := GetOrmer().Raw(sql, userID, NonExistUserID, userID, userID, count).QueryRows(&repositories)
if err != nil {
return topRepos, err
}
for _, repository := range repositories { return repositories, err
topRepos = append(topRepos, models.TopRepo{
RepoName: repository.Name,
AccessCount: repository.PullCount,
})
}
return topRepos, nil
} }
// GetTotalOfRepositories ... // GetTotalOfRepositories ...

View File

@ -269,75 +269,28 @@ func TestGetTopRepos(t *testing.T) {
err = DeleteProject(deletedPublicProject.ProjectID) err = DeleteProject(deletedPublicProject.ProjectID)
require.NoError(err) require.NoError(err)
var topRepos []models.TopRepo var topRepos []*models.RepoRecord
// anonymous should retrieve public non-deleted repositories // anonymous should retrieve public non-deleted repositories
topRepos, err = GetTopRepos(NonExistUserID, 100) topRepos, err = GetTopRepos(NonExistUserID, 100)
require.NoError(err) require.NoError(err)
require.Len(topRepos, 1) require.Len(topRepos, 1)
require.Equal(topRepos, []models.TopRepo{ require.Equal(topRepos[0].Name, repository.Name)
models.TopRepo{
RepoName: repository.Name,
AccessCount: repository.PullCount,
},
})
// admin should retrieve all repositories // admin should retrieve all repositories
topRepos, err = GetTopRepos(admin.UserID, 100) topRepos, err = GetTopRepos(admin.UserID, 100)
require.NoError(err) require.NoError(err)
require.Len(topRepos, 4) require.Len(topRepos, 4)
require.Equal(topRepos, []models.TopRepo{
models.TopRepo{
RepoName: repository3.Name,
AccessCount: repository3.PullCount,
},
models.TopRepo{
RepoName: repository2.Name,
AccessCount: repository2.PullCount,
},
models.TopRepo{
RepoName: repository1.Name,
AccessCount: repository1.PullCount,
},
models.TopRepo{
RepoName: repository.Name,
AccessCount: repository.PullCount,
},
})
// user should retrieve visible repositories // user should retrieve visible repositories
topRepos, err = GetTopRepos(user.UserID, 100) topRepos, err = GetTopRepos(user.UserID, 100)
require.NoError(err) require.NoError(err)
require.Len(topRepos, 2) require.Len(topRepos, 2)
require.Equal(topRepos, []models.TopRepo{
models.TopRepo{
RepoName: repository3.Name,
AccessCount: repository3.PullCount,
},
models.TopRepo{
RepoName: repository.Name,
AccessCount: repository.PullCount,
},
})
// limit by count // limit by count
topRepos, err = GetTopRepos(admin.UserID, 3) topRepos, err = GetTopRepos(admin.UserID, 3)
require.NoError(err) require.NoError(err)
require.Len(topRepos, 3) require.Len(topRepos, 3)
require.Equal(topRepos, []models.TopRepo{
models.TopRepo{
RepoName: repository3.Name,
AccessCount: repository3.PullCount,
},
models.TopRepo{
RepoName: repository2.Name,
AccessCount: repository2.PullCount,
},
models.TopRepo{
RepoName: repository1.Name,
AccessCount: repository1.PullCount,
},
})
} }
func TestGetTotalOfRepositoriesByProject(t *testing.T) { func TestGetTotalOfRepositoriesByProject(t *testing.T) {

View File

@ -28,7 +28,6 @@ import (
) )
var ( var (
notaryEndpoint = "http://notary-server:4443"
notaryCachePath = "/root/notary" notaryCachePath = "/root/notary"
trustPin trustpinning.TrustPinConfig trustPin trustpinning.TrustPinConfig
mockRetriever notary.PassRetriever mockRetriever notary.PassRetriever
@ -55,7 +54,7 @@ func init() {
// GetTargets is a help function called by API to fetch signature information of a given repository. // GetTargets is a help function called by API to fetch signature information of a given repository.
// Per docker's convention the repository should contain the information of endpoint, i.e. it should look // Per docker's convention the repository should contain the information of endpoint, i.e. it should look
// like "10.117.4.117/library/ubuntu", instead of "library/ubuntu" (fqRepo for fully-qualified repo) // like "10.117.4.117/library/ubuntu", instead of "library/ubuntu" (fqRepo for fully-qualified repo)
func GetTargets(username string, fqRepo string) ([]Target, error) { func GetTargets(notaryEndpoint string, username string, fqRepo string) ([]Target, error) {
res := []Target{} res := []Target{}
authorizer := auth.NewNotaryUsernameTokenAuthorizer(username, "repository", fqRepo, "pull") authorizer := auth.NewNotaryUsernameTokenAuthorizer(username, "repository", fqRepo, "pull")
store, err := auth.NewAuthorizerStore(strings.Split(notaryEndpoint, "//")[1], true, authorizer) store, err := auth.NewAuthorizerStore(strings.Split(notaryEndpoint, "//")[1], true, authorizer)

View File

@ -5,17 +5,18 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
notarytest "github.com/vmware/harbor/src/common/utils/notary/test" notarytest "github.com/vmware/harbor/src/common/utils/notary/test"
"net/http/httptest"
"os" "os"
"path" "path"
"testing" "testing"
) )
var endpoint = "10.117.4.142" var endpoint = "10.117.4.142"
var notaryServer *httptest.Server
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
notaryServer := notarytest.NewNotaryServer(endpoint) notaryServer = notarytest.NewNotaryServer(endpoint)
defer notaryServer.Close() defer notaryServer.Close()
notaryEndpoint = notaryServer.URL
notaryCachePath = "/tmp/notary" notaryCachePath = "/tmp/notary"
result := m.Run() result := m.Run()
if result != 0 { if result != 0 {
@ -24,12 +25,12 @@ func TestMain(m *testing.M) {
} }
func TestGetTargets(t *testing.T) { func TestGetTargets(t *testing.T) {
targets, err := GetTargets("admin", path.Join(endpoint, "notary-demo/busybox")) targets, err := GetTargets(notaryServer.URL, "admin", path.Join(endpoint, "notary-demo/busybox"))
assert.Nil(t, err, fmt.Sprintf("Unexpected error: %v", err)) assert.Nil(t, err, fmt.Sprintf("Unexpected error: %v", err))
assert.Equal(t, 1, len(targets), "") assert.Equal(t, 1, len(targets), "")
assert.Equal(t, "1.0", targets[0].Tag, "") assert.Equal(t, "1.0", targets[0].Tag, "")
targets, err = GetTargets("admin", path.Join(endpoint, "notary-demo/notexist")) targets, err = GetTargets(notaryServer.URL, "admin", path.Join(endpoint, "notary-demo/notexist"))
assert.Nil(t, err, fmt.Sprintf("Unexpected error: %v", err)) assert.Nil(t, err, fmt.Sprintf("Unexpected error: %v", err))
assert.Equal(t, 0, len(targets), "Targets list should be empty for non exist repo.") assert.Equal(t, 0, len(targets), "Targets list should be empty for non exist repo.")
} }

View File

@ -58,6 +58,8 @@ var adminServerDefaultConfig = map[string]interface{}{
config.CfgExpiration: 5, config.CfgExpiration: 5,
config.UseCompressedJS: true, config.UseCompressedJS: true,
config.AdminInitialPassword: "password", config.AdminInitialPassword: "password",
config.AdmiralEndpoint: "http://www.vmware.com",
config.WithNotary: false,
} }
// NewAdminserver returns a mock admin server // NewAdminserver returns a mock admin server

View File

@ -97,6 +97,7 @@ func init() {
beego.Router("/api/policies/replication", &RepPolicyAPI{}, "get:List") beego.Router("/api/policies/replication", &RepPolicyAPI{}, "get:List")
beego.Router("/api/policies/replication", &RepPolicyAPI{}, "post:Post;delete:Delete") beego.Router("/api/policies/replication", &RepPolicyAPI{}, "post:Post;delete:Delete")
beego.Router("/api/policies/replication/:id([0-9]+)/enablement", &RepPolicyAPI{}, "put:UpdateEnablement") beego.Router("/api/policies/replication/:id([0-9]+)/enablement", &RepPolicyAPI{}, "put:UpdateEnablement")
beego.Router("/api/systeminfo", &SystemInfoAPI{}, "get:GetGeneralInfo")
beego.Router("/api/systeminfo/volumes", &SystemInfoAPI{}, "get:GetVolumeInfo") beego.Router("/api/systeminfo/volumes", &SystemInfoAPI{}, "get:GetVolumeInfo")
beego.Router("/api/systeminfo/getcert", &SystemInfoAPI{}, "get:GetCert") beego.Router("/api/systeminfo/getcert", &SystemInfoAPI{}, "get:GetCert")
beego.Router("/api/ldap/ping", &LdapAPI{}, "post:Ping") beego.Router("/api/ldap/ping", &LdapAPI{}, "post:Ping")
@ -558,7 +559,8 @@ func (a testapi) GetReposManifests(authInfo usrInfo, repoName string, tag string
} }
//Get public repositories which are accessed most //Get public repositories which are accessed most
func (a testapi) GetReposTop(authInfo usrInfo, count string) (int, error) { func (a testapi) GetReposTop(authInfo usrInfo, count,
detail string) (int, interface{}, error) {
_sling := sling.New().Get(a.basePath) _sling := sling.New().Get(a.basePath)
path := "/api/repositories/top" path := "/api/repositories/top"
@ -566,12 +568,36 @@ func (a testapi) GetReposTop(authInfo usrInfo, count string) (int, error) {
_sling = _sling.Path(path) _sling = _sling.Path(path)
type QueryParams struct { type QueryParams struct {
Count string `url:"count"` Count string `url:"count"`
Detail string `url:"detail"`
} }
_sling = _sling.QueryStruct(&QueryParams{Count: count}) _sling = _sling.QueryStruct(&QueryParams{
httpStatusCode, _, err := request(_sling, jsonAcceptHeader, authInfo) Count: count,
return httpStatusCode, err Detail: detail,
})
code, body, err := request(_sling, jsonAcceptHeader, authInfo)
if err != nil {
return 0, nil, err
}
if code != http.StatusOK {
return code, body, err
}
if detail == "true" || detail == "1" {
result := []*repoResp{}
if err = json.Unmarshal(body, &result); err != nil {
return 0, nil, err
}
return http.StatusOK, result, nil
}
result := []*models.TopRepo{}
if err = json.Unmarshal(body, &result); err != nil {
return 0, nil, err
}
return http.StatusOK, result, nil
} }
//-------------------------Targets Test---------------------------------------// //-------------------------Targets Test---------------------------------------//
@ -953,6 +979,11 @@ func (a testapi) VolumeInfoGet(authInfo usrInfo) (int, apilib.SystemInfo, error)
return httpStatusCode, successPayLoad, err return httpStatusCode, successPayLoad, err
} }
func (a testapi) GetGeneralInfo() (int, []byte, error) {
_sling := sling.New().Get(a.basePath).Path("/api/systeminfo")
return request(_sling, jsonAcceptHeader)
}
//Get system cert //Get system cert
func (a testapi) CertGet(authInfo usrInfo) (int, []byte, error) { func (a testapi) CertGet(authInfo usrInfo) (int, []byte, error) {
_sling := sling.New().Get(a.basePath) _sling := sling.New().Get(a.basePath)

View File

@ -139,6 +139,10 @@ func getRepositories(projectID int64, keyword string,
return result, nil return result, nil
} }
return populateTagsCount(repositories)
}
func populateTagsCount(repositories []*models.RepoRecord) ([]*repoResp, error) {
result := []*repoResp{} result := []*repoResp{}
for _, repository := range repositories { for _, repository := range repositories {
repo := &repoResp{ repo := &repoResp{
@ -160,7 +164,6 @@ func getRepositories(projectID int64, keyword string,
repo.TagsCount = int64(len(tags)) repo.TagsCount = int64(len(tags))
result = append(result, repo) result = append(result, repo)
} }
return result, nil return result, nil
} }
@ -531,7 +534,30 @@ func (ra *RepositoryAPI) GetTopRepos() {
log.Errorf("failed to get top repos: %v", err) log.Errorf("failed to get top repos: %v", err)
ra.CustomAbort(http.StatusInternalServerError, "internal server error") ra.CustomAbort(http.StatusInternalServerError, "internal server error")
} }
ra.Data["json"] = repos
detail := ra.GetString("detail") == "1" || ra.GetString("detail") == "true"
if !detail {
result := []*models.TopRepo{}
for _, repo := range repos {
result = append(result, &models.TopRepo{
RepoName: repo.Name,
AccessCount: repo.PullCount,
})
}
ra.Data["json"] = result
ra.ServeJSON()
return
}
result, err := populateTagsCount(repos)
if err != nil {
log.Errorf("failed to popultate tags count to repositories: %v", err)
ra.CustomAbort(http.StatusInternalServerError, "internal server error")
}
ra.Data["json"] = result
ra.ServeJSON() ra.ServeJSON()
} }
@ -554,7 +580,7 @@ func (ra *RepositoryAPI) GetSignatures() {
if err != nil { if err != nil {
log.Warningf("Error when getting username: %v", err) log.Warningf("Error when getting username: %v", err)
} }
targets, err := notary.GetTargets(username, fqRepo) targets, err := notary.GetTargets(config.InternalNotaryEndpoint(), username, fqRepo)
if err != nil { if err != nil {
log.Errorf("Error while fetching signature from notary: %v", err) log.Errorf("Error while fetching signature from notary: %v", err)
ra.CustomAbort(http.StatusInternalServerError, "internal error") ra.CustomAbort(http.StatusInternalServerError, "internal error")

View File

@ -5,6 +5,7 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/vmware/harbor/src/common/models"
) )
func TestGetRepos(t *testing.T) { func TestGetRepos(t *testing.T) {
@ -193,9 +194,6 @@ func TestGetReposManifests(t *testing.T) {
} }
func TestGetReposTop(t *testing.T) { func TestGetReposTop(t *testing.T) {
var httpStatusCode int
var err error
var count string
assert := assert.New(t) assert := assert.New(t)
apiTest := newHarborAPI() apiTest := newHarborAPI()
@ -203,24 +201,46 @@ func TestGetReposTop(t *testing.T) {
fmt.Println("Testing ReposTop Get API") fmt.Println("Testing ReposTop Get API")
//-------------------case 1 : response code = 200------------------------// //-------------------case 1 : response code = 200------------------------//
fmt.Println("case 1 : response code = 200") fmt.Println("case 1 : response code = 200")
count = "1" count := "1"
httpStatusCode, err = apiTest.GetReposTop(*admin, count) detail := "false"
code, repos, err := apiTest.GetReposTop(*admin, count, detail)
if err != nil { if err != nil {
t.Error("Error whihle get reposTop to show the most popular public repositories ", err.Error()) t.Errorf("failed to get the most popular repositories: %v", err)
t.Log(err)
} else { } else {
assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200") assert.Equal(int(200), code, "response code should be 200")
if r, ok := repos.([]*models.TopRepo); ok {
assert.Equal(int(1), len(r), "the length should be 1")
assert.Equal(r[0].RepoName, "library/docker", "the name of repository should be library/docker")
} else {
t.Error("the repositories should in simple style as the detail is false")
}
} }
//-------------------case 2 : response code = 400------------------------// //-------------------case 2 : response code = 400------------------------//
fmt.Println("case 2 : response code = 400,invalid count") fmt.Println("case 2 : response code = 400,invalid count")
count = "cc" count = "cc"
httpStatusCode, err = apiTest.GetReposTop(*admin, count) code, _, err = apiTest.GetReposTop(*admin, count, detail)
if err != nil { if err != nil {
t.Error("Error whihle get reposTop to show the most popular public repositories ", err.Error()) t.Errorf("failed to get the most popular repositories: %v", err)
t.Log(err)
} else { } else {
assert.Equal(int(400), httpStatusCode, "httpStatusCode should be 400") assert.Equal(int(400), code, "response code should be 400")
}
//-------------------case 3 : response code = 200------------------------//
fmt.Println("case 3 : response code = 200")
count = "1"
detail = "true"
code, repos, err = apiTest.GetReposTop(*admin, count, detail)
if err != nil {
t.Errorf("failed to get the most popular repositories: %v", err)
} else {
assert.Equal(int(200), code, "response code should be 200")
if r, ok := repos.([]*repoResp); ok {
assert.Equal(int(1), len(r), "the length should be 1")
assert.Equal(r[0].Name, "library/docker", "the name of repository should be library/docker")
} else {
t.Error("the repositories should in detail style as the detail is true")
}
} }
fmt.Printf("\n") fmt.Printf("\n")

View File

@ -4,11 +4,14 @@ import (
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"syscall" "syscall"
"github.com/vmware/harbor/src/common/api" "github.com/vmware/harbor/src/common/api"
comcfg "github.com/vmware/harbor/src/common/config"
"github.com/vmware/harbor/src/common/dao" "github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/common/utils/log" "github.com/vmware/harbor/src/common/utils/log"
"github.com/vmware/harbor/src/ui/config"
) )
//SystemInfoAPI handle requests for getting system info /api/systeminfo //SystemInfoAPI handle requests for getting system info /api/systeminfo
@ -32,8 +35,19 @@ type Storage struct {
Free uint64 `json:"free"` Free uint64 `json:"free"`
} }
// Prepare for validating user if an admin. //GeneralInfo wraps common systeminfo for anonymous request
func (sia *SystemInfoAPI) Prepare() { type GeneralInfo struct {
WithNotary bool `json:"with_notary"`
WithAdmiral bool `json:"with_admiral"`
AdmiralEndpoint string `json:"admiral_endpoint"`
AuthMode string `json:"auth_mode"`
RegistryURL string `json:"registry_url"`
ProjectCreationRestrict string `json:"project_creation_restriction"`
SelfRegistration bool `json:"self_registration"`
}
// validate for validating user if an admin.
func (sia *SystemInfoAPI) validate() {
sia.currentUserID = sia.ValidateUser() sia.currentUserID = sia.ValidateUser()
var err error var err error
@ -46,6 +60,7 @@ func (sia *SystemInfoAPI) Prepare() {
// GetVolumeInfo gets specific volume storage info. // GetVolumeInfo gets specific volume storage info.
func (sia *SystemInfoAPI) GetVolumeInfo() { func (sia *SystemInfoAPI) GetVolumeInfo() {
sia.validate()
if !sia.isAdmin { if !sia.isAdmin {
sia.RenderError(http.StatusForbidden, "User does not have admin role.") sia.RenderError(http.StatusForbidden, "User does not have admin role.")
return return
@ -71,6 +86,7 @@ func (sia *SystemInfoAPI) GetVolumeInfo() {
//GetCert gets default self-signed certificate. //GetCert gets default self-signed certificate.
func (sia *SystemInfoAPI) GetCert() { func (sia *SystemInfoAPI) GetCert() {
sia.validate()
if sia.isAdmin { if sia.isAdmin {
if _, err := os.Stat(defaultRootCert); !os.IsNotExist(err) { if _, err := os.Stat(defaultRootCert); !os.IsNotExist(err) {
sia.Ctx.Output.Header("Content-Type", "application/octet-stream") sia.Ctx.Output.Header("Content-Type", "application/octet-stream")
@ -83,3 +99,29 @@ func (sia *SystemInfoAPI) GetCert() {
} }
sia.CustomAbort(http.StatusForbidden, "") sia.CustomAbort(http.StatusForbidden, "")
} }
// GetGeneralInfo returns the general system info, which is to be called by anonymous user
func (sia *SystemInfoAPI) GetGeneralInfo() {
cfg, err := config.GetSystemCfg()
if err != nil {
log.Errorf("Error occured getting config: %v", err)
sia.CustomAbort(http.StatusInternalServerError, "Unexpected error")
}
var registryURL string
if l := strings.Split(cfg[comcfg.ExtEndpoint].(string), "://"); len(l) > 1 {
registryURL = l[1]
} else {
registryURL = l[0]
}
info := GeneralInfo{
AdmiralEndpoint: cfg[comcfg.AdmiralEndpoint].(string),
WithAdmiral: config.WithAdmiral(),
WithNotary: config.WithNotary(),
AuthMode: cfg[comcfg.AUTHMode].(string),
ProjectCreationRestrict: cfg[comcfg.ProjectCreationRestriction].(string),
SelfRegistration: cfg[comcfg.SelfRegistration].(bool),
RegistryURL: registryURL,
}
sia.Data["json"] = info
sia.ServeJSON()
}

View File

@ -1,6 +1,7 @@
package api package api
import ( import (
"encoding/json"
"fmt" "fmt"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"testing" "testing"
@ -37,6 +38,18 @@ func TestGetVolumeInfo(t *testing.T) {
} }
func TestGetGeneralInfo(t *testing.T) {
apiTest := newHarborAPI()
code, body, err := apiTest.GetGeneralInfo()
assert := assert.New(t)
assert.Nil(err, fmt.Sprintf("Unexpected Error: %v", err))
assert.Equal(200, code, fmt.Sprintf("Unexpected status code: %d", code))
g := &GeneralInfo{}
err = json.Unmarshal(body, g)
assert.Nil(err, fmt.Sprintf("Unexpected Error: %v", err))
assert.Equal(false, g.WithNotary, "with notary should be false")
}
func TestGetCert(t *testing.T) { func TestGetCert(t *testing.T) {
fmt.Println("Testing Get Cert") fmt.Println("Testing Get Cert")
assert := assert.New(t) assert := assert.New(t)

View File

@ -18,6 +18,7 @@ package config
import ( import (
"encoding/json" "encoding/json"
"os" "os"
"strings"
comcfg "github.com/vmware/harbor/src/common/config" comcfg "github.com/vmware/harbor/src/common/config"
"github.com/vmware/harbor/src/common/models" "github.com/vmware/harbor/src/common/models"
@ -132,7 +133,7 @@ func TokenExpiration() (int, error) {
return int(cfg[comcfg.TokenExpiration].(float64)), nil return int(cfg[comcfg.TokenExpiration].(float64)), nil
} }
// ExtEndpoint returns the external URL of Harbor: protocal://host:port // ExtEndpoint returns the external URL of Harbor: protocol://host:port
func ExtEndpoint() (string, error) { func ExtEndpoint() (string, error) {
cfg, err := mg.Get() cfg, err := mg.Get()
if err != nil { if err != nil {
@ -141,6 +142,19 @@ func ExtEndpoint() (string, error) {
return cfg[comcfg.ExtEndpoint].(string), nil return cfg[comcfg.ExtEndpoint].(string), nil
} }
// ExtURL returns the external URL: host:port
func ExtURL() (string, error) {
endpoint, err := ExtEndpoint()
if err != nil {
return "", err
}
l := strings.Split(endpoint, "://")
if len(l) > 0 {
return l[1], nil
}
return endpoint, nil
}
// SecretKey returns the secret key to encrypt the password of target // SecretKey returns the secret key to encrypt the password of target
func SecretKey() (string, error) { func SecretKey() (string, error) {
return keyProvider.Get(nil) return keyProvider.Get(nil)
@ -174,6 +188,12 @@ func InternalTokenServiceEndpoint() string {
return "http://ui/service/token" return "http://ui/service/token"
} }
// InternalNotaryEndpoint returns notary server endpoint for internal communication between Harbor containers
// This is currently a conventional value and can be unaccessible when Harbor is not deployed with Notary.
func InternalNotaryEndpoint() string {
return "http://notary-server:4443"
}
// InitialAdminPassword returns the initial password for administrator // InitialAdminPassword returns the initial password for administrator
func InitialAdminPassword() (string, error) { func InitialAdminPassword() (string, error) {
cfg, err := mg.Get() cfg, err := mg.Get()
@ -253,3 +273,32 @@ func UISecret() string {
func JobserviceSecret() string { func JobserviceSecret() string {
return os.Getenv("JOBSERVICE_SECRET") return os.Getenv("JOBSERVICE_SECRET")
} }
// WithNotary returns a bool value to indicate if Harbor's deployed with Notary
func WithNotary() bool {
cfg, err := mg.Get()
if err != nil {
log.Errorf("Failed to get configuration, will return WithNotary == false")
return false
}
return cfg[comcfg.WithNotary].(bool)
}
// AdmiralEndpoint returns the URL of admiral, if Harbor is not deployed with admiral it should return an empty string.
func AdmiralEndpoint() string {
cfg, err := mg.Get()
if err != nil {
log.Errorf("Failed to get configuration, will return empty string as admiral's endpoint")
return ""
}
if e, ok := cfg[comcfg.AdmiralEndpoint].(string); !ok || e == "NA" {
cfg[comcfg.AdmiralEndpoint] = ""
}
return cfg[comcfg.AdmiralEndpoint].(string)
}
// WithAdmiral returns a bool to indicate if Harbor's deployed with admiral.
func WithAdmiral() bool {
return len(AdmiralEndpoint()) > 0
}

View File

@ -120,4 +120,24 @@ func TestConfig(t *testing.T) {
if _, err := Database(); err != nil { if _, err := Database(); err != nil {
t.Fatalf("failed to get database: %v", err) t.Fatalf("failed to get database: %v", err)
} }
if InternalNotaryEndpoint() != "http://notary-server:4443" {
t.Errorf("Unexpected notary endpoint: %s", InternalNotaryEndpoint())
}
if WithNotary() {
t.Errorf("Withnotary should be false")
}
if !WithAdmiral() {
t.Errorf("WithAdmiral should be true")
}
if AdmiralEndpoint() != "http://www.vmware.com" {
t.Errorf("Unexpected admiral endpoint: %s", AdmiralEndpoint())
}
extURL, err := ExtURL()
if err != nil {
t.Errorf("Unexpected error getting external URL: %v", err)
}
if extURL != "host01.com" {
t.Errorf(`extURL should be "host01.com".`)
}
} }

View File

@ -92,6 +92,7 @@ func initRouters() {
beego.Router("/api/logs", &api.LogAPI{}) beego.Router("/api/logs", &api.LogAPI{})
beego.Router("/api/configurations", &api.ConfigAPI{}) beego.Router("/api/configurations", &api.ConfigAPI{})
beego.Router("/api/systeminfo", &api.SystemInfoAPI{}, "get:GetGeneralInfo")
beego.Router("/api/systeminfo/volumes", &api.SystemInfoAPI{}, "get:GetVolumeInfo") beego.Router("/api/systeminfo/volumes", &api.SystemInfoAPI{}, "get:GetVolumeInfo")
beego.Router("/api/systeminfo/getcert", &api.SystemInfoAPI{}, "get:GetCert") beego.Router("/api/systeminfo/getcert", &api.SystemInfoAPI{}, "get:GetCert")
beego.Router("/api/ldap/ping", &api.LdapAPI{}, "post:Ping") beego.Router("/api/ldap/ping", &api.LdapAPI{}, "post:Ping")

View File

@ -43,14 +43,14 @@ func InitCreators() {
}, },
"registry": &registryFilter{}, "registry": &registryFilter{},
} }
ext, err := config.ExtEndpoint() ext, err := config.ExtURL()
if err != nil { if err != nil {
log.Warningf("Failed to get ext enpoint, err: %v, the token service will not be functional with notary requests", err) log.Warningf("Failed to get ext url, err: %v, the token service will not be functional with notary requests", err)
} else { } else {
notaryFilterMap = map[string]accessFilter{ notaryFilterMap = map[string]accessFilter{
"repository": &repositoryFilter{ "repository": &repositoryFilter{
parser: &endpointParser{ parser: &endpointParser{
endpoint: strings.Split(ext, "//")[1], endpoint: ext,
}, },
}, },
} }