mirror of
https://github.com/goharbor/harbor
synced 2025-04-18 18:09:48 +00:00
Merge branch 'master' of github.com:steven-zou/harbor
This commit is contained in:
commit
a477cb1a3e
27
Makefile
27
Makefile
|
@ -12,10 +12,12 @@
|
|||
# golang:1.7.3
|
||||
# compile_adminserver, compile_ui, compile_jobservice: compile specific binary
|
||||
#
|
||||
# build: build Harbor docker images (defuault: build_photon)
|
||||
# build: build Harbor docker images (default: build_photon)
|
||||
# for example: make build -e BASEIMAGE=photon
|
||||
# build_photon: build Harbor docker images from photon baseimage
|
||||
#
|
||||
# build_postgresql: build postgresql images basaed on photon os
|
||||
# make build -e BASEIMAGE=postgresql
|
||||
# install: include compile binarys, build images, prepare specific \
|
||||
# version composefile and startup Harbor instance
|
||||
#
|
||||
|
@ -80,7 +82,7 @@ REGISTRYPROJECTNAME=vmware
|
|||
DEVFLAG=true
|
||||
NOTARYFLAG=false
|
||||
REGISTRYVERSION=2.6.1-photon
|
||||
NGINXVERSION=1.11.5-patched
|
||||
NGINXVERSION=1.11.13
|
||||
PHOTONVERSION=1.0
|
||||
NOTARYVERSION=server-0.5.0
|
||||
NOTARYSIGNERVERSION=signer-0.5.0
|
||||
|
@ -92,7 +94,7 @@ NEWCLARITYVERSION=
|
|||
#clair parameters
|
||||
CLAIRVERSION=v2.0.0
|
||||
CLAIRFLAG=false
|
||||
CLAIRDBVERSION=9.6.3
|
||||
CLAIRDBVERSION=9.6.3-photon
|
||||
|
||||
#clarity parameters
|
||||
CLARITYIMAGE=vmware/harbor-clarity-ui-builder[:tag]
|
||||
|
@ -166,6 +168,10 @@ DOCKERFILEPATH_DB=$(DOCKERFILEPATH_COMMON)/db
|
|||
DOCKERFILENAME_DB=Dockerfile
|
||||
DOCKERFILE_CLARITY=$(MAKEPATH)/dev/nodeclarity/Dockerfile
|
||||
|
||||
DOCKERFILEPATH_POSTGRESQL=$(DOCKERFILEPATH_COMMON)/postgresql
|
||||
DOCKERFILENAME_POSTGRESQL=Dockerfile
|
||||
|
||||
|
||||
# docker image name
|
||||
DOCKERIMAGENAME_ADMINSERVER=vmware/harbor-adminserver
|
||||
DOCKERIMAGENAME_UI=vmware/harbor-ui
|
||||
|
@ -173,7 +179,7 @@ DOCKERIMAGENAME_JOBSERVICE=vmware/harbor-jobservice
|
|||
DOCKERIMAGENAME_LOG=vmware/harbor-log
|
||||
DOCKERIMAGENAME_DB=vmware/harbor-db
|
||||
DOCKERIMAGENAME_CLATIRY=vmware/harbor-clarity-ui-builder
|
||||
|
||||
DOCKERIMAGENAME_POSTGRESQL=vmware/postgresql
|
||||
# docker-compose files
|
||||
DOCKERCOMPOSEFILEPATH=$(MAKEPATH)
|
||||
DOCKERCOMPOSETPLFILENAME=docker-compose.tpl
|
||||
|
@ -213,7 +219,7 @@ DOCKERSAVE_PARA=$(DOCKERIMAGENAME_ADMINSERVER):$(VERSIONTAG) \
|
|||
$(DOCKERIMAGENAME_LOG):$(VERSIONTAG) \
|
||||
$(DOCKERIMAGENAME_DB):$(VERSIONTAG) \
|
||||
$(DOCKERIMAGENAME_JOBSERVICE):$(VERSIONTAG) \
|
||||
vmware/nginx:$(NGINXVERSION) vmware/registry:$(REGISTRYVERSION) \
|
||||
vmware/nginx-photon:$(NGINXVERSION) vmware/registry:$(REGISTRYVERSION) \
|
||||
photon:$(PHOTONVERSION)
|
||||
PACKAGE_OFFLINE_PARA=-zcvf harbor-offline-installer-$(GITTAGVERSION).tgz \
|
||||
$(HARBORPKG)/common/templates $(HARBORPKG)/$(DOCKERIMGFILE).$(VERSIONTAG).tar.gz \
|
||||
|
@ -237,7 +243,7 @@ ifeq ($(NOTARYFLAG), true)
|
|||
DOCKERCOMPOSE_LIST+= -f $(DOCKERCOMPOSEFILEPATH)/$(DOCKERCOMPOSENOTARYFILENAME)
|
||||
endif
|
||||
ifeq ($(CLAIRFLAG), true)
|
||||
DOCKERSAVE_PARA+= quay.io/coreos/clair:$(CLAIRVERSION) postgres:$(CLAIRDBVERSION)
|
||||
DOCKERSAVE_PARA+= quay.io/coreos/clair:$(CLAIRVERSION) vmware/postgresql:$(CLAIRDBVERSION)
|
||||
PACKAGE_OFFLINE_PARA+= $(HARBORPKG)/$(DOCKERCOMPOSECLAIRFILENAME)
|
||||
PACKAGE_ONLINE_PARA+= $(HARBORPKG)/$(DOCKERCOMPOSECLAIRFILENAME)
|
||||
DOCKERCOMPOSE_LIST+= -f $(DOCKERCOMPOSEFILEPATH)/$(DOCKERCOMPOSECLAIRFILENAME)
|
||||
|
@ -305,7 +311,10 @@ build_common: version
|
|||
|
||||
build_photon: build_common
|
||||
make -f $(MAKEFILEPATH_PHOTON)/Makefile build -e DEVFLAG=$(DEVFLAG)
|
||||
|
||||
build_postgresql:
|
||||
@echo "buildging postgresql container for photon..."
|
||||
@cd $(DOCKERFILEPATH_POSTGRESQL) && $(DOCKERBUILD) -f $(DOCKERFILENAME_POSTGRESQL) -t $(DOCKERIMAGENAME_POSTGRESQL):$(CLAIRDBVERSION) .
|
||||
@echo "Done."
|
||||
build: build_$(BASEIMAGE)
|
||||
|
||||
modify_composefile:
|
||||
|
@ -350,7 +359,7 @@ package_offline: compile build modify_sourcefiles modify_composefile
|
|||
|
||||
@echo "pulling nginx and registry..."
|
||||
@$(DOCKERPULL) vmware/registry:$(REGISTRYVERSION)
|
||||
@$(DOCKERPULL) vmware/nginx:$(NGINXVERSION)
|
||||
@$(DOCKERPULL) vmware/nginx-photon:$(NGINXVERSION)
|
||||
@if [ "$(NOTARYFLAG)" = "true" ] ; then \
|
||||
echo "pulling notary and harbor-notary-db..."; \
|
||||
$(DOCKERPULL) vmware/notary-photon:$(NOTARYVERSION); \
|
||||
|
@ -360,7 +369,7 @@ package_offline: compile build modify_sourcefiles modify_composefile
|
|||
@if [ "$(CLAIRFLAG)" = "true" ] ; then \
|
||||
echo "pulling claiy and postgres..."; \
|
||||
$(DOCKERPULL) quay.io/coreos/clair:$(CLAIRVERSION); \
|
||||
$(DOCKERPULL) postgres:$(CLAIRDBVERSION); \
|
||||
$(DOCKERPULL) vmware/postgresql:$(CLAIRDBVERSION); \
|
||||
fi
|
||||
|
||||
@echo "saving harbor docker image"
|
||||
|
|
|
@ -566,6 +566,33 @@ paths:
|
|||
$ref: '#/definitions/User'
|
||||
401:
|
||||
description: User need to log in first.
|
||||
/users/{user_id}:
|
||||
get:
|
||||
summary: Get a user's profile.
|
||||
description: |
|
||||
Get user's profile with user id.
|
||||
parameters:
|
||||
- name: user_id
|
||||
in: path
|
||||
type: integer
|
||||
format: int
|
||||
required: true
|
||||
description: Registered user ID
|
||||
tags:
|
||||
- Products
|
||||
responses:
|
||||
200:
|
||||
description: Get user's profile successfully.
|
||||
400:
|
||||
description: Invalid user ID.
|
||||
401:
|
||||
description: User need to log in first.
|
||||
403:
|
||||
description: User does not have permission of admin role.
|
||||
404:
|
||||
description: User ID does not exist.
|
||||
500:
|
||||
description: Unexpected internal errors.
|
||||
/users/{user_id}:
|
||||
put:
|
||||
summary: Update a registered user to change his profile.
|
||||
|
@ -588,7 +615,7 @@ paths:
|
|||
- Products
|
||||
responses:
|
||||
200:
|
||||
description: Updated user's admin role successfully.
|
||||
description: Updated user's profile successfully.
|
||||
400:
|
||||
description: Invalid user ID.
|
||||
401:
|
||||
|
@ -905,7 +932,7 @@ paths:
|
|||
503:
|
||||
description: Harbor is not deployed with Clair.
|
||||
|
||||
/repositories/{repo_name}/tags/{tag}/vulnerability/detail:
|
||||
/repositories/{repo_name}/tags/{tag}/vulnerability/details:
|
||||
get:
|
||||
summary: Get vulnerability details of the image.
|
||||
description: |
|
||||
|
@ -1065,7 +1092,7 @@ paths:
|
|||
in: query
|
||||
type: integer
|
||||
format: int
|
||||
required: false
|
||||
required: true
|
||||
description: The ID of the policy that triggered this job.
|
||||
- name: num
|
||||
in: query
|
||||
|
@ -1113,7 +1140,7 @@ paths:
|
|||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/RepPolicy'
|
||||
$ref: '#/definitions/JobStatus'
|
||||
headers:
|
||||
X-Total-Count:
|
||||
description: The total count of jobs
|
||||
|
@ -1202,7 +1229,7 @@ paths:
|
|||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/JobStatus'
|
||||
$ref: '#/definitions/RepPolicy'
|
||||
400:
|
||||
description: Invalid project ID.
|
||||
401:
|
||||
|
@ -1809,6 +1836,18 @@ definitions:
|
|||
type: integer
|
||||
format: int
|
||||
description: The public status of the project.
|
||||
enable_content_trust:
|
||||
type: boolean
|
||||
description: Whether content trust is enabled or not. If it is enabled, user cann't pull unsigned images from this project.
|
||||
prevent_vulnerable_images_from_running:
|
||||
type: boolean
|
||||
description: Whether prevent the vulnerable images from running.
|
||||
prevent_vulnerable_images_from_running_severity:
|
||||
type: string
|
||||
description: If the vulnerability is high than severity defined here, the images cann't be pulled.
|
||||
automatically_scan_images_on_push:
|
||||
type: boolean
|
||||
description: Whether scan images automatically when pushing.
|
||||
Project:
|
||||
type: object
|
||||
properties:
|
||||
|
@ -1849,6 +1888,18 @@ definitions:
|
|||
repo_count:
|
||||
type: integer
|
||||
description: The number of the repositories under this project.
|
||||
enable_content_trust:
|
||||
type: boolean
|
||||
description: Whether content trust is enabled or not. If it is enabled, user cann't pull unsigned images from this project.
|
||||
prevent_vulnerable_images_from_running:
|
||||
type: boolean
|
||||
description: Whether prevent the vulnerable images from running.
|
||||
prevent_vulnerable_images_from_running_severity:
|
||||
type: string
|
||||
description: If the vulnerability is high than severity defined here, the images cann't be pulled.
|
||||
automatically_scan_images_on_push:
|
||||
type: boolean
|
||||
description: Whether scan images automatically when pushing.
|
||||
Manifest:
|
||||
type: object
|
||||
properties:
|
||||
|
@ -1981,14 +2032,14 @@ definitions:
|
|||
StatisticMap:
|
||||
type: object
|
||||
properties:
|
||||
my_project_count:
|
||||
private_project_count:
|
||||
type: integer
|
||||
format: int32
|
||||
description: The count of the projects which the user is a member of.
|
||||
my_repo_count:
|
||||
description: The count of the private projects which the user is a member of.
|
||||
private_repo_count:
|
||||
type: integer
|
||||
format: int32
|
||||
description: The count of the repositories belonging to the projects which the user is a member of.
|
||||
description: The count of the private repositories belonging to the projects which the user is a member of.
|
||||
public_project_count:
|
||||
type: integer
|
||||
format: int32
|
||||
|
|
|
@ -204,4 +204,4 @@ CREATE TABLE IF NOT EXISTS `alembic_version` (
|
|||
`version_num` varchar(32) NOT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
|
||||
insert into alembic_version values ('0.4.0');
|
||||
insert into alembic_version values ('1.2.0');
|
||||
|
|
11
make/common/nginx/Dockerfile
Normal file
11
make/common/nginx/Dockerfile
Normal file
|
@ -0,0 +1,11 @@
|
|||
FROM library/photon:1.0
|
||||
|
||||
RUN tdnf install -y nginx \
|
||||
&& ln -sf /dev/stdout /var/log/nginx/access.log \
|
||||
&& ln -sf /dev/stderr /var/log/nginx/error.log \
|
||||
&& mkdir -p /var/run
|
||||
|
||||
EXPOSE 80
|
||||
STOPSIGNAL SIGQUIT
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
26
make/common/postgresql/Dockerfile
Normal file
26
make/common/postgresql/Dockerfile
Normal file
|
@ -0,0 +1,26 @@
|
|||
FROM library/photon:1.0
|
||||
|
||||
ENV PGDATA /var/lib/postgresql/data
|
||||
|
||||
RUN touch /etc/localtime.bak \
|
||||
&& tdnf install -y sed shadow gzip postgresql\
|
||||
&& groupadd -r postgres --gid=999 \
|
||||
&& useradd -r -g postgres --uid=999 postgres \
|
||||
&& mkdir -p /docker-entrypoint-initdb.d \
|
||||
&& mkdir -p /run/postgresql \
|
||||
&& chown -R postgres:postgres /run/postgresql \
|
||||
&& chmod 2777 /run/postgresql \
|
||||
&& mkdir -p "$PGDATA" && chown -R postgres:postgres "$PGDATA" && chmod 777 "$PGDATA" \
|
||||
&& sed -i "s|#listen_addresses = 'localhost'.*|listen_addresses = '*'|g" /usr/share/postgresql/postgresql.conf.sample \
|
||||
&& sed -i "s|#unix_socket_directories = '/tmp'.*|unix_socket_directories = '/run/postgresql'|g" /usr/share/postgresql/postgresql.conf.sample \
|
||||
&& touch /usr/share/locale/locale.alias \
|
||||
&& locale-gen.sh en_US.UTF-8
|
||||
|
||||
VOLUME /var/lib/postgresql/data
|
||||
|
||||
ADD docker-entrypoint.sh /entrypoint.sh
|
||||
RUN chmod u+x /entrypoint.sh
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
|
||||
EXPOSE 5432
|
||||
CMD ["postgres"]
|
120
make/common/postgresql/docker-entrypoint.sh
Normal file
120
make/common/postgresql/docker-entrypoint.sh
Normal file
|
@ -0,0 +1,120 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# usage: file_env VAR [DEFAULT]
|
||||
# ie: file_env 'XYZ_DB_PASSWORD' 'example'
|
||||
# (will allow for "$XYZ_DB_PASSWORD_FILE" to fill in the value of
|
||||
# "$XYZ_DB_PASSWORD" from a file, especially for Docker's secrets feature)
|
||||
file_env() {
|
||||
local var="$1"
|
||||
local fileVar="${var}_FILE"
|
||||
local def="${2:-}"
|
||||
if [ "${!var:-}" ] && [ "${!fileVar:-}" ]; then
|
||||
echo >&2 "error: both $var and $fileVar are set (but are exclusive)"
|
||||
exit 1
|
||||
fi
|
||||
local val="$def"
|
||||
if [ "${!var:-}" ]; then
|
||||
val="${!var}"
|
||||
elif [ "${!fileVar:-}" ]; then
|
||||
val="$(< "${!fileVar}")"
|
||||
fi
|
||||
export "$var"="$val"
|
||||
unset "$fileVar"
|
||||
}
|
||||
|
||||
if [ "${1:0:1}" = '-' ]; then
|
||||
set -- postgres "$@"
|
||||
fi
|
||||
|
||||
if [ "$1" = 'postgres' ]; then
|
||||
chown -R postgres:postgres $PGDATA
|
||||
echo here1
|
||||
# look specifically for PG_VERSION, as it is expected in the DB dir
|
||||
if [ ! -s "$PGDATA/PG_VERSION" ]; then
|
||||
file_env 'POSTGRES_INITDB_ARGS'
|
||||
if [ "$POSTGRES_INITDB_XLOGDIR" ]; then
|
||||
export POSTGRES_INITDB_ARGS="$POSTGRES_INITDB_ARGS --xlogdir $POSTGRES_INITDB_XLOGDIR"
|
||||
fi
|
||||
echo hehe2
|
||||
su - $1 -c "initdb -D $PGDATA -U postgres -E UTF-8 --lc-collate=en_US.UTF-8 --lc-ctype=en_US.UTF-8 $POSTGRES_INITDB_ARGS"
|
||||
echo hehe3
|
||||
# check password first so we can output the warning before postgres
|
||||
# messes it up
|
||||
file_env 'POSTGRES_PASSWORD'
|
||||
if [ "$POSTGRES_PASSWORD" ]; then
|
||||
pass="PASSWORD '$POSTGRES_PASSWORD'"
|
||||
authMethod=md5
|
||||
else
|
||||
# The - option suppresses leading tabs but *not* spaces. :)
|
||||
cat >&2 <<-EOF
|
||||
****************************************************
|
||||
WARNING: No password has been set for the database.
|
||||
This will allow anyone with access to the
|
||||
Postgres port to access your database. In
|
||||
Docker's default configuration, this is
|
||||
effectively any other container on the same
|
||||
system.
|
||||
Use "-e POSTGRES_PASSWORD=password" to set
|
||||
it in "docker run".
|
||||
****************************************************
|
||||
EOF
|
||||
|
||||
pass=
|
||||
authMethod=trust
|
||||
fi
|
||||
|
||||
{
|
||||
echo
|
||||
echo "host all all all $authMethod"
|
||||
} >> "$PGDATA/pg_hba.conf"
|
||||
su postgres
|
||||
echo `whoami`
|
||||
# internal start of server in order to allow set-up using psql-client
|
||||
# does not listen on external TCP/IP and waits until start finishes
|
||||
su - $1 -c "pg_ctl -D \"$PGDATA\" -o \"-c listen_addresses='localhost'\" -w start"
|
||||
|
||||
file_env 'POSTGRES_USER' 'postgres'
|
||||
file_env 'POSTGRES_DB' "$POSTGRES_USER"
|
||||
|
||||
psql=( psql -v ON_ERROR_STOP=1 )
|
||||
|
||||
if [ "$POSTGRES_DB" != 'postgres' ]; then
|
||||
"${psql[@]}" --username postgres <<-EOSQL
|
||||
CREATE DATABASE "$POSTGRES_DB" ;
|
||||
EOSQL
|
||||
echo
|
||||
fi
|
||||
|
||||
if [ "$POSTGRES_USER" = 'postgres' ]; then
|
||||
op='ALTER'
|
||||
else
|
||||
op='CREATE'
|
||||
fi
|
||||
"${psql[@]}" --username postgres <<-EOSQL
|
||||
$op USER "$POSTGRES_USER" WITH SUPERUSER $pass ;
|
||||
EOSQL
|
||||
echo
|
||||
|
||||
psql+=( --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" )
|
||||
|
||||
echo
|
||||
for f in /docker-entrypoint-initdb.d/*; do
|
||||
case "$f" in
|
||||
*.sh) echo "$0: running $f"; . "$f" ;;
|
||||
*.sql) echo "$0: running $f"; "${psql[@]}" -f "$f"; echo ;;
|
||||
*.sql.gz) echo "$0: running $f"; gunzip -c "$f" | "${psql[@]}"; echo ;;
|
||||
*) echo "$0: ignoring $f" ;;
|
||||
esac
|
||||
echo
|
||||
done
|
||||
|
||||
PGUSER="${PGUSER:-postgres}" \
|
||||
su - $1 -c "pg_ctl -D \"$PGDATA\" -m fast -w stop"
|
||||
|
||||
echo
|
||||
echo 'PostgreSQL init process complete; ready for start up.'
|
||||
echo
|
||||
fi
|
||||
fi
|
||||
exec su - $1 -c "$@ -D $PGDATA"
|
7
make/common/templates/clair/postgresql-init.d/README.md
Normal file
7
make/common/templates/clair/postgresql-init.d/README.md
Normal file
|
@ -0,0 +1,7 @@
|
|||
This folder used to run some initial sql for clair if needed.
|
||||
|
||||
Just put the sql file in this directory and then start the
|
||||
clair .
|
||||
|
||||
both .sql and .gz format supported
|
||||
|
|
@ -83,6 +83,7 @@ services:
|
|||
- ./common/config/ui/private_key.pem:/etc/ui/private_key.pem:z
|
||||
- /data/secretkey:/etc/ui/key:z
|
||||
- /data/ca_download/:/etc/ui/ca/:z
|
||||
- /data/service_token:/etc/ui/service_token:z
|
||||
networks:
|
||||
- harbor
|
||||
depends_on:
|
||||
|
|
|
@ -17,13 +17,14 @@ services:
|
|||
aliases:
|
||||
- postgres
|
||||
container_name: clair-db
|
||||
image: postgres:9.6.3
|
||||
image: vmware/postgresql:9.6.3-photon
|
||||
restart: always
|
||||
depends_on:
|
||||
- log
|
||||
env_file:
|
||||
./common/config/clair/postgres_env
|
||||
volumes:
|
||||
- ./common/config/clair/postgresql-init.d/:/docker-entrypoint-initdb.d
|
||||
- /data/clair-db:/var/lib/postgresql/data
|
||||
logging:
|
||||
driver: "syslog"
|
||||
|
|
|
@ -77,6 +77,7 @@ services:
|
|||
- ./common/config/ui/private_key.pem:/etc/ui/private_key.pem:z
|
||||
- /data/secretkey:/etc/ui/key:z
|
||||
- /data/ca_download/:/etc/ui/ca/:z
|
||||
- /data/service_token:/etc/ui/service_token:z
|
||||
networks:
|
||||
- harbor
|
||||
depends_on:
|
||||
|
@ -109,7 +110,7 @@ services:
|
|||
syslog-address: "tcp://127.0.0.1:1514"
|
||||
tag: "jobservice"
|
||||
proxy:
|
||||
image: vmware/nginx:1.11.5-patched
|
||||
image: vmware/nginx-photon:1.11.13
|
||||
container_name: nginx
|
||||
restart: always
|
||||
volumes:
|
||||
|
|
|
@ -373,6 +373,10 @@ if args.clair_mode:
|
|||
pg_password = "password"
|
||||
clair_temp_dir = os.path.join(templates_dir, "clair")
|
||||
clair_config_dir = prep_conf_dir(config_dir, "clair")
|
||||
print("Copying offline data file for clair DB")
|
||||
if os.path.exists(os.path.join(clair_config_dir, "postgresql-init.d")):
|
||||
shutil.rmtree(os.path.join(clair_config_dir, "postgresql-init.d"))
|
||||
shutil.copytree(os.path.join(clair_temp_dir, "postgresql-init.d"), os.path.join(clair_config_dir, "postgresql-init.d"))
|
||||
postgres_env = os.path.join(clair_config_dir, "postgres_env")
|
||||
render(os.path.join(clair_temp_dir, "postgres_env"), postgres_env, password = pg_password)
|
||||
clair_conf = os.path.join(clair_config_dir, "config.yaml")
|
||||
|
|
|
@ -20,9 +20,9 @@ const (
|
|||
LDAPAuth = "ldap_auth"
|
||||
ProCrtRestrEveryone = "everyone"
|
||||
ProCrtRestrAdmOnly = "adminonly"
|
||||
LDAPScopeBase = "1"
|
||||
LDAPScopeOnelevel = "2"
|
||||
LDAPScopeSubtree = "3"
|
||||
LDAPScopeBase = 1
|
||||
LDAPScopeOnelevel = 2
|
||||
LDAPScopeSubtree = 3
|
||||
|
||||
RoleProjectAdmin = 1
|
||||
RoleDeveloper = 2
|
||||
|
@ -65,4 +65,5 @@ const (
|
|||
AdmiralEndpoint = "admiral_url"
|
||||
WithNotary = "with_notary"
|
||||
WithClair = "with_clair"
|
||||
ScanAllPolicy = "scan_all_policy"
|
||||
)
|
||||
|
|
|
@ -54,9 +54,8 @@ func DeleteProjectMember(projectID int64, userID int) error {
|
|||
}
|
||||
|
||||
// GetUserByProject gets all members of the project.
|
||||
func GetUserByProject(projectID int64, queryUser models.User) ([]models.User, error) {
|
||||
func GetUserByProject(projectID int64, queryUser models.User) ([]*models.Member, error) {
|
||||
o := GetOrmer()
|
||||
u := []models.User{}
|
||||
sql := `select u.user_id, u.username, u.creation_time, u.update_time, r.name as rolename,
|
||||
r.role_id as role
|
||||
from user u
|
||||
|
@ -74,6 +73,9 @@ func GetUserByProject(projectID int64, queryUser models.User) ([]models.User, er
|
|||
queryParam = append(queryParam, "%"+escape(queryUser.Username)+"%")
|
||||
}
|
||||
sql += ` order by u.username `
|
||||
_, err := o.Raw(sql, queryParam).QueryRows(&u)
|
||||
return u, err
|
||||
|
||||
members := []*models.Member{}
|
||||
_, err := o.Raw(sql, queryParam).QueryRows(&members)
|
||||
|
||||
return members, err
|
||||
}
|
||||
|
|
|
@ -12,21 +12,12 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package utils contains methods to support security, cache, and webhook functions.
|
||||
package utils
|
||||
package models
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/vmware/harbor/src/common/utils/log"
|
||||
)
|
||||
|
||||
// VerifySecret verifies the UI_SECRET cookie in a http request.
|
||||
// TODO remove
|
||||
func VerifySecret(r *http.Request, expectedSecret string) bool {
|
||||
c, err := r.Cookie("secret")
|
||||
if err != nil {
|
||||
log.Warningf("Failed to get secret cookie, error: %v", err)
|
||||
}
|
||||
return c != nil && c.Value == expectedSecret
|
||||
// Member holds the details of a member.
|
||||
type Member struct {
|
||||
ID int `orm:"pk;column(user_id)" json:"user_id"`
|
||||
Username string `json:"username"`
|
||||
Rolename string `json:"role_name"`
|
||||
Role int `json:"role_id"`
|
||||
}
|
|
@ -73,15 +73,15 @@ func (ps *ProjectSorter) Swap(i, j int) {
|
|||
// List projects which user1 is member of: query := &QueryParam{Member:&Member{Name:"user1"}}
|
||||
// List projects which user1 is the project admin : query := &QueryParam{Memeber:&Member{Name:"user1",Role:1}}
|
||||
type ProjectQueryParam struct {
|
||||
Name string // the name of project
|
||||
Owner string // the username of project owner
|
||||
Public *bool // the project is public or not, can be ture, false and nil
|
||||
Member *Member // the member of project
|
||||
Pagination *Pagination // pagination information
|
||||
Name string // the name of project
|
||||
Owner string // the username of project owner
|
||||
Public *bool // the project is public or not, can be ture, false and nil
|
||||
Member *MemberQuery // the member of project
|
||||
Pagination *Pagination // pagination information
|
||||
}
|
||||
|
||||
// Member fitler by member's username and role
|
||||
type Member struct {
|
||||
// MemberQuery fitler by member's username and role
|
||||
type MemberQuery struct {
|
||||
Name string // the username of member
|
||||
Role int // the role of the member has to the project
|
||||
}
|
||||
|
@ -99,3 +99,13 @@ type BaseProjectCollection struct {
|
|||
Public bool
|
||||
Member string
|
||||
}
|
||||
|
||||
// ProjectRequest holds informations that need for creating project API
|
||||
type ProjectRequest struct {
|
||||
Name string `json:"project_name"`
|
||||
Public int `json:"public"`
|
||||
EnableContentTrust bool `json:"enable_content_trust"`
|
||||
PreventVulnerableImagesFromRunning bool `json:"prevent_vulnerable_images_from_running"`
|
||||
PreventVulnerableImagesFromRunningSeverity string `json:"prevent_vulnerable_images_from_running_severity"`
|
||||
AutomaticallyScanImagesOnPush bool `json:"automatically_scan_images_on_push"`
|
||||
}
|
||||
|
|
|
@ -96,3 +96,28 @@ type VulnerabilityItem struct {
|
|||
Description string `json:"description"`
|
||||
Fixed string `json:"fixedVersion,omitempty"`
|
||||
}
|
||||
|
||||
// ScanAllPolicy is represent the json request and object for scan all policy, the parm is het
|
||||
type ScanAllPolicy struct {
|
||||
Type string `json:"type"`
|
||||
Parm map[string]interface{} `json:"parameter, omitempty"`
|
||||
}
|
||||
|
||||
const (
|
||||
// ScanAllNone "none" for not doing any scan all
|
||||
ScanAllNone = "none"
|
||||
// ScanAllDaily for doing scan all daily
|
||||
ScanAllDaily = "daily"
|
||||
// ScanAllOnRefresh for doing scan all when the Clair DB is refreshed.
|
||||
ScanAllOnRefresh = "on_refresh"
|
||||
// ScanAllDailyTime the key for parm of daily scan all policy.
|
||||
ScanAllDailyTime = "daily_time"
|
||||
)
|
||||
|
||||
//DefaultScanAllPolicy ...
|
||||
var DefaultScanAllPolicy = ScanAllPolicy{
|
||||
Type: ScanAllDaily,
|
||||
Parm: map[string]interface{}{
|
||||
ScanAllDailyTime: 0,
|
||||
},
|
||||
}
|
||||
|
|
185
src/common/scheduler/policy/alternate_policy.go
Normal file
185
src/common/scheduler/policy/alternate_policy.go
Normal file
|
@ -0,0 +1,185 @@
|
|||
package policy
|
||||
|
||||
import "github.com/vmware/harbor/src/common/scheduler/task"
|
||||
import "errors"
|
||||
import "time"
|
||||
|
||||
//AlternatePolicyConfiguration store the related configurations for alternate policy.
|
||||
type AlternatePolicyConfiguration struct {
|
||||
//The interval of executing attached tasks.
|
||||
Duration time.Duration
|
||||
|
||||
//The execution time point of each turn
|
||||
//It's a number to indicate the seconds offset to the 00:00 of UTC time.
|
||||
OffsetTime int64
|
||||
|
||||
//Time should be later than start time.
|
||||
//If set <=0 value, no limitation.
|
||||
StartTimestamp int64
|
||||
|
||||
//Time should be earlier than end time.
|
||||
//If set <=0 value, no limitation.
|
||||
EndTimestamp int64
|
||||
}
|
||||
|
||||
//AlternatePolicy is a policy that repeatedly executing tasks with specified duration during a specified time scope.
|
||||
type AlternatePolicy struct {
|
||||
//Keep the attached tasks.
|
||||
tasks []task.Task
|
||||
|
||||
//Policy configurations.
|
||||
config *AlternatePolicyConfiguration
|
||||
|
||||
//Generate time ticks with specified duration.
|
||||
ticker *time.Ticker
|
||||
|
||||
//To indicated whether policy is completed.
|
||||
isEnabled bool
|
||||
|
||||
//Channel used to send evaluation result signals.
|
||||
evaluation chan EvaluationResult
|
||||
|
||||
//Channel used to notify policy termination.
|
||||
done chan bool
|
||||
|
||||
//Channel used to receive terminate signal.
|
||||
terminator chan bool
|
||||
}
|
||||
|
||||
//NewAlternatePolicy is constructor of creating AlternatePolicy.
|
||||
func NewAlternatePolicy(config *AlternatePolicyConfiguration) *AlternatePolicy {
|
||||
return &AlternatePolicy{
|
||||
tasks: []task.Task{},
|
||||
config: config,
|
||||
isEnabled: false,
|
||||
}
|
||||
}
|
||||
|
||||
//GetConfig returns the current configuration options of this policy.
|
||||
func (alp *AlternatePolicy) GetConfig() *AlternatePolicyConfiguration {
|
||||
return alp.config
|
||||
}
|
||||
|
||||
//Name is an implementation of same method in policy interface.
|
||||
func (alp *AlternatePolicy) Name() string {
|
||||
return "Alternate Policy"
|
||||
}
|
||||
|
||||
//Tasks is an implementation of same method in policy interface.
|
||||
func (alp *AlternatePolicy) Tasks() []task.Task {
|
||||
copyList := []task.Task{}
|
||||
if alp.tasks != nil && len(alp.tasks) > 0 {
|
||||
copyList = append(copyList, alp.tasks...)
|
||||
}
|
||||
|
||||
return copyList
|
||||
}
|
||||
|
||||
//Done is an implementation of same method in policy interface.
|
||||
func (alp *AlternatePolicy) Done() chan bool {
|
||||
return alp.done
|
||||
}
|
||||
|
||||
//AttachTasks is an implementation of same method in policy interface.
|
||||
func (alp *AlternatePolicy) AttachTasks(tasks ...task.Task) error {
|
||||
if tasks == nil || len(tasks) == 0 {
|
||||
return errors.New("No tasks can be attached")
|
||||
}
|
||||
|
||||
alp.tasks = append(alp.tasks, tasks...)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
//Disable is an implementation of same method in policy interface.
|
||||
func (alp *AlternatePolicy) Disable() error {
|
||||
alp.isEnabled = false
|
||||
|
||||
//Stop the ticker
|
||||
if alp.ticker != nil {
|
||||
alp.ticker.Stop()
|
||||
}
|
||||
|
||||
//Stop the evaluation goroutine
|
||||
alp.terminator <- true
|
||||
alp.ticker = nil
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
//Evaluate is an implementation of same method in policy interface.
|
||||
func (alp *AlternatePolicy) Evaluate() chan EvaluationResult {
|
||||
//Keep idempotent
|
||||
if alp.isEnabled && alp.evaluation != nil {
|
||||
return alp.evaluation
|
||||
}
|
||||
|
||||
alp.done = make(chan bool)
|
||||
alp.terminator = make(chan bool)
|
||||
alp.evaluation = make(chan EvaluationResult)
|
||||
|
||||
go func() {
|
||||
timeNow := time.Now().UTC()
|
||||
timeSeconds := timeNow.Unix()
|
||||
|
||||
//Pre-check
|
||||
//If now is still in the specified time scope.
|
||||
if alp.config.EndTimestamp > 0 && timeSeconds >= alp.config.EndTimestamp {
|
||||
//Invalid configuration, exit.
|
||||
alp.done <- true
|
||||
alp.isEnabled = false
|
||||
return
|
||||
}
|
||||
if alp.config.StartTimestamp > 0 && timeSeconds < alp.config.StartTimestamp {
|
||||
//Let's hold on for a while.
|
||||
forWhile := alp.config.StartTimestamp - timeSeconds
|
||||
time.Sleep(time.Duration(forWhile) * time.Second)
|
||||
}
|
||||
|
||||
//Reach the execution time point?
|
||||
utcTime := (int64)(timeNow.Hour()*3600 + timeNow.Minute()*60)
|
||||
diff := alp.config.OffsetTime - utcTime
|
||||
if diff < 0 {
|
||||
diff += 24 * 3600
|
||||
}
|
||||
if diff > 0 {
|
||||
//Wait for a while.
|
||||
time.Sleep(time.Duration(diff) * time.Second)
|
||||
}
|
||||
|
||||
//Trigger the first tick.
|
||||
alp.evaluation <- EvaluationResult{}
|
||||
|
||||
//Start the ticker for repeat checking.
|
||||
alp.ticker = time.NewTicker(alp.config.Duration)
|
||||
for {
|
||||
select {
|
||||
case now := <-alp.ticker.C:
|
||||
{
|
||||
time := now.UTC().Unix()
|
||||
if alp.config.EndTimestamp > 0 && time >= alp.config.EndTimestamp {
|
||||
//Ploicy is done.
|
||||
alp.done <- true
|
||||
alp.isEnabled = false
|
||||
if alp.ticker != nil {
|
||||
alp.ticker.Stop()
|
||||
}
|
||||
alp.ticker = nil
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
res := EvaluationResult{}
|
||||
alp.evaluation <- res
|
||||
}
|
||||
case <-alp.terminator:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
//Enabled
|
||||
alp.isEnabled = true
|
||||
|
||||
return alp.evaluation
|
||||
}
|
120
src/common/scheduler/policy/alternate_policy_test.go
Normal file
120
src/common/scheduler/policy/alternate_policy_test.go
Normal file
|
@ -0,0 +1,120 @@
|
|||
package policy
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type fakeTask struct {
|
||||
number int
|
||||
}
|
||||
|
||||
func (ft *fakeTask) TaskName() string {
|
||||
return "for testing"
|
||||
}
|
||||
|
||||
func (ft *fakeTask) Run() error {
|
||||
ft.number++
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestBasic(t *testing.T) {
|
||||
tp := NewAlternatePolicy(&AlternatePolicyConfiguration{})
|
||||
err := tp.AttachTasks(&fakeTask{number: 100})
|
||||
if err != nil {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
if tp.GetConfig() == nil {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
if tp.Name() != "Alternate Policy" {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
tks := tp.Tasks()
|
||||
if tks == nil || len(tks) != 1 {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestEvaluatePolicy(t *testing.T) {
|
||||
now := time.Now().UTC()
|
||||
utcOffset := (int64)(now.Hour()*3600 + now.Minute()*60)
|
||||
tp := NewAlternatePolicy(&AlternatePolicyConfiguration{
|
||||
Duration: 1 * time.Second,
|
||||
OffsetTime: utcOffset + 1,
|
||||
StartTimestamp: -1,
|
||||
EndTimestamp: now.Add(3 * time.Second).Unix(),
|
||||
})
|
||||
err := tp.AttachTasks(&fakeTask{number: 100})
|
||||
if err != nil {
|
||||
t.Fail()
|
||||
}
|
||||
ch := tp.Evaluate()
|
||||
done := tp.Done()
|
||||
counter := 0
|
||||
READ_SIGNAL:
|
||||
for {
|
||||
select {
|
||||
case <-ch:
|
||||
counter++
|
||||
case <-done:
|
||||
break READ_SIGNAL
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fail()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if counter != 2 {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisablePolicy(t *testing.T) {
|
||||
now := time.Now().UTC()
|
||||
utcOffset := (int64)(now.Hour()*3600 + now.Minute()*60)
|
||||
tp := NewAlternatePolicy(&AlternatePolicyConfiguration{
|
||||
Duration: 1 * time.Second,
|
||||
OffsetTime: utcOffset + 1,
|
||||
StartTimestamp: -1,
|
||||
EndTimestamp: -1,
|
||||
})
|
||||
err := tp.AttachTasks(&fakeTask{number: 100})
|
||||
if err != nil {
|
||||
t.Fail()
|
||||
}
|
||||
ch := tp.Evaluate()
|
||||
counter := 0
|
||||
terminate := make(chan bool)
|
||||
defer func() {
|
||||
terminate <- true
|
||||
}()
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-ch:
|
||||
counter++
|
||||
case <-terminate:
|
||||
return
|
||||
case <-time.After(6 * time.Second):
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
time.Sleep(2 * time.Second)
|
||||
if tp.Disable() != nil {
|
||||
t.Fatal("Failed to disable policy")
|
||||
}
|
||||
//Waiting for everything is stabel
|
||||
<-time.After(1 * time.Second)
|
||||
//Copy value
|
||||
copiedCounter := counter
|
||||
time.Sleep(2 * time.Second)
|
||||
if counter != copiedCounter {
|
||||
t.Fatalf("Policy is still running after calling Disable() %d=%d", copiedCounter, counter)
|
||||
}
|
||||
}
|
15
src/common/scheduler/policy/evaluation_result.go
Normal file
15
src/common/scheduler/policy/evaluation_result.go
Normal file
|
@ -0,0 +1,15 @@
|
|||
package policy
|
||||
|
||||
//EvaluationResult is defined to carry the policy evaluated result.
|
||||
//
|
||||
//Filed 'Result' is optional.
|
||||
//Filed 'Error' is optional
|
||||
//
|
||||
type EvaluationResult struct {
|
||||
//Policy is successfully evaluated and the related information can
|
||||
//be contained in Result if have.
|
||||
Result interface{}
|
||||
|
||||
//Policy is failed to evaluated.
|
||||
Error error
|
||||
}
|
37
src/common/scheduler/policy/policy.go
Normal file
37
src/common/scheduler/policy/policy.go
Normal file
|
@ -0,0 +1,37 @@
|
|||
package policy
|
||||
|
||||
import "github.com/vmware/harbor/src/common/scheduler/task"
|
||||
|
||||
//Policy is an if-then logic to determine how the attached tasks should be
|
||||
//executed based on the evaluation result of the defined conditions.
|
||||
//E.g:
|
||||
// Daily execute TASK between 2017/06/24 and 2018/06/23
|
||||
// Execute TASK at 2017/09/01 14:30:00
|
||||
//
|
||||
//Each policy should have a name to identify itself.
|
||||
//Please be aware that policy with no tasks will be treated as invalid.
|
||||
//
|
||||
type Policy interface {
|
||||
//Return the name of the policy.
|
||||
Name() string
|
||||
|
||||
//Return the attached tasks with this policy.
|
||||
Tasks() []task.Task
|
||||
|
||||
//Attach tasks to this policy
|
||||
AttachTasks(...task.Task) error
|
||||
|
||||
//Done will setup a channel for other components to check whether or not
|
||||
//the policy is completed. Possibly designed for the none loop policy.
|
||||
Done() chan bool
|
||||
|
||||
//Evaluate the policy based on its definition and return the result via
|
||||
//result channel. Policy is enabled after it is evaluated.
|
||||
//Make sure Evaluate is idempotent, that means one policy can be only enabled
|
||||
//only once even if Evaluate is called more than one times.
|
||||
Evaluate() chan EvaluationResult
|
||||
|
||||
//Disable the enabled policy and release all the allocated resources.
|
||||
//Disable should also send signal to the terminated channel which returned by Done.
|
||||
Disable() error
|
||||
}
|
258
src/common/scheduler/scheduler.go
Normal file
258
src/common/scheduler/scheduler.go
Normal file
|
@ -0,0 +1,258 @@
|
|||
package scheduler
|
||||
|
||||
import "github.com/vmware/harbor/src/common/scheduler/policy"
|
||||
import "github.com/vmware/harbor/src/common/utils/log"
|
||||
|
||||
import "errors"
|
||||
import "strings"
|
||||
import "fmt"
|
||||
import "reflect"
|
||||
import "time"
|
||||
|
||||
const (
|
||||
defaultQueueSize = 10
|
||||
|
||||
statSchedulePolicy = "Schedule Policy"
|
||||
statUnSchedulePolicy = "Unschedule Policy"
|
||||
statTaskRun = "Task Run"
|
||||
statTaskComplete = "Task Complete"
|
||||
statTaskFail = "Task Fail"
|
||||
)
|
||||
|
||||
//StatItem is defined for the stat metrics.
|
||||
type StatItem struct {
|
||||
//Metrics catalog
|
||||
Type string
|
||||
|
||||
//The stat value
|
||||
Value uint32
|
||||
|
||||
//Attach some other info
|
||||
Attachment interface{}
|
||||
}
|
||||
|
||||
//StatSummary is used to collect some metrics of scheduler.
|
||||
type StatSummary struct {
|
||||
//Count of scheduled policy
|
||||
PolicyCount uint32
|
||||
|
||||
//Total count of tasks
|
||||
Tasks uint32
|
||||
|
||||
//Count of successfully complete tasks
|
||||
CompletedTasks uint32
|
||||
|
||||
//Count of tasks with errors
|
||||
TasksWithError uint32
|
||||
}
|
||||
|
||||
//Configuration defines configuration of Scheduler.
|
||||
type Configuration struct {
|
||||
QueueSize uint8
|
||||
}
|
||||
|
||||
//Scheduler is designed for scheduling policies.
|
||||
type Scheduler struct {
|
||||
//Related configuration options for scheduler.
|
||||
config *Configuration
|
||||
|
||||
//Store to keep the references of scheduled policies.
|
||||
policies Store
|
||||
|
||||
//Queue for accepting the scheduling polices.
|
||||
scheduleQueue chan policy.Policy
|
||||
|
||||
//Queue for receiving policy unschedule request or complete signal.
|
||||
unscheduleQueue chan string
|
||||
|
||||
//Channel for receiving stat metrics.
|
||||
statChan chan *StatItem
|
||||
|
||||
//Channel for terminate scheduler damon.
|
||||
terminateChan chan bool
|
||||
|
||||
//The stat metrics of scheduler.
|
||||
stats *StatSummary
|
||||
|
||||
//To indicate whether scheduler is stopped or not
|
||||
stopped bool
|
||||
}
|
||||
|
||||
//DefaultScheduler is a default scheduler.
|
||||
var DefaultScheduler = NewScheduler(nil)
|
||||
|
||||
//NewScheduler is constructor for creating a scheduler.
|
||||
func NewScheduler(config *Configuration) *Scheduler {
|
||||
var qSize uint8 = defaultQueueSize
|
||||
if config != nil && config.QueueSize > 0 {
|
||||
qSize = config.QueueSize
|
||||
}
|
||||
|
||||
sq := make(chan policy.Policy, qSize)
|
||||
usq := make(chan string, qSize)
|
||||
stChan := make(chan *StatItem, 4)
|
||||
tc := make(chan bool, 2)
|
||||
|
||||
store := NewConcurrentStore(10)
|
||||
return &Scheduler{
|
||||
config: config,
|
||||
policies: store,
|
||||
scheduleQueue: sq,
|
||||
unscheduleQueue: usq,
|
||||
statChan: stChan,
|
||||
terminateChan: tc,
|
||||
stats: &StatSummary{
|
||||
PolicyCount: 0,
|
||||
Tasks: 0,
|
||||
CompletedTasks: 0,
|
||||
TasksWithError: 0,
|
||||
},
|
||||
stopped: true,
|
||||
}
|
||||
}
|
||||
|
||||
//Start the scheduler damon.
|
||||
func (sch *Scheduler) Start() {
|
||||
if !sch.stopped {
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Errorf("Runtime error in scheduler:%s\n", r)
|
||||
}
|
||||
}()
|
||||
defer func() {
|
||||
sch.stopped = true
|
||||
}()
|
||||
for {
|
||||
select {
|
||||
case p := <-sch.scheduleQueue:
|
||||
//Schedule the policy.
|
||||
watcher := NewWatcher(p, sch.statChan, sch.unscheduleQueue)
|
||||
|
||||
//Keep the policy for future use after it's successfully scheduled.
|
||||
sch.policies.Put(p.Name(), watcher)
|
||||
|
||||
//Enable it.
|
||||
watcher.Start()
|
||||
|
||||
sch.statChan <- &StatItem{statSchedulePolicy, 1, nil}
|
||||
case name := <-sch.unscheduleQueue:
|
||||
//Find the watcher.
|
||||
watcher := sch.policies.Remove(name)
|
||||
if watcher != nil && watcher.IsRunning() {
|
||||
watcher.Stop()
|
||||
}
|
||||
|
||||
sch.statChan <- &StatItem{statUnSchedulePolicy, 1, nil}
|
||||
case <-sch.terminateChan:
|
||||
//Exit
|
||||
return
|
||||
|
||||
case stat := <-sch.statChan:
|
||||
{
|
||||
switch stat.Type {
|
||||
case statSchedulePolicy:
|
||||
sch.stats.PolicyCount += stat.Value
|
||||
break
|
||||
case statUnSchedulePolicy:
|
||||
sch.stats.PolicyCount -= stat.Value
|
||||
break
|
||||
case statTaskRun:
|
||||
sch.stats.Tasks += stat.Value
|
||||
break
|
||||
case statTaskComplete:
|
||||
sch.stats.CompletedTasks += stat.Value
|
||||
break
|
||||
case statTaskFail:
|
||||
sch.stats.TasksWithError += stat.Value
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
log.Infof("Policies:%d, Tasks:%d, CompletedTasks:%d, FailedTasks:%d\n",
|
||||
sch.stats.PolicyCount,
|
||||
sch.stats.Tasks,
|
||||
sch.stats.CompletedTasks,
|
||||
sch.stats.TasksWithError)
|
||||
|
||||
if stat.Attachment != nil &&
|
||||
reflect.TypeOf(stat.Attachment).String() == "*errors.errorString" {
|
||||
log.Errorf("%s: %s\n", stat.Type, stat.Attachment.(error).Error())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
sch.stopped = false
|
||||
log.Infof("Policy scheduler start at %s\n", time.Now().UTC().Format(time.RFC3339))
|
||||
}
|
||||
|
||||
//Stop the scheduler damon.
|
||||
func (sch *Scheduler) Stop() {
|
||||
if sch.stopped {
|
||||
return
|
||||
}
|
||||
|
||||
//Terminate damon firstly to stop receiving signals.
|
||||
sch.terminateChan <- true
|
||||
|
||||
//Stop all watchers.
|
||||
for _, wt := range sch.policies.GetAll() {
|
||||
wt.Stop()
|
||||
}
|
||||
|
||||
//Clear resources
|
||||
sch.policies.Clear()
|
||||
|
||||
log.Infof("Policy scheduler stop at %s\n", time.Now().UTC().Format(time.RFC3339))
|
||||
}
|
||||
|
||||
//Schedule and enable the policy.
|
||||
func (sch *Scheduler) Schedule(scheduledPolicy policy.Policy) error {
|
||||
if scheduledPolicy == nil {
|
||||
return errors.New("nil is not Policy object")
|
||||
}
|
||||
|
||||
if strings.TrimSpace(scheduledPolicy.Name()) == "" {
|
||||
return errors.New("Policy should be assigned a name")
|
||||
}
|
||||
|
||||
tasks := scheduledPolicy.Tasks()
|
||||
if tasks == nil || len(tasks) == 0 {
|
||||
return errors.New("Policy must attach task(s)")
|
||||
}
|
||||
|
||||
if sch.policies.Exists(scheduledPolicy.Name()) {
|
||||
return errors.New("Duplicated policy")
|
||||
}
|
||||
|
||||
//Schedule the policy.
|
||||
sch.scheduleQueue <- scheduledPolicy
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
//UnSchedule the specified policy from the enabled policies list.
|
||||
func (sch *Scheduler) UnSchedule(policyName string) error {
|
||||
if strings.TrimSpace(policyName) == "" {
|
||||
return errors.New("Empty policy name is invalid")
|
||||
}
|
||||
|
||||
if !sch.policies.Exists(policyName) {
|
||||
return fmt.Errorf("Policy %s is not existing", policyName)
|
||||
}
|
||||
|
||||
//Unschedule the policy.
|
||||
sch.unscheduleQueue <- policyName
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
//IsStopped to indicate whether the scheduler is stopped
|
||||
func (sch *Scheduler) IsStopped() bool {
|
||||
return sch.stopped
|
||||
}
|
140
src/common/scheduler/scheduler_store.go
Normal file
140
src/common/scheduler/scheduler_store.go
Normal file
|
@ -0,0 +1,140 @@
|
|||
package scheduler
|
||||
|
||||
import "sync"
|
||||
import "strings"
|
||||
|
||||
const defaultSize = 10
|
||||
|
||||
//Store define the basic operations for storing and managing policy watcher.
|
||||
//The concrete implementation should consider concurrent supporting scenario.
|
||||
//
|
||||
type Store interface {
|
||||
//Put a new policy in.
|
||||
Put(key string, value *Watcher)
|
||||
|
||||
//Get the corresponding policy with the key.
|
||||
Get(key string) *Watcher
|
||||
|
||||
//Check if the key existing in the store.
|
||||
Exists(key string) bool
|
||||
|
||||
//Remove the specified policy and return its reference.
|
||||
Remove(key string) *Watcher
|
||||
|
||||
//Size return the total count of items in store.
|
||||
Size() uint32
|
||||
|
||||
//Get all the items in the store.
|
||||
GetAll() []*Watcher
|
||||
|
||||
//Clear store.
|
||||
Clear()
|
||||
}
|
||||
|
||||
//ConcurrentStore implements Store interface and supports concurrent operations.
|
||||
type ConcurrentStore struct {
|
||||
//Read-write mutex to synchronize the data map.
|
||||
mutex *sync.RWMutex
|
||||
|
||||
//Map used to keep the policy list.
|
||||
data map[string]*Watcher
|
||||
}
|
||||
|
||||
//NewConcurrentStore is used to create a new store and return the pointer reference.
|
||||
func NewConcurrentStore(initialSize uint32) *ConcurrentStore {
|
||||
var initSize uint32 = defaultSize
|
||||
if initialSize > 0 {
|
||||
initSize = initialSize
|
||||
}
|
||||
mutex := new(sync.RWMutex)
|
||||
data := make(map[string]*Watcher, initSize)
|
||||
|
||||
return &ConcurrentStore{mutex, data}
|
||||
}
|
||||
|
||||
//Put a policy into store.
|
||||
func (cs *ConcurrentStore) Put(key string, value *Watcher) {
|
||||
if strings.TrimSpace(key) == "" || value == nil {
|
||||
return
|
||||
}
|
||||
|
||||
defer cs.mutex.Unlock()
|
||||
|
||||
cs.mutex.Lock()
|
||||
cs.data[key] = value
|
||||
}
|
||||
|
||||
//Get policy via key.
|
||||
func (cs *ConcurrentStore) Get(key string) *Watcher {
|
||||
if strings.TrimSpace(key) == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
defer cs.mutex.RUnlock()
|
||||
|
||||
cs.mutex.RLock()
|
||||
return cs.data[key]
|
||||
}
|
||||
|
||||
//Exists is used to check whether or not the key exists in store.
|
||||
func (cs *ConcurrentStore) Exists(key string) bool {
|
||||
if strings.TrimSpace(key) == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
defer cs.mutex.RUnlock()
|
||||
|
||||
cs.mutex.RLock()
|
||||
_, ok := cs.data[key]
|
||||
|
||||
return ok
|
||||
}
|
||||
|
||||
//Remove is to delete the specified policy.
|
||||
func (cs *ConcurrentStore) Remove(key string) *Watcher {
|
||||
if !cs.Exists(key) {
|
||||
return nil
|
||||
}
|
||||
|
||||
defer cs.mutex.Unlock()
|
||||
|
||||
cs.mutex.Lock()
|
||||
if wt, ok := cs.data[key]; ok {
|
||||
delete(cs.data, key)
|
||||
return wt
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
//Size return the total count of items in store.
|
||||
func (cs *ConcurrentStore) Size() uint32 {
|
||||
return (uint32)(len(cs.data))
|
||||
}
|
||||
|
||||
//Get all the items of store.
|
||||
func (cs *ConcurrentStore) GetAll() []*Watcher {
|
||||
all := []*Watcher{}
|
||||
|
||||
defer cs.mutex.RUnlock()
|
||||
cs.mutex.RLock()
|
||||
for _, v := range cs.data {
|
||||
all = append(all, v)
|
||||
}
|
||||
|
||||
return all
|
||||
}
|
||||
|
||||
//Clear all the items in store.
|
||||
func (cs *ConcurrentStore) Clear() {
|
||||
if cs.Size() == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
defer cs.mutex.Unlock()
|
||||
cs.mutex.Lock()
|
||||
|
||||
for k := range cs.data {
|
||||
delete(cs.data, k)
|
||||
}
|
||||
}
|
71
src/common/scheduler/scheduler_store_test.go
Normal file
71
src/common/scheduler/scheduler_store_test.go
Normal file
|
@ -0,0 +1,71 @@
|
|||
package scheduler
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPut(t *testing.T) {
|
||||
store := NewConcurrentStore(10)
|
||||
if store == nil {
|
||||
t.Fatal("Failed to creat store instance")
|
||||
}
|
||||
|
||||
store.Put("testing", NewWatcher(nil, nil, nil))
|
||||
if store.Size() != 1 {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestGet(t *testing.T) {
|
||||
store := NewConcurrentStore(10)
|
||||
if store == nil {
|
||||
t.Fatal("Failed to creat store instance")
|
||||
}
|
||||
store.Put("testing", NewWatcher(nil, nil, nil))
|
||||
w := store.Get("testing")
|
||||
if w == nil {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemove(t *testing.T) {
|
||||
store := NewConcurrentStore(10)
|
||||
if store == nil {
|
||||
t.Fatal("Failed to creat store instance")
|
||||
}
|
||||
store.Put("testing", NewWatcher(nil, nil, nil))
|
||||
if !store.Exists("testing") {
|
||||
t.Fail()
|
||||
}
|
||||
w := store.Remove("testing")
|
||||
if w == nil {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestExisting(t *testing.T) {
|
||||
store := NewConcurrentStore(10)
|
||||
if store == nil {
|
||||
t.Fatal("Failed to creat store instance")
|
||||
}
|
||||
store.Put("testing", NewWatcher(nil, nil, nil))
|
||||
if !store.Exists("testing") {
|
||||
t.Fail()
|
||||
}
|
||||
if store.Exists("fake_key") {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAll(t *testing.T) {
|
||||
store := NewConcurrentStore(10)
|
||||
if store == nil {
|
||||
t.Fatal("Failed to creat store instance")
|
||||
}
|
||||
store.Put("testing", NewWatcher(nil, nil, nil))
|
||||
store.Put("testing2", NewWatcher(nil, nil, nil))
|
||||
list := store.GetAll()
|
||||
if list == nil || len(list) != 2 {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
142
src/common/scheduler/scheduler_test.go
Normal file
142
src/common/scheduler/scheduler_test.go
Normal file
|
@ -0,0 +1,142 @@
|
|||
package scheduler
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/vmware/harbor/src/common/scheduler/policy"
|
||||
"github.com/vmware/harbor/src/common/scheduler/task"
|
||||
)
|
||||
|
||||
type fakePolicy struct {
|
||||
tasks []task.Task
|
||||
done chan bool
|
||||
evaluation chan policy.EvaluationResult
|
||||
terminate chan bool
|
||||
ticker *time.Ticker
|
||||
}
|
||||
|
||||
func (fp *fakePolicy) Name() string {
|
||||
return "testing policy"
|
||||
}
|
||||
|
||||
func (fp *fakePolicy) Tasks() []task.Task {
|
||||
return fp.tasks
|
||||
}
|
||||
|
||||
func (fp *fakePolicy) AttachTasks(tasks ...task.Task) error {
|
||||
fp.tasks = append(fp.tasks, tasks...)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fp *fakePolicy) Done() chan bool {
|
||||
return fp.done
|
||||
}
|
||||
|
||||
func (fp *fakePolicy) Evaluate() chan policy.EvaluationResult {
|
||||
fp.evaluation = make(chan policy.EvaluationResult, 2)
|
||||
fp.done = make(chan bool)
|
||||
fp.terminate = make(chan bool)
|
||||
|
||||
fp.evaluation <- policy.EvaluationResult{}
|
||||
go func() {
|
||||
fp.ticker = time.NewTicker(1 * time.Second)
|
||||
for {
|
||||
select {
|
||||
case <-fp.terminate:
|
||||
return
|
||||
case <-fp.ticker.C:
|
||||
fp.evaluation <- policy.EvaluationResult{}
|
||||
}
|
||||
}
|
||||
}()
|
||||
return fp.evaluation
|
||||
}
|
||||
|
||||
func (fp *fakePolicy) Disable() error {
|
||||
if fp.ticker != nil {
|
||||
fp.ticker.Stop()
|
||||
}
|
||||
|
||||
fp.terminate <- true
|
||||
return nil
|
||||
}
|
||||
|
||||
type fakeTask struct {
|
||||
number int
|
||||
}
|
||||
|
||||
func (ft *fakeTask) TaskName() string {
|
||||
return "for testing"
|
||||
}
|
||||
|
||||
func (ft *fakeTask) Run() error {
|
||||
ft.number++
|
||||
return nil
|
||||
}
|
||||
|
||||
//Wacher will be tested together with scheduler.
|
||||
func TestScheduler(t *testing.T) {
|
||||
DefaultScheduler.Start()
|
||||
if DefaultScheduler.policies.Size() != 0 {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
if DefaultScheduler.stats.PolicyCount != 0 {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
if DefaultScheduler.IsStopped() {
|
||||
t.Fatal("Scheduler is not started")
|
||||
}
|
||||
|
||||
fp := &fakePolicy{
|
||||
tasks: []task.Task{},
|
||||
}
|
||||
fk := &fakeTask{number: 100}
|
||||
fp.AttachTasks(fk)
|
||||
|
||||
if DefaultScheduler.Schedule(fp) != nil {
|
||||
t.Fatal("Schedule policy failed")
|
||||
}
|
||||
//Waiting for everything is stable
|
||||
time.Sleep(1 * time.Second)
|
||||
if DefaultScheduler.policies.Size() == 0 {
|
||||
t.Fatal("No policy in the store after calling Schedule()")
|
||||
}
|
||||
if DefaultScheduler.stats.PolicyCount != 1 {
|
||||
t.Fatal("Policy stats do not match")
|
||||
}
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
if fk.number == 100 {
|
||||
t.Fatal("Task is not triggered")
|
||||
}
|
||||
if DefaultScheduler.stats.Tasks == 0 {
|
||||
t.Fail()
|
||||
}
|
||||
if DefaultScheduler.stats.CompletedTasks == 0 {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
if DefaultScheduler.UnSchedule(fp.Name()) != nil {
|
||||
t.Fatal("Unschedule policy failed")
|
||||
}
|
||||
//Waiting for everything is stable
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
if DefaultScheduler.stats.PolicyCount != 0 {
|
||||
t.Fatal("Policy count does not match after calling UnSchedule()")
|
||||
}
|
||||
copiedValue := DefaultScheduler.stats.CompletedTasks
|
||||
<-time.After(2 * time.Second)
|
||||
|
||||
if copiedValue != DefaultScheduler.stats.CompletedTasks {
|
||||
t.Fatalf("Policy is still enabled after calling UnSchedule(),%d=%d", copiedValue, DefaultScheduler.stats.CompletedTasks)
|
||||
}
|
||||
|
||||
DefaultScheduler.Stop()
|
||||
if DefaultScheduler.policies.Size() != 0 {
|
||||
t.Fatal("Scheduler is not cleared after stopping")
|
||||
}
|
||||
}
|
21
src/common/scheduler/task/scan_all_task.go
Normal file
21
src/common/scheduler/task/scan_all_task.go
Normal file
|
@ -0,0 +1,21 @@
|
|||
package task
|
||||
|
||||
import "github.com/vmware/harbor/src/ui/utils"
|
||||
|
||||
//ScanAllTask is task of scanning all tags.
|
||||
type ScanAllTask struct{}
|
||||
|
||||
//NewScanAllTask is constructor of creating ScanAllTask.
|
||||
func NewScanAllTask() *ScanAllTask {
|
||||
return &ScanAllTask{}
|
||||
}
|
||||
|
||||
//TaskName returns the name of the task.
|
||||
func (sat *ScanAllTask) TaskName() string {
|
||||
return "scan all"
|
||||
}
|
||||
|
||||
//Run the actions.
|
||||
func (sat *ScanAllTask) Run() error {
|
||||
return utils.ScanAllImages()
|
||||
}
|
14
src/common/scheduler/task/scan_all_task_test.go
Normal file
14
src/common/scheduler/task/scan_all_task_test.go
Normal file
|
@ -0,0 +1,14 @@
|
|||
package task
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestTask(t *testing.T) {
|
||||
tk := NewScanAllTask()
|
||||
if tk == nil {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
if tk.TaskName() != "scan all" {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
10
src/common/scheduler/task/task.go
Normal file
10
src/common/scheduler/task/task.go
Normal file
|
@ -0,0 +1,10 @@
|
|||
package task
|
||||
|
||||
//Task is used to synchronously run specific action(s).
|
||||
type Task interface {
|
||||
//Name of the task.
|
||||
TaskName() string
|
||||
|
||||
//Run the concrete code here
|
||||
Run() error
|
||||
}
|
128
src/common/scheduler/watcher.go
Normal file
128
src/common/scheduler/watcher.go
Normal file
|
@ -0,0 +1,128 @@
|
|||
package scheduler
|
||||
|
||||
import (
|
||||
"github.com/vmware/harbor/src/common/scheduler/policy"
|
||||
"github.com/vmware/harbor/src/common/scheduler/task"
|
||||
"github.com/vmware/harbor/src/common/utils/log"
|
||||
)
|
||||
|
||||
//Watcher is an asynchronous runner to provide an evaluation environment for the policy.
|
||||
type Watcher struct {
|
||||
//The target policy.
|
||||
p policy.Policy
|
||||
|
||||
//The channel for receive stop signal.
|
||||
cmdChan chan bool
|
||||
|
||||
//Indicate whether the watch is started and running.
|
||||
isRunning bool
|
||||
|
||||
//Report stats to scheduler.
|
||||
stats chan *StatItem
|
||||
|
||||
//If policy is automatically completed, report the policy to scheduler.
|
||||
doneChan chan string
|
||||
}
|
||||
|
||||
//NewWatcher is used as a constructor.
|
||||
func NewWatcher(p policy.Policy, st chan *StatItem, done chan string) *Watcher {
|
||||
return &Watcher{
|
||||
p: p,
|
||||
cmdChan: make(chan bool),
|
||||
isRunning: false,
|
||||
stats: st,
|
||||
doneChan: done,
|
||||
}
|
||||
}
|
||||
|
||||
//Start the running.
|
||||
func (wc *Watcher) Start() {
|
||||
if wc.isRunning {
|
||||
return
|
||||
}
|
||||
|
||||
if wc.p == nil {
|
||||
return
|
||||
}
|
||||
|
||||
go func(pl policy.Policy) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Errorf("Runtime error in watcher:%s\n", r)
|
||||
}
|
||||
}()
|
||||
|
||||
evalChan := pl.Evaluate()
|
||||
done := pl.Done()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-evalChan:
|
||||
{
|
||||
//Start to run the attached tasks.
|
||||
for _, t := range pl.Tasks() {
|
||||
go func(tk task.Task) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Errorf("Runtime error in task execution:%s\n", r)
|
||||
}
|
||||
}()
|
||||
err := tk.Run()
|
||||
|
||||
//Report task execution stats.
|
||||
st := &StatItem{statTaskComplete, 1, err}
|
||||
if err != nil {
|
||||
st.Type = statTaskFail
|
||||
}
|
||||
if wc.stats != nil {
|
||||
wc.stats <- st
|
||||
}
|
||||
}(t)
|
||||
|
||||
//Report task run stats.
|
||||
st := &StatItem{statTaskRun, 1, nil}
|
||||
if wc.stats != nil {
|
||||
wc.stats <- st
|
||||
}
|
||||
}
|
||||
}
|
||||
case <-done:
|
||||
{
|
||||
//Policy is automatically completed.
|
||||
wc.isRunning = false
|
||||
|
||||
//Report policy change stats.
|
||||
wc.doneChan <- wc.p.Name()
|
||||
|
||||
return
|
||||
}
|
||||
case <-wc.cmdChan:
|
||||
//Exit goroutine.
|
||||
return
|
||||
}
|
||||
}
|
||||
}(wc.p)
|
||||
|
||||
wc.isRunning = true
|
||||
}
|
||||
|
||||
//Stop the running.
|
||||
func (wc *Watcher) Stop() {
|
||||
if !wc.isRunning {
|
||||
return
|
||||
}
|
||||
|
||||
//Disable policy.
|
||||
if wc.p != nil {
|
||||
wc.p.Disable()
|
||||
}
|
||||
//Stop watcher.
|
||||
wc.cmdChan <- true
|
||||
|
||||
wc.isRunning = false
|
||||
}
|
||||
|
||||
//IsRunning to indicate if the watcher is still running.
|
||||
func (wc *Watcher) IsRunning() bool {
|
||||
return wc.isRunning
|
||||
}
|
|
@ -15,34 +15,37 @@
|
|||
package authcontext
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/vmware/harbor/src/ui/config"
|
||||
)
|
||||
|
||||
// TODO update the value of role when admiral API is ready
|
||||
const (
|
||||
// AuthTokenHeader is the key of auth token header
|
||||
AuthTokenHeader = "x-xenon-auth-token"
|
||||
sysAdminRole = "CLOUD_ADMIN"
|
||||
projectAdminRole = "DEVOPS_ADMIN"
|
||||
developerRole = "DEVELOPER"
|
||||
guestRole = "GUEST"
|
||||
projectAdminRole = "PROJECT_ADMIN"
|
||||
developerRole = "PROJECT_MEMBER"
|
||||
guestRole = "PROJECT_VIEWER"
|
||||
)
|
||||
|
||||
var client = &http.Client{
|
||||
Transport: &http.Transport{},
|
||||
type project struct {
|
||||
DocumentSelfLink string `json:"documentSelfLink"`
|
||||
Name string `json:"name"`
|
||||
Roles []string `json:"roles"`
|
||||
}
|
||||
|
||||
// AuthContext ...
|
||||
type AuthContext struct {
|
||||
PrincipalID string `json:"principalId"`
|
||||
Name string `json:"name"`
|
||||
Roles []string `json:"projects"`
|
||||
Projects map[string][]string `json:"roles"`
|
||||
PrincipalID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
Roles []string `json:"roles"`
|
||||
Projects []*project `json:"projects"`
|
||||
}
|
||||
|
||||
// GetUsername ...
|
||||
|
@ -54,7 +57,6 @@ func (a *AuthContext) GetUsername() string {
|
|||
func (a *AuthContext) IsSysAdmin() bool {
|
||||
isSysAdmin := false
|
||||
for _, role := range a.Roles {
|
||||
// TODO update the value of role when admiral API is ready
|
||||
if role == sysAdminRole {
|
||||
isSysAdmin = true
|
||||
break
|
||||
|
@ -64,14 +66,14 @@ func (a *AuthContext) IsSysAdmin() bool {
|
|||
}
|
||||
|
||||
// HasReadPerm ...
|
||||
func (a *AuthContext) HasReadPerm(project string) bool {
|
||||
_, exist := a.Projects[project]
|
||||
return exist
|
||||
func (a *AuthContext) HasReadPerm(projectName string) bool {
|
||||
roles := a.getRoles(projectName)
|
||||
return len(roles) > 0
|
||||
}
|
||||
|
||||
// HasWritePerm ...
|
||||
func (a *AuthContext) HasWritePerm(project string) bool {
|
||||
roles, _ := a.Projects[project]
|
||||
func (a *AuthContext) HasWritePerm(projectName string) bool {
|
||||
roles := a.getRoles(projectName)
|
||||
for _, role := range roles {
|
||||
if role == projectAdminRole || role == developerRole {
|
||||
return true
|
||||
|
@ -81,8 +83,8 @@ func (a *AuthContext) HasWritePerm(project string) bool {
|
|||
}
|
||||
|
||||
// HasAllPerm ...
|
||||
func (a *AuthContext) HasAllPerm(project string) bool {
|
||||
roles, _ := a.Projects[project]
|
||||
func (a *AuthContext) HasAllPerm(projectName string) bool {
|
||||
roles := a.getRoles(projectName)
|
||||
for _, role := range roles {
|
||||
if role == projectAdminRole {
|
||||
return true
|
||||
|
@ -91,17 +93,77 @@ func (a *AuthContext) HasAllPerm(project string) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
// GetByToken ...
|
||||
func GetByToken(token string) (*AuthContext, error) {
|
||||
endpoint := config.AdmiralEndpoint()
|
||||
path := strings.TrimRight(endpoint, "/") + "/sso/auth-context"
|
||||
req, err := http.NewRequest(http.MethodGet, path, nil)
|
||||
func (a *AuthContext) getRoles(projectName string) []string {
|
||||
for _, project := range a.Projects {
|
||||
if project.Name == projectName {
|
||||
return project.Roles
|
||||
}
|
||||
}
|
||||
|
||||
return []string{}
|
||||
}
|
||||
|
||||
// GetMyProjects returns all projects which the user is a member of
|
||||
func (a *AuthContext) GetMyProjects() []string {
|
||||
projects := []string{}
|
||||
for _, project := range a.Projects {
|
||||
projects = append(projects, project.Name)
|
||||
}
|
||||
return projects
|
||||
}
|
||||
|
||||
// GetAuthCtx returns the auth context of the current user
|
||||
func GetAuthCtx(client *http.Client, url, token string) (*AuthContext, error) {
|
||||
return get(client, url, token)
|
||||
}
|
||||
|
||||
// GetAuthCtxOfUser returns the auth context of the specific user
|
||||
func GetAuthCtxOfUser(client *http.Client, url, token string, username string) (*AuthContext, error) {
|
||||
return get(client, url, token, username)
|
||||
}
|
||||
|
||||
// get the user's auth context, if the username is not provided
|
||||
// get the default auth context of the token
|
||||
func get(client *http.Client, url, token string, username ...string) (*AuthContext, error) {
|
||||
endpoint := ""
|
||||
if len(username) > 0 && len(username[0]) > 0 {
|
||||
endpoint = buildSpecificUserAuthCtxURL(url, username[0])
|
||||
} else {
|
||||
endpoint = buildCurrentUserAuthCtxURL(url)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, endpoint, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Add(AuthTokenHeader, token)
|
||||
|
||||
return send(client, req)
|
||||
}
|
||||
|
||||
// Login with credential and returns auth context and error
|
||||
func Login(client *http.Client, url, username, password string) (*AuthContext, error) {
|
||||
data, err := json.Marshal(&struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}{
|
||||
Username: username,
|
||||
Password: password,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, buildLoginURL(url), bytes.NewBuffer(data))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return send(client, req)
|
||||
}
|
||||
|
||||
func send(client *http.Client, req *http.Request) (*AuthContext, error) {
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -114,16 +176,27 @@ func GetByToken(token string) (*AuthContext, error) {
|
|||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("failed to get auth context by token: %d %s",
|
||||
resp.StatusCode, string(data))
|
||||
return nil, fmt.Errorf("unexpected status code: %d %s", resp.StatusCode, string(data))
|
||||
}
|
||||
|
||||
ctx := &AuthContext{
|
||||
Projects: make(map[string][]string),
|
||||
}
|
||||
ctx := &AuthContext{}
|
||||
if err = json.Unmarshal(data, ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
func buildCurrentUserAuthCtxURL(url string) string {
|
||||
return strings.TrimRight(url, "/") + "/auth/session"
|
||||
}
|
||||
|
||||
func buildSpecificUserAuthCtxURL(url, principalID string) string {
|
||||
return fmt.Sprintf("%s/auth/idm/principals/%s/security-context",
|
||||
strings.TrimRight(url, "/"), principalID)
|
||||
}
|
||||
|
||||
// TODO update the url
|
||||
func buildLoginURL(url string) string {
|
||||
return strings.TrimRight(url, "/") + "/sso/login"
|
||||
}
|
||||
|
|
|
@ -24,8 +24,8 @@ type Context interface {
|
|||
IsSysAdmin() bool
|
||||
// HasReadPerm returns whether the user has read permission to the project
|
||||
HasReadPerm(projectIDOrName interface{}) bool
|
||||
// HasWritePerm returns whether the user has write permission to the project
|
||||
// HasWritePerm returns whether the user has write permission to the project
|
||||
HasWritePerm(projectIDOrName interface{}) bool
|
||||
// HasAllPerm returns whether the user has all permissions to the project
|
||||
// HasAllPerm returns whether the user has all permissions to the project
|
||||
HasAllPerm(projectIDOrName interface{}) bool
|
||||
}
|
||||
|
|
|
@ -67,7 +67,7 @@ func (s *SecurityContext) HasReadPerm(projectIDOrName interface{}) bool {
|
|||
if s.store == nil {
|
||||
return false
|
||||
}
|
||||
return s.store.GetUsername(s.secret) == secret.JobserviceUser
|
||||
return s.store.GetUsername(s.secret) == secret.JobserviceUser || s.store.GetUsername(s.secret) == secret.UIUser
|
||||
}
|
||||
|
||||
// HasWritePerm always returns false
|
||||
|
|
|
@ -56,8 +56,6 @@ func (c *Client) ScanLayer(l models.ClairLayer) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.logger.Infof("endpoint: %s", c.endpoint)
|
||||
c.logger.Infof("body: %s", string(data))
|
||||
req, err := http.NewRequest("POST", c.endpoint+"/v1/layers", bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -68,13 +66,13 @@ func (c *Client) ScanLayer(l models.ClairLayer) error {
|
|||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
b, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.logger.Infof("response code: %d", resp.StatusCode)
|
||||
if resp.StatusCode != http.StatusCreated {
|
||||
c.logger.Warningf("Unexpected status code: %d", resp.StatusCode)
|
||||
b, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fmt.Errorf("Unexpected status code: %d, text: %s", resp.StatusCode, string(b))
|
||||
}
|
||||
c.logger.Infof("Returning.")
|
||||
|
|
|
@ -15,20 +15,13 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
//"github.com/vmware/harbor/src/common/config"
|
||||
"github.com/vmware/harbor/src/common/models"
|
||||
"github.com/vmware/harbor/src/common/utils/log"
|
||||
"github.com/vmware/harbor/src/common/utils/registry"
|
||||
registry_error "github.com/vmware/harbor/src/common/utils/error"
|
||||
token_util "github.com/vmware/harbor/src/ui/service/token"
|
||||
)
|
||||
|
||||
|
@ -36,13 +29,14 @@ const (
|
|||
latency int = 10 //second, the network latency when token is received
|
||||
)
|
||||
|
||||
type scope struct {
|
||||
// Scope ...
|
||||
type Scope struct {
|
||||
Type string
|
||||
Name string
|
||||
Actions []string
|
||||
}
|
||||
|
||||
func (s *scope) string() string {
|
||||
func (s *Scope) string() string {
|
||||
return fmt.Sprintf("%s:%s:%s", s.Type, s.Name, strings.Join(s.Actions, ","))
|
||||
}
|
||||
|
||||
|
@ -50,7 +44,7 @@ type tokenGenerator func(realm, service string, scopes []string) (token string,
|
|||
|
||||
// Implements interface Authorizer
|
||||
type tokenAuthorizer struct {
|
||||
scope *scope
|
||||
scope *Scope
|
||||
tg tokenGenerator
|
||||
cache string // cached token
|
||||
expiresAt *time.Time // The UTC standard time at when the token will expire
|
||||
|
@ -64,13 +58,13 @@ func (t *tokenAuthorizer) Scheme() string {
|
|||
|
||||
// AuthorizeRequest will add authorization header which contains a token before the request is sent
|
||||
func (t *tokenAuthorizer) Authorize(req *http.Request, params map[string]string) error {
|
||||
var scopes []*scope
|
||||
var scopes []*Scope
|
||||
var token string
|
||||
|
||||
hasFrom := false
|
||||
from := req.URL.Query().Get("from")
|
||||
if len(from) != 0 {
|
||||
s := &scope{
|
||||
s := &Scope{
|
||||
Type: "repository",
|
||||
Name: from,
|
||||
Actions: []string{"pull"},
|
||||
|
@ -154,7 +148,7 @@ func NewStandardTokenAuthorizer(credential Credential, insecure bool,
|
|||
}
|
||||
|
||||
if len(scopeType) != 0 || len(scopeName) != 0 {
|
||||
authorizer.scope = &scope{
|
||||
authorizer.scope = &Scope{
|
||||
Type: scopeType,
|
||||
Name: scopeName,
|
||||
Actions: scopeActions,
|
||||
|
@ -166,66 +160,21 @@ func NewStandardTokenAuthorizer(credential Credential, insecure bool,
|
|||
return authorizer
|
||||
}
|
||||
|
||||
func (s *standardTokenAuthorizer) generateToken(realm, service string, scopes []string) (token string, expiresIn int, issuedAt *time.Time, err error) {
|
||||
func (s *standardTokenAuthorizer) generateToken(realm, service string, scopes []string) (string, int, *time.Time, error) {
|
||||
realm = s.tokenURL(realm)
|
||||
tk, err := getToken(s.client, s.credential, realm,
|
||||
service, scopes)
|
||||
|
||||
u, err := url.Parse(realm)
|
||||
if len(tk.IssuedAt) == 0 {
|
||||
return tk.Token, tk.ExpiresIn, nil, nil
|
||||
}
|
||||
|
||||
issuedAt, err := time.Parse(time.RFC3339, tk.IssuedAt)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
q := u.Query()
|
||||
q.Add("service", service)
|
||||
for _, scope := range scopes {
|
||||
q.Add("scope", scope)
|
||||
}
|
||||
u.RawQuery = q.Encode()
|
||||
r, err := http.NewRequest("GET", u.String(), nil)
|
||||
if err != nil {
|
||||
return
|
||||
return "", 0, nil, err
|
||||
}
|
||||
|
||||
if s.credential != nil {
|
||||
s.credential.AddAuthorization(r)
|
||||
}
|
||||
|
||||
resp, err := s.client.Do(r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
b, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
err = ®istry_error.Error{
|
||||
StatusCode: resp.StatusCode,
|
||||
Detail: string(b),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
tk := models.Token{}
|
||||
if err = json.Unmarshal(b, &tk); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
token = tk.Token
|
||||
|
||||
expiresIn = tk.ExpiresIn
|
||||
|
||||
if len(tk.IssuedAt) != 0 {
|
||||
t, err := time.Parse(time.RFC3339, tk.IssuedAt)
|
||||
if err != nil {
|
||||
log.Errorf("error occurred while parsing issued_at: %v", err)
|
||||
err = nil
|
||||
} else {
|
||||
issuedAt = &t
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
return tk.Token, tk.ExpiresIn, &issuedAt, nil
|
||||
}
|
||||
|
||||
// when the registry client is used inside Harbor, the token request
|
||||
|
@ -267,7 +216,7 @@ func newUsernameTokenAuthorizer(notary bool, username, scopeType, scopeName stri
|
|||
username: username,
|
||||
}
|
||||
|
||||
authorizer.scope = &scope{
|
||||
authorizer.scope = &Scope{
|
||||
Type: scopeType,
|
||||
Name: scopeName,
|
||||
Actions: scopeActions,
|
||||
|
|
93
src/common/utils/registry/auth/util.go
Normal file
93
src/common/utils/registry/auth/util.go
Normal file
|
@ -0,0 +1,93 @@
|
|||
// Copyright (c) 2017 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"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/vmware/harbor/src/common/models"
|
||||
|
||||
registry_error "github.com/vmware/harbor/src/common/utils/error"
|
||||
"github.com/vmware/harbor/src/common/utils/registry"
|
||||
)
|
||||
|
||||
const (
|
||||
service = "harbor-registry"
|
||||
)
|
||||
|
||||
// GetToken requests a token against the endpoint using credetial provided
|
||||
func GetToken(endpoint string, insecure bool, credential Credential,
|
||||
scopes []*Scope) (*models.Token, error) {
|
||||
client := &http.Client{
|
||||
Transport: registry.GetHTTPTransport(insecure),
|
||||
}
|
||||
|
||||
scopesStr := []string{}
|
||||
for _, scope := range scopes {
|
||||
scopesStr = append(scopesStr, scope.string())
|
||||
}
|
||||
|
||||
return getToken(client, credential, endpoint, service, scopesStr)
|
||||
}
|
||||
|
||||
func getToken(client *http.Client, credential Credential, realm, service string,
|
||||
scopes []string) (*models.Token, error) {
|
||||
u, err := url.Parse(realm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
query := u.Query()
|
||||
query.Add("service", service)
|
||||
for _, scope := range scopes {
|
||||
query.Add("scope", scope)
|
||||
}
|
||||
u.RawQuery = query.Encode()
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, u.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if credential != nil {
|
||||
credential.AddAuthorization(req)
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
data, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, ®istry_error.Error{
|
||||
StatusCode: resp.StatusCode,
|
||||
Detail: string(data),
|
||||
}
|
||||
}
|
||||
|
||||
token := &models.Token{}
|
||||
if err = json.Unmarshal(data, token); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
|
@ -22,6 +22,7 @@ import (
|
|||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
// "time"
|
||||
|
@ -106,11 +107,19 @@ func (r *Repository) ListTag() ([]string, error) {
|
|||
if err := json.Unmarshal(b, &tagsResp); err != nil {
|
||||
return tags, err
|
||||
}
|
||||
|
||||
sort.Strings(tags)
|
||||
tags = tagsResp.Tags
|
||||
|
||||
return tags, nil
|
||||
} else if resp.StatusCode == http.StatusNotFound {
|
||||
|
||||
// TODO remove the logic if the bug of registry is fixed
|
||||
// It's a workaround for a bug of registry: when listing tags of
|
||||
// a repository which is being pushed, a "NAME_UNKNOWN" error will
|
||||
// been returned, while the catalog API can list this repository.
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
return tags, ®istry_error.Error{
|
||||
StatusCode: resp.StatusCode,
|
||||
Detail: string(b),
|
||||
|
|
|
@ -27,7 +27,6 @@ import (
|
|||
"github.com/docker/distribution"
|
||||
"github.com/docker/distribution/manifest/schema1"
|
||||
"github.com/docker/distribution/manifest/schema2"
|
||||
"github.com/vmware/harbor/src/common/dao"
|
||||
"github.com/vmware/harbor/src/common/models"
|
||||
comutils "github.com/vmware/harbor/src/common/utils"
|
||||
"github.com/vmware/harbor/src/common/utils/log"
|
||||
|
@ -182,13 +181,13 @@ func (c *Checker) Enter() (string, error) {
|
|||
}
|
||||
|
||||
func (c *Checker) enter() (string, error) {
|
||||
project, err := dao.GetProjectByName(c.project)
|
||||
project, err := getProject(c.project)
|
||||
if err != nil {
|
||||
c.logger.Errorf("an error occurred while getting project %s in DB: %v", c.project, err)
|
||||
c.logger.Errorf("failed to get project %s from %s: %v", c.project, c.srcURL, err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = c.createProject(project.Public)
|
||||
err = c.createProject(project)
|
||||
if err == nil {
|
||||
c.logger.Infof("project %s is created on %s with user %s", c.project, c.dstURL, c.dstUsr)
|
||||
return StatePullManifest, nil
|
||||
|
@ -207,16 +206,61 @@ func (c *Checker) enter() (string, error) {
|
|||
return "", err
|
||||
}
|
||||
|
||||
func (c *Checker) createProject(public int) error {
|
||||
project := struct {
|
||||
ProjectName string `json:"project_name"`
|
||||
Public int `json:"public"`
|
||||
}{
|
||||
ProjectName: c.project,
|
||||
Public: public,
|
||||
func getProject(name string) (*models.Project, error) {
|
||||
req, err := http.NewRequest(http.MethodGet, buildProjectURL(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data, err := json.Marshal(project)
|
||||
req.URL.Query().Set("name", name)
|
||||
req.URL.Query().Encode()
|
||||
req.AddCookie(&http.Cookie{
|
||||
Name: models.UISecretCookie,
|
||||
Value: config.JobserviceSecret(),
|
||||
})
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
data, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
list := []*models.Project{}
|
||||
if err = json.Unmarshal(data, &list); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var project *models.Project
|
||||
for _, p := range list {
|
||||
if p.Name == name {
|
||||
project = p
|
||||
break
|
||||
}
|
||||
}
|
||||
if project == nil {
|
||||
return nil, fmt.Errorf("project %s not found", name)
|
||||
}
|
||||
|
||||
return project, nil
|
||||
}
|
||||
|
||||
func (c *Checker) createProject(project *models.Project) error {
|
||||
pro := &models.ProjectRequest{
|
||||
Name: project.Name,
|
||||
Public: project.Public,
|
||||
EnableContentTrust: project.EnableContentTrust,
|
||||
PreventVulnerableImagesFromRunning: project.PreventVulnerableImagesFromRunning,
|
||||
PreventVulnerableImagesFromRunningSeverity: project.PreventVulnerableImagesFromRunningSeverity,
|
||||
AutomaticallyScanImagesOnPush: project.AutomaticallyScanImagesOnPush,
|
||||
}
|
||||
|
||||
data, err := json.Marshal(pro)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -263,6 +307,10 @@ func (c *Checker) createProject(public int) error {
|
|||
c.project, c.dstURL, c.dstUsr, resp.StatusCode, string(message))
|
||||
}
|
||||
|
||||
func buildProjectURL() string {
|
||||
return strings.TrimRight(config.LocalUIURL(), "/") + "/api/projects/"
|
||||
}
|
||||
|
||||
// ManifestPuller pulls the manifest of a tag. And if no tag needs to be pulled,
|
||||
// the next state that state machine should enter is "finished".
|
||||
type ManifestPuller struct {
|
||||
|
|
|
@ -15,11 +15,8 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/vmware/harbor/src/common/models"
|
||||
"github.com/vmware/harbor/src/common/utils/registry"
|
||||
|
@ -64,38 +61,18 @@ func BuildBlobURL(endpoint, repository, digest string) string {
|
|||
return fmt.Sprintf("%s/v2/%s/blobs/%s", endpoint, repository, digest)
|
||||
}
|
||||
|
||||
//GetTokenForRepo is a temp solution for job handler to get a token for clair.
|
||||
//GetTokenForRepo is used for job handler to get a token for clair.
|
||||
func GetTokenForRepo(repository string) (string, error) {
|
||||
u, err := url.Parse(config.InternalTokenServiceEndpoint())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
q := u.Query()
|
||||
q.Add("service", "harbor-registry")
|
||||
q.Add("scope", fmt.Sprintf("repository:%s:pull", repository))
|
||||
u.RawQuery = q.Encode()
|
||||
r, err := http.NewRequest("GET", u.String(), nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
c := &http.Cookie{Name: models.UISecretCookie, Value: config.JobserviceSecret()}
|
||||
r.AddCookie(c)
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(r)
|
||||
credentail := auth.NewCookieCredential(c)
|
||||
token, err := auth.GetToken(config.InternalTokenServiceEndpoint(), true, credentail, []*auth.Scope{&auth.Scope{
|
||||
Type: "repository",
|
||||
Name: repository,
|
||||
Actions: []string{"pull"},
|
||||
}})
|
||||
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 "", fmt.Errorf("Unexpected response from token service, code: %d, %s", resp.StatusCode, string(b))
|
||||
}
|
||||
tk := models.Token{}
|
||||
if err := json.Unmarshal(b, &tk); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return tk.Token, nil
|
||||
|
||||
return token.Token, nil
|
||||
}
|
||||
|
|
|
@ -17,10 +17,11 @@ package api
|
|||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"reflect"
|
||||
|
||||
"github.com/vmware/harbor/src/common"
|
||||
"github.com/vmware/harbor/src/common/dao"
|
||||
"github.com/vmware/harbor/src/common/models"
|
||||
"github.com/vmware/harbor/src/common/utils/log"
|
||||
"github.com/vmware/harbor/src/ui/config"
|
||||
)
|
||||
|
@ -62,6 +63,34 @@ var (
|
|||
common.CfgExpiration,
|
||||
common.JobLogDir,
|
||||
common.AdminInitialPassword,
|
||||
common.ScanAllPolicy,
|
||||
}
|
||||
|
||||
stringKeys = []string{
|
||||
common.ExtEndpoint,
|
||||
common.AUTHMode,
|
||||
common.DatabaseType,
|
||||
common.MySQLHost,
|
||||
common.MySQLUsername,
|
||||
common.MySQLPassword,
|
||||
common.MySQLDatabase,
|
||||
common.SQLiteFile,
|
||||
common.LDAPURL,
|
||||
common.LDAPSearchDN,
|
||||
common.LDAPSearchPwd,
|
||||
common.LDAPBaseDN,
|
||||
common.LDAPUID,
|
||||
common.LDAPFilter,
|
||||
common.TokenServiceURL,
|
||||
common.RegistryURL,
|
||||
common.EmailHost,
|
||||
common.EmailUsername,
|
||||
common.EmailPassword,
|
||||
common.EmailFrom,
|
||||
common.EmailIdentity,
|
||||
common.ProjectCreationRestriction,
|
||||
common.JobLogDir,
|
||||
common.AdminInitialPassword,
|
||||
}
|
||||
|
||||
numKeys = []string{
|
||||
|
@ -131,10 +160,10 @@ func (c *ConfigAPI) Get() {
|
|||
|
||||
// Put updates configurations
|
||||
func (c *ConfigAPI) Put() {
|
||||
m := map[string]string{}
|
||||
m := map[string]interface{}{}
|
||||
c.DecodeJSONReq(&m)
|
||||
|
||||
cfg := map[string]string{}
|
||||
cfg := map[string]interface{}{}
|
||||
for _, k := range validKeys {
|
||||
if v, ok := m[k]; ok {
|
||||
cfg[k] = v
|
||||
|
@ -152,35 +181,7 @@ func (c *ConfigAPI) Put() {
|
|||
c.CustomAbort(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
if value, ok := cfg[common.AUTHMode]; ok {
|
||||
mode, err := config.AuthMode()
|
||||
if err != nil {
|
||||
log.Errorf("failed to get auth mode: %v", err)
|
||||
c.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
||||
}
|
||||
|
||||
if mode != value {
|
||||
flag, err := authModeCanBeModified()
|
||||
if err != nil {
|
||||
log.Errorf("failed to determine whether auth mode can be modified: %v", err)
|
||||
c.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
||||
}
|
||||
|
||||
if !flag {
|
||||
c.CustomAbort(http.StatusBadRequest,
|
||||
fmt.Sprintf("%s can not be modified as new users have been inserted into database",
|
||||
common.AUTHMode))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result, err := convertForPut(cfg)
|
||||
if err != nil {
|
||||
log.Errorf("failed to convert configurations: %v", err)
|
||||
c.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
||||
}
|
||||
|
||||
if err := config.Upload(result); err != nil {
|
||||
if err := config.Upload(cfg); err != nil {
|
||||
log.Errorf("failed to upload configurations: %v", err)
|
||||
c.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
||||
}
|
||||
|
@ -199,18 +200,53 @@ func (c *ConfigAPI) Reset() {
|
|||
}
|
||||
}
|
||||
|
||||
func validateCfg(c map[string]string) (bool, error) {
|
||||
isSysErr := false
|
||||
func validateCfg(c map[string]interface{}) (bool, error) {
|
||||
strMap := map[string]string{}
|
||||
for _, k := range stringKeys {
|
||||
if _, ok := c[k]; !ok {
|
||||
continue
|
||||
}
|
||||
if _, ok := c[k].(string); !ok {
|
||||
return false, fmt.Errorf("Invalid value type, expected string, key: %s, value: %v, type: %v", k, c[k], reflect.TypeOf(c[k]))
|
||||
}
|
||||
strMap[k] = c[k].(string)
|
||||
}
|
||||
numMap := map[string]int{}
|
||||
for _, k := range numKeys {
|
||||
if _, ok := c[k]; !ok {
|
||||
continue
|
||||
}
|
||||
if _, ok := c[k].(float64); !ok {
|
||||
return false, fmt.Errorf("Invalid value type, expected float64, key: %s, value: %v, type: %v", k, c[k], reflect.TypeOf(c[k]))
|
||||
}
|
||||
numMap[k] = int(c[k].(float64))
|
||||
}
|
||||
boolMap := map[string]bool{}
|
||||
for _, k := range boolKeys {
|
||||
if _, ok := c[k]; !ok {
|
||||
continue
|
||||
}
|
||||
if _, ok := c[k].(bool); !ok {
|
||||
return false, fmt.Errorf("Invalid value type, expected bool, key: %s, value: %v, type: %v", k, c[k], reflect.TypeOf(c[k]))
|
||||
}
|
||||
boolMap[k] = c[k].(bool)
|
||||
}
|
||||
|
||||
mode, err := config.AuthMode()
|
||||
if err != nil {
|
||||
isSysErr = true
|
||||
return isSysErr, err
|
||||
return true, err
|
||||
}
|
||||
|
||||
if value, ok := c[common.AUTHMode]; ok {
|
||||
if value, ok := strMap[common.AUTHMode]; ok {
|
||||
if value != common.DBAuth && value != common.LDAPAuth {
|
||||
return isSysErr, fmt.Errorf("invalid %s, shoud be %s or %s", common.AUTHMode, common.DBAuth, common.LDAPAuth)
|
||||
return false, fmt.Errorf("invalid %s, shoud be %s or %s", common.AUTHMode, common.DBAuth, common.LDAPAuth)
|
||||
}
|
||||
flag, err := authModeCanBeModified()
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
if mode != value && !flag {
|
||||
return false, fmt.Errorf("%s can not be modified as new users have been inserted into database", common.AUTHMode)
|
||||
}
|
||||
mode = value
|
||||
}
|
||||
|
@ -218,123 +254,70 @@ func validateCfg(c map[string]string) (bool, error) {
|
|||
if mode == common.LDAPAuth {
|
||||
ldap, err := config.LDAP()
|
||||
if err != nil {
|
||||
isSysErr = true
|
||||
return isSysErr, err
|
||||
return true, err
|
||||
}
|
||||
|
||||
if len(ldap.URL) == 0 {
|
||||
if _, ok := c[common.LDAPURL]; !ok {
|
||||
return isSysErr, fmt.Errorf("%s is missing", common.LDAPURL)
|
||||
if _, ok := strMap[common.LDAPURL]; !ok {
|
||||
return false, fmt.Errorf("%s is missing", common.LDAPURL)
|
||||
}
|
||||
}
|
||||
|
||||
if len(ldap.BaseDN) == 0 {
|
||||
if _, ok := c[common.LDAPBaseDN]; !ok {
|
||||
return isSysErr, fmt.Errorf("%s is missing", common.LDAPBaseDN)
|
||||
if _, ok := strMap[common.LDAPBaseDN]; !ok {
|
||||
return false, fmt.Errorf("%s is missing", common.LDAPBaseDN)
|
||||
}
|
||||
}
|
||||
if len(ldap.UID) == 0 {
|
||||
if _, ok := c[common.LDAPUID]; !ok {
|
||||
return isSysErr, fmt.Errorf("%s is missing", common.LDAPUID)
|
||||
if _, ok := strMap[common.LDAPUID]; !ok {
|
||||
return false, fmt.Errorf("%s is missing", common.LDAPUID)
|
||||
}
|
||||
}
|
||||
if ldap.Scope == 0 {
|
||||
if _, ok := c[common.LDAPScope]; !ok {
|
||||
return isSysErr, fmt.Errorf("%s is missing", common.LDAPScope)
|
||||
if _, ok := numMap[common.LDAPScope]; !ok {
|
||||
return false, fmt.Errorf("%s is missing", common.LDAPScope)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ldapURL, ok := c[common.LDAPURL]; ok && len(ldapURL) == 0 {
|
||||
return isSysErr, fmt.Errorf("%s is empty", common.LDAPURL)
|
||||
if ldapURL, ok := strMap[common.LDAPURL]; ok && len(ldapURL) == 0 {
|
||||
return false, fmt.Errorf("%s is empty", common.LDAPURL)
|
||||
}
|
||||
if baseDN, ok := c[common.LDAPBaseDN]; ok && len(baseDN) == 0 {
|
||||
return isSysErr, fmt.Errorf("%s is empty", common.LDAPBaseDN)
|
||||
if baseDN, ok := strMap[common.LDAPBaseDN]; ok && len(baseDN) == 0 {
|
||||
return false, fmt.Errorf("%s is empty", common.LDAPBaseDN)
|
||||
}
|
||||
if uID, ok := c[common.LDAPUID]; ok && len(uID) == 0 {
|
||||
return isSysErr, fmt.Errorf("%s is empty", common.LDAPUID)
|
||||
if uID, ok := strMap[common.LDAPUID]; ok && len(uID) == 0 {
|
||||
return false, fmt.Errorf("%s is empty", common.LDAPUID)
|
||||
}
|
||||
if scope, ok := c[common.LDAPScope]; ok &&
|
||||
if scope, ok := numMap[common.LDAPScope]; ok &&
|
||||
scope != common.LDAPScopeBase &&
|
||||
scope != common.LDAPScopeOnelevel &&
|
||||
scope != common.LDAPScopeSubtree {
|
||||
return isSysErr, fmt.Errorf("invalid %s, should be %s, %s or %s",
|
||||
return false, fmt.Errorf("invalid %s, should be %s, %s or %s",
|
||||
common.LDAPScope,
|
||||
common.LDAPScopeBase,
|
||||
common.LDAPScopeOnelevel,
|
||||
common.LDAPScopeSubtree)
|
||||
}
|
||||
|
||||
for _, k := range boolKeys {
|
||||
v, ok := c[k]
|
||||
if !ok {
|
||||
continue
|
||||
for k, n := range numMap {
|
||||
if n < 0 {
|
||||
return false, fmt.Errorf("invalid %s: %d", k, n)
|
||||
}
|
||||
|
||||
if v != "0" && v != "1" {
|
||||
return isSysErr, fmt.Errorf("%s should be %s or %s",
|
||||
k, "0", "1")
|
||||
}
|
||||
}
|
||||
|
||||
for _, k := range numKeys {
|
||||
v, ok := c[k]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
n, err := strconv.Atoi(v)
|
||||
if err != nil || n < 0 {
|
||||
return isSysErr, fmt.Errorf("invalid %s: %s", k, v)
|
||||
}
|
||||
|
||||
if (k == common.EmailPort ||
|
||||
k == common.MySQLPort) && n > 65535 {
|
||||
return isSysErr, fmt.Errorf("invalid %s: %s", k, v)
|
||||
return false, fmt.Errorf("invalid %s: %d", k, n)
|
||||
}
|
||||
}
|
||||
|
||||
if crt, ok := c[common.ProjectCreationRestriction]; ok &&
|
||||
if crt, ok := strMap[common.ProjectCreationRestriction]; ok &&
|
||||
crt != common.ProCrtRestrEveryone &&
|
||||
crt != common.ProCrtRestrAdmOnly {
|
||||
return isSysErr, fmt.Errorf("invalid %s, should be %s or %s",
|
||||
return false, fmt.Errorf("invalid %s, should be %s or %s",
|
||||
common.ProjectCreationRestriction,
|
||||
common.ProCrtRestrAdmOnly,
|
||||
common.ProCrtRestrEveryone)
|
||||
}
|
||||
|
||||
return isSysErr, nil
|
||||
}
|
||||
|
||||
//convert map[string]string to map[string]interface{}
|
||||
func convertForPut(m map[string]string) (map[string]interface{}, error) {
|
||||
cfg := map[string]interface{}{}
|
||||
|
||||
for k, v := range m {
|
||||
cfg[k] = v
|
||||
}
|
||||
|
||||
for _, k := range numKeys {
|
||||
if _, ok := cfg[k]; !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
v, err := strconv.Atoi(cfg[k].(string))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg[k] = v
|
||||
}
|
||||
|
||||
for _, k := range boolKeys {
|
||||
if _, ok := cfg[k]; !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
cfg[k] = cfg[k] == "1"
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// delete sensitive attrs and add editable field to every attr
|
||||
|
@ -347,6 +330,9 @@ func convertForGet(cfg map[string]interface{}) (map[string]*value, error) {
|
|||
}
|
||||
}
|
||||
|
||||
if _, ok := cfg[common.ScanAllPolicy]; !ok {
|
||||
cfg[common.ScanAllPolicy] = models.DefaultScanAllPolicy
|
||||
}
|
||||
for k, v := range cfg {
|
||||
result[k] = &value{
|
||||
Value: v,
|
||||
|
|
|
@ -60,8 +60,8 @@ func TestPutConfig(t *testing.T) {
|
|||
assert := assert.New(t)
|
||||
apiTest := newHarborAPI()
|
||||
|
||||
cfg := map[string]string{
|
||||
common.VerifyRemoteCert: "0",
|
||||
cfg := map[string]interface{}{
|
||||
common.VerifyRemoteCert: false,
|
||||
}
|
||||
|
||||
code, err := apiTest.PutConfig(*admin, cfg)
|
||||
|
|
|
@ -1005,7 +1005,7 @@ func (a testapi) GetConfig(authInfo usrInfo) (int, map[string]*value, error) {
|
|||
return code, cfg, err
|
||||
}
|
||||
|
||||
func (a testapi) PutConfig(authInfo usrInfo, cfg map[string]string) (int, error) {
|
||||
func (a testapi) PutConfig(authInfo usrInfo, cfg map[string]interface{}) (int, error) {
|
||||
_sling := sling.New().Base(a.basePath).Put("/api/configurations").BodyJSON(cfg)
|
||||
|
||||
code, _, err := request(_sling, jsonAcceptHeader, authInfo)
|
||||
|
|
|
@ -36,11 +36,6 @@ type ProjectAPI struct {
|
|||
project *models.Project
|
||||
}
|
||||
|
||||
type projectReq struct {
|
||||
ProjectName string `json:"project_name"`
|
||||
Public int `json:"public"`
|
||||
}
|
||||
|
||||
const projectNameMaxLen int = 30
|
||||
const projectNameMinLen int = 2
|
||||
const restrictedNameChars = `[a-z0-9]+(?:[._-][a-z0-9]+)*`
|
||||
|
@ -84,18 +79,24 @@ func (p *ProjectAPI) Post() {
|
|||
p.HandleUnauthorized()
|
||||
return
|
||||
}
|
||||
|
||||
onlyAdmin, err := config.OnlyAdminCreateProject()
|
||||
if err != nil {
|
||||
log.Errorf("failed to determine whether only admin can create projects: %v", err)
|
||||
p.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
||||
var onlyAdmin bool
|
||||
var err error
|
||||
if config.WithAdmiral() {
|
||||
onlyAdmin = true
|
||||
} else {
|
||||
onlyAdmin, err = config.OnlyAdminCreateProject()
|
||||
if err != nil {
|
||||
log.Errorf("failed to determine whether only admin can create projects: %v", err)
|
||||
p.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
||||
}
|
||||
}
|
||||
|
||||
if onlyAdmin && !p.SecurityCtx.IsSysAdmin() {
|
||||
log.Errorf("Only sys admin can create project")
|
||||
p.RenderError(http.StatusForbidden, "Only system admin can create project")
|
||||
return
|
||||
}
|
||||
var pro projectReq
|
||||
var pro *models.ProjectRequest
|
||||
p.DecodeJSONReq(&pro)
|
||||
err = validateProjectReq(pro)
|
||||
if err != nil {
|
||||
|
@ -104,10 +105,10 @@ func (p *ProjectAPI) Post() {
|
|||
return
|
||||
}
|
||||
|
||||
exist, err := p.ProjectMgr.Exist(pro.ProjectName)
|
||||
exist, err := p.ProjectMgr.Exist(pro.Name)
|
||||
if err != nil {
|
||||
p.HandleInternalServerError(fmt.Sprintf("failed to check the existence of project %s: %v",
|
||||
pro.ProjectName, err))
|
||||
pro.Name, err))
|
||||
return
|
||||
}
|
||||
if exist {
|
||||
|
@ -116,9 +117,13 @@ func (p *ProjectAPI) Post() {
|
|||
}
|
||||
|
||||
projectID, err := p.ProjectMgr.Create(&models.Project{
|
||||
Name: pro.ProjectName,
|
||||
Public: pro.Public,
|
||||
OwnerName: p.SecurityCtx.GetUsername(),
|
||||
Name: pro.Name,
|
||||
Public: pro.Public,
|
||||
OwnerName: p.SecurityCtx.GetUsername(),
|
||||
EnableContentTrust: pro.EnableContentTrust,
|
||||
PreventVulnerableImagesFromRunning: pro.PreventVulnerableImagesFromRunning,
|
||||
PreventVulnerableImagesFromRunningSeverity: pro.PreventVulnerableImagesFromRunningSeverity,
|
||||
AutomaticallyScanImagesOnPush: pro.AutomaticallyScanImagesOnPush,
|
||||
})
|
||||
if err != nil {
|
||||
log.Errorf("Failed to add project, error: %v", err)
|
||||
|
@ -136,7 +141,7 @@ func (p *ProjectAPI) Post() {
|
|||
models.AccessLog{
|
||||
Username: p.SecurityCtx.GetUsername(),
|
||||
ProjectID: projectID,
|
||||
RepoName: pro.ProjectName + "/",
|
||||
RepoName: pro.Name + "/",
|
||||
RepoTag: "N/A",
|
||||
Operation: "create",
|
||||
OpTime: time.Now(),
|
||||
|
@ -349,7 +354,7 @@ func (p *ProjectAPI) ToggleProjectPublic() {
|
|||
return
|
||||
}
|
||||
|
||||
var req projectReq
|
||||
var req *models.ProjectRequest
|
||||
p.DecodeJSONReq(&req)
|
||||
if req.Public != 0 && req.Public != 1 {
|
||||
p.HandleBadRequest("public should be 0 or 1")
|
||||
|
@ -431,9 +436,9 @@ func (p *ProjectAPI) Logs() {
|
|||
}
|
||||
|
||||
// TODO move this to package models
|
||||
func validateProjectReq(req projectReq) error {
|
||||
pn := req.ProjectName
|
||||
if isIllegalLength(req.ProjectName, projectNameMinLen, projectNameMaxLen) {
|
||||
func validateProjectReq(req *models.ProjectRequest) error {
|
||||
pn := req.Name
|
||||
if isIllegalLength(req.Name, projectNameMinLen, projectNameMaxLen) {
|
||||
return fmt.Errorf("Project name is illegal in length. (greater than 2 or less than 30)")
|
||||
}
|
||||
validProjectName := regexp.MustCompile(`^` + restrictedNameChars + `$`)
|
||||
|
|
|
@ -25,6 +25,7 @@ import (
|
|||
"github.com/vmware/harbor/src/common/dao"
|
||||
"github.com/vmware/harbor/src/common/models"
|
||||
"github.com/vmware/harbor/src/common/utils/log"
|
||||
"github.com/vmware/harbor/src/ui/utils"
|
||||
)
|
||||
|
||||
// RepJobAPI handles request to /api/replicationJobs /api/replicationJobs/:id/log
|
||||
|
@ -152,7 +153,7 @@ func (ra *RepJobAPI) GetLog() {
|
|||
log.Errorf("failed to create a request: %v", err)
|
||||
ra.CustomAbort(http.StatusInternalServerError, "")
|
||||
}
|
||||
addAuthentication(req)
|
||||
utils.AddUISecret(req)
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
|
|
|
@ -19,7 +19,6 @@ import (
|
|||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/docker/distribution/manifest/schema1"
|
||||
|
@ -33,6 +32,7 @@ import (
|
|||
"github.com/vmware/harbor/src/common/utils/notary"
|
||||
"github.com/vmware/harbor/src/common/utils/registry"
|
||||
"github.com/vmware/harbor/src/ui/config"
|
||||
uiutils "github.com/vmware/harbor/src/ui/utils"
|
||||
)
|
||||
|
||||
// RepositoryAPI handles request to /api/repositories /api/repositories/tags /api/repositories/manifests, the parm has to be put
|
||||
|
@ -370,7 +370,7 @@ func (ra *RepositoryAPI) GetTags() {
|
|||
ra.CustomAbort(http.StatusInternalServerError, "internal error")
|
||||
}
|
||||
|
||||
tags, err := getSimpleTags(client)
|
||||
tags, err := client.ListTag()
|
||||
if err != nil {
|
||||
ra.HandleInternalServerError(fmt.Sprintf("failed to get tag of %s: %v", repoName, err))
|
||||
return
|
||||
|
@ -485,31 +485,6 @@ func getV2Manifest(client *registry.Repository, tag string) (
|
|||
return digest, manifest, config, nil
|
||||
}
|
||||
|
||||
// return tag name list for the repository
|
||||
func getSimpleTags(client *registry.Repository) ([]string, error) {
|
||||
tags := []string{}
|
||||
|
||||
ts, err := client.ListTag()
|
||||
if err != nil {
|
||||
// TODO remove the logic if the bug of registry is fixed
|
||||
// It's a workaround for a bug of registry: when listing tags of
|
||||
// a repository which is being pushed, a "NAME_UNKNOWN" error will
|
||||
// been returned, while the catalog API can list this repository.
|
||||
|
||||
if regErr, ok := err.(*registry_error.Error); ok &&
|
||||
regErr.StatusCode == http.StatusNotFound {
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tags = append(tags, ts...)
|
||||
sort.Strings(tags)
|
||||
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
// GetManifests returns the manifest of a tag
|
||||
func (ra *RepositoryAPI) GetManifests() {
|
||||
repoName := ra.GetString(":splat")
|
||||
|
@ -615,8 +590,8 @@ func (ra *RepositoryAPI) initRepositoryClient(repoName string) (r *registry.Repo
|
|||
return nil, err
|
||||
}
|
||||
|
||||
return NewRepositoryClient(endpoint, true, ra.SecurityCtx.GetUsername(),
|
||||
repoName, "repository", repoName, "pull", "push", "*")
|
||||
return uiutils.NewRepositoryClientForUI(endpoint, true, ra.SecurityCtx.GetUsername(),
|
||||
repoName, "pull", "push", "*")
|
||||
}
|
||||
|
||||
//GetTopRepos returns the most populor repositories
|
||||
|
@ -629,14 +604,14 @@ func (ra *RepositoryAPI) GetTopRepos() {
|
|||
projectIDs := []int64{}
|
||||
projects, err := ra.ProjectMgr.GetPublic()
|
||||
if err != nil {
|
||||
log.Errorf("failed to get the public projects: %v", err)
|
||||
ra.HandleInternalServerError(fmt.Sprintf("failed to get public projects: %v", err))
|
||||
return
|
||||
}
|
||||
if ra.SecurityCtx.IsAuthenticated() {
|
||||
list, err := ra.ProjectMgr.GetByMember(ra.SecurityCtx.GetUsername())
|
||||
if err != nil {
|
||||
log.Errorf("failed to get projects which the user %s is a member of: %v",
|
||||
ra.SecurityCtx.GetUsername(), err)
|
||||
ra.HandleInternalServerError(fmt.Sprintf("failed to get projects which the user %s is a member of: %v",
|
||||
ra.SecurityCtx.GetUsername(), err))
|
||||
return
|
||||
}
|
||||
projects = append(projects, list...)
|
||||
|
@ -726,7 +701,7 @@ func (ra *RepositoryAPI) ScanImage() {
|
|||
ra.HandleForbidden(ra.SecurityCtx.GetUsername())
|
||||
return
|
||||
}
|
||||
err = TriggerImageScan(repoName, tag)
|
||||
err = uiutils.TriggerImageScan(repoName, tag)
|
||||
//TODO better check existence
|
||||
if err != nil {
|
||||
log.Errorf("Error while calling job service to trigger image scan: %v", err)
|
||||
|
@ -762,20 +737,47 @@ func (ra *RepositoryAPI) VulnerabilityDetails() {
|
|||
ra.HandleForbidden(ra.SecurityCtx.GetUsername())
|
||||
return
|
||||
}
|
||||
res := []*models.VulnerabilityItem{}
|
||||
overview, err := dao.GetImgScanOverview(digest)
|
||||
if err != nil {
|
||||
ra.HandleInternalServerError(fmt.Sprintf("failed to get the scan overview, error: %v", err))
|
||||
return
|
||||
}
|
||||
clairClient := clair.NewClient(config.ClairEndpoint(), nil)
|
||||
log.Debugf("The key for getting details: %s", overview.DetailsKey)
|
||||
details, err := clairClient.GetResult(overview.DetailsKey)
|
||||
if err != nil {
|
||||
ra.HandleInternalServerError(fmt.Sprintf("Failed to get scan details from Clair, error: %v", err))
|
||||
if overview != nil && len(overview.DetailsKey) > 0 {
|
||||
clairClient := clair.NewClient(config.ClairEndpoint(), nil)
|
||||
log.Debugf("The key for getting details: %s", overview.DetailsKey)
|
||||
details, err := clairClient.GetResult(overview.DetailsKey)
|
||||
if err != nil {
|
||||
ra.HandleInternalServerError(fmt.Sprintf("Failed to get scan details from Clair, error: %v", err))
|
||||
return
|
||||
}
|
||||
res = transformVulnerabilities(details)
|
||||
}
|
||||
ra.Data["json"] = res
|
||||
ra.ServeJSON()
|
||||
}
|
||||
|
||||
// ScanAll handles the api to scan all images on Harbor.
|
||||
func (ra *RepositoryAPI) ScanAll() {
|
||||
if !config.WithClair() {
|
||||
log.Warningf("Harbor is not deployed with Clair, it's not possible to scan images.")
|
||||
ra.RenderError(http.StatusServiceUnavailable, "")
|
||||
return
|
||||
}
|
||||
ra.Data["json"] = transformVulnerabilities(details)
|
||||
ra.ServeJSON()
|
||||
if !ra.SecurityCtx.IsAuthenticated() {
|
||||
ra.HandleUnauthorized()
|
||||
return
|
||||
}
|
||||
if !ra.SecurityCtx.IsSysAdmin() {
|
||||
ra.HandleForbidden(ra.SecurityCtx.GetUsername())
|
||||
return
|
||||
}
|
||||
if err := uiutils.ScanAllImages(); err != nil {
|
||||
log.Errorf("Failed triggering scan all images, error: %v", err)
|
||||
ra.HandleInternalServerError(fmt.Sprintf("Error: %v", err))
|
||||
return
|
||||
}
|
||||
ra.Ctx.ResponseWriter.WriteHeader(http.StatusAccepted)
|
||||
}
|
||||
|
||||
func getSignatures(repository, username string) (map[string]*notary.Target, error) {
|
||||
|
|
|
@ -26,6 +26,7 @@ import (
|
|||
"github.com/vmware/harbor/src/common/utils"
|
||||
"github.com/vmware/harbor/src/common/utils/log"
|
||||
"github.com/vmware/harbor/src/ui/config"
|
||||
uiutils "github.com/vmware/harbor/src/ui/utils"
|
||||
)
|
||||
|
||||
// SearchAPI handles requesst to /api/search
|
||||
|
@ -157,13 +158,13 @@ func getTags(repository string) ([]string, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
client, err := NewRepositoryClient(url, true,
|
||||
"admin", repository, "repository", repository, "pull")
|
||||
client, err := uiutils.NewRepositoryClientForUI(url, true,
|
||||
"admin", repository, "pull")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tags, err := getSimpleTags(client)
|
||||
tags, err := client.ListTag()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -24,14 +24,14 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
// MPC : count of my projects
|
||||
MPC = "my_project_count"
|
||||
// MRC : count of my repositories
|
||||
MRC = "my_repo_count"
|
||||
// PPC : count of public projects
|
||||
PPC = "public_project_count"
|
||||
// PRC : count of public repositories
|
||||
PRC = "public_repo_count"
|
||||
// PriPC : count of private projects
|
||||
PriPC = "private_project_count"
|
||||
// PriRC : count of private repositories
|
||||
PriRC = "private_repo_count"
|
||||
// PubPC : count of public projects
|
||||
PubPC = "public_project_count"
|
||||
// PubRC : count of public repositories
|
||||
PubRC = "public_repo_count"
|
||||
// TPC : total count of projects
|
||||
TPC = "total_project_count"
|
||||
// TRC : total count of repositories
|
||||
|
@ -57,17 +57,17 @@ func (s *StatisticAPI) Prepare() {
|
|||
// Get total projects and repos of the user
|
||||
func (s *StatisticAPI) Get() {
|
||||
statistic := map[string]int64{}
|
||||
projects, err := s.ProjectMgr.GetPublic()
|
||||
pubProjs, err := s.ProjectMgr.GetPublic()
|
||||
if err != nil {
|
||||
s.HandleInternalServerError(fmt.Sprintf(
|
||||
"failed to get public projects: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
statistic[PPC] = (int64)(len(projects))
|
||||
statistic[PubPC] = (int64)(len(pubProjs))
|
||||
|
||||
ids := []int64{}
|
||||
for _, p := range projects {
|
||||
for _, p := range pubProjs {
|
||||
ids = append(ids, p.ProjectID)
|
||||
}
|
||||
n, err := dao.GetTotalOfRepositoriesByProject(ids, "")
|
||||
|
@ -75,7 +75,7 @@ func (s *StatisticAPI) Get() {
|
|||
log.Errorf("failed to get total of public repositories: %v", err)
|
||||
s.CustomAbort(http.StatusInternalServerError, "")
|
||||
}
|
||||
statistic[PRC] = n
|
||||
statistic[PubRC] = n
|
||||
|
||||
if s.SecurityCtx.IsSysAdmin() {
|
||||
n, err := dao.GetTotalOfProjects(nil)
|
||||
|
@ -83,19 +83,21 @@ func (s *StatisticAPI) Get() {
|
|||
log.Errorf("failed to get total of projects: %v", err)
|
||||
s.CustomAbort(http.StatusInternalServerError, "")
|
||||
}
|
||||
statistic[MPC] = n
|
||||
statistic[TPC] = n
|
||||
statistic[PriPC] = n - statistic[PubPC]
|
||||
|
||||
n, err = dao.GetTotalOfRepositories("")
|
||||
if err != nil {
|
||||
log.Errorf("failed to get total of repositories: %v", err)
|
||||
s.CustomAbort(http.StatusInternalServerError, "")
|
||||
}
|
||||
statistic[MRC] = n
|
||||
statistic[TRC] = n
|
||||
statistic[PriRC] = n - statistic[PubRC]
|
||||
} else {
|
||||
value := false
|
||||
projects, err := s.ProjectMgr.GetAll(&models.ProjectQueryParam{
|
||||
Member: &models.Member{
|
||||
Public: &value,
|
||||
Member: &models.MemberQuery{
|
||||
Name: s.username,
|
||||
},
|
||||
})
|
||||
|
@ -104,7 +106,8 @@ func (s *StatisticAPI) Get() {
|
|||
"failed to get projects of user %s: %v", s.username, err))
|
||||
return
|
||||
}
|
||||
statistic[MPC] = (int64)(len(projects))
|
||||
|
||||
statistic[PriPC] = (int64)(len(projects))
|
||||
|
||||
ids := []int64{}
|
||||
for _, p := range projects {
|
||||
|
@ -118,7 +121,7 @@ func (s *StatisticAPI) Get() {
|
|||
s.username, err))
|
||||
return
|
||||
}
|
||||
statistic[MRC] = n
|
||||
statistic[PriRC] = n
|
||||
}
|
||||
|
||||
s.Data["json"] = statistic
|
||||
|
|
|
@ -30,7 +30,7 @@ func TestStatisticGet(t *testing.T) {
|
|||
|
||||
//prepare for test
|
||||
|
||||
var priMyProjectCount, priMyRepoCount int32
|
||||
var privateProjectCount, privateRepoCount int32
|
||||
var priPublicProjectCount, priPublicRepoCount int32
|
||||
var priTotalProjectCount, priTotalRepoCount int32
|
||||
|
||||
|
@ -53,8 +53,8 @@ func TestStatisticGet(t *testing.T) {
|
|||
} else {
|
||||
assert.Equal(httpStatusCode, int(200), "Case 2: Get status info with admin login. (200)")
|
||||
//fmt.Println("pri status data %+v", result)
|
||||
priMyProjectCount = result.MyProjectCount
|
||||
priMyRepoCount = result.MyRepoCount
|
||||
privateProjectCount = result.PrivateProjectCount
|
||||
privateRepoCount = result.PrivateRepoCount
|
||||
priPublicProjectCount = result.PublicProjectCount
|
||||
priPublicRepoCount = result.PublicRepoCount
|
||||
priTotalProjectCount = result.TotalProjectCount
|
||||
|
@ -74,8 +74,8 @@ func TestStatisticGet(t *testing.T) {
|
|||
t.Error("Error while get statistic information", err.Error())
|
||||
t.Log(err)
|
||||
} else {
|
||||
assert.Equal(priMyProjectCount+1, result.MyProjectCount, "MyProjectCount should be +1")
|
||||
assert.Equal(priMyRepoCount+1, result.MyRepoCount, "MyRepoCount should be +1")
|
||||
assert.Equal(privateProjectCount+1, result.PrivateProjectCount, "PrivateProjectCount should be +1")
|
||||
assert.Equal(privateRepoCount, result.PrivateRepoCount)
|
||||
assert.Equal(priPublicProjectCount, result.PublicProjectCount, "PublicProjectCount should be equal")
|
||||
assert.Equal(priPublicRepoCount+1, result.PublicRepoCount, "PublicRepoCount should be +1")
|
||||
assert.Equal(priTotalProjectCount+1, result.TotalProjectCount, "TotalProCount should be +1")
|
||||
|
|
|
@ -18,7 +18,6 @@ import (
|
|||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"sort"
|
||||
|
@ -34,6 +33,7 @@ import (
|
|||
"github.com/vmware/harbor/src/common/utils/registry/auth"
|
||||
"github.com/vmware/harbor/src/ui/config"
|
||||
"github.com/vmware/harbor/src/ui/projectmanager"
|
||||
uiutils "github.com/vmware/harbor/src/ui/utils"
|
||||
)
|
||||
|
||||
//sysadmin has all privileges to all projects
|
||||
|
@ -96,7 +96,7 @@ func TriggerReplication(policyID int64, repository string,
|
|||
}
|
||||
url := buildReplicationURL()
|
||||
|
||||
return requestAsUI("POST", url, bytes.NewBuffer(b), http.StatusOK)
|
||||
return uiutils.RequestAsUI("POST", url, bytes.NewBuffer(b), http.StatusOK)
|
||||
}
|
||||
|
||||
// TriggerReplicationByRepository triggers the replication according to the repository
|
||||
|
@ -140,7 +140,7 @@ func postReplicationAction(policyID int64, acton string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
addAuthentication(req)
|
||||
uiutils.AddUISecret(req)
|
||||
|
||||
client := &http.Client{}
|
||||
|
||||
|
@ -163,15 +163,6 @@ func postReplicationAction(policyID int64, acton string) error {
|
|||
return fmt.Errorf("%d %s", resp.StatusCode, string(b))
|
||||
}
|
||||
|
||||
func addAuthentication(req *http.Request) {
|
||||
if req != nil {
|
||||
req.AddCookie(&http.Cookie{
|
||||
Name: models.UISecretCookie,
|
||||
Value: config.UISecret(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// SyncRegistry syncs the repositories of registry with database.
|
||||
func SyncRegistry(pm projectmanager.ProjectManager) error {
|
||||
|
||||
|
@ -291,8 +282,8 @@ func diffRepos(reposInRegistry []string, reposInDB []string,
|
|||
if err != nil {
|
||||
return needsAdd, needsDel, err
|
||||
}
|
||||
client, err := NewRepositoryClient(endpoint, true,
|
||||
"admin", repoInR, "repository", repoInR, "pull")
|
||||
client, err := uiutils.NewRepositoryClientForUI(endpoint, true,
|
||||
"admin", repoInR, "pull")
|
||||
if err != nil {
|
||||
return needsAdd, needsDel, err
|
||||
}
|
||||
|
@ -316,8 +307,7 @@ func diffRepos(reposInRegistry []string, reposInDB []string,
|
|||
if err != nil {
|
||||
return needsAdd, needsDel, err
|
||||
}
|
||||
client, err := NewRepositoryClient(endpoint, true,
|
||||
"admin", repoInR, "repository", repoInR, "pull")
|
||||
client, err := uiutils.NewRepositoryClientForUI(endpoint, true, "admin", repoInR, "pull")
|
||||
if err != nil {
|
||||
return needsAdd, needsDel, err
|
||||
}
|
||||
|
@ -354,8 +344,7 @@ func diffRepos(reposInRegistry []string, reposInDB []string,
|
|||
log.Errorf("failed to get registry URL: %v", err)
|
||||
continue
|
||||
}
|
||||
client, err := NewRepositoryClient(endpoint, true,
|
||||
"admin", repoInR, "repository", repoInR, "pull")
|
||||
client, err := uiutils.NewRepositoryClientForUI(endpoint, true, "admin", repoInR, "pull")
|
||||
if err != nil {
|
||||
log.Errorf("failed to create repository client: %v", err)
|
||||
continue
|
||||
|
@ -411,11 +400,6 @@ func initRegistryClient() (r *registry.Registry, err error) {
|
|||
return registryClient, nil
|
||||
}
|
||||
|
||||
func buildScanJobURL() string {
|
||||
url := config.InternalJobServiceURL()
|
||||
return fmt.Sprintf("%s/api/jobs/scan", url)
|
||||
}
|
||||
|
||||
func buildReplicationURL() string {
|
||||
url := config.InternalJobServiceURL()
|
||||
return fmt.Sprintf("%s/api/jobs/replication", url)
|
||||
|
@ -482,64 +466,6 @@ func NewRegistryClient(endpoint string, insecure bool, username, scopeType, scop
|
|||
return client, nil
|
||||
}
|
||||
|
||||
// NewRepositoryClient ...
|
||||
// TODO need a registry client which accept a raw token as param
|
||||
func NewRepositoryClient(endpoint string, insecure bool, username, repository, scopeType, scopeName string,
|
||||
scopeActions ...string) (*registry.Repository, error) {
|
||||
|
||||
authorizer := auth.NewRegistryUsernameTokenAuthorizer(username, scopeType, scopeName, scopeActions...)
|
||||
|
||||
store, err := auth.NewAuthorizerStore(endpoint, insecure, authorizer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client, err := registry.NewRepositoryWithModifiers(repository, endpoint, insecure, store)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
// TriggerImageScan triggers an image scan job on jobservice.
|
||||
func TriggerImageScan(repository string, tag string) error {
|
||||
data := &models.ImageScanReq{
|
||||
Repo: repository,
|
||||
Tag: tag,
|
||||
}
|
||||
b, err := json.Marshal(&data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
url := buildScanJobURL()
|
||||
return requestAsUI("POST", url, bytes.NewBuffer(b), http.StatusOK)
|
||||
}
|
||||
|
||||
// Do not use this when you want to handle the response
|
||||
// TODO: add a response handler to replace expectSC *when needed*
|
||||
func requestAsUI(method, url string, body io.Reader, expectSC int) error {
|
||||
req, err := http.NewRequest(method, url, body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
addAuthentication(req)
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != expectSC {
|
||||
b, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fmt.Errorf("Unexpected status code: %d, text: %s", resp.StatusCode, string(b))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// transformVulnerabilities transforms the returned value of Clair API to a list of VulnerabilityItem
|
||||
func transformVulnerabilities(layerWithVuln *models.ClairLayerEnvelope) []*models.VulnerabilityItem {
|
||||
res := []*models.VulnerabilityItem{}
|
||||
|
|
|
@ -15,7 +15,10 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
|
@ -32,8 +35,9 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
defaultKeyPath string = "/etc/ui/key"
|
||||
secretCookieName string = "secret"
|
||||
defaultKeyPath string = "/etc/ui/key"
|
||||
defaultTokenFilePath string = "/etc/ui/service_token"
|
||||
secretCookieName string = "secret"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -45,6 +49,9 @@ var (
|
|||
GlobalProjectMgr projectmanager.ProjectManager
|
||||
mg *comcfg.Manager
|
||||
keyProvider comcfg.KeyProvider
|
||||
// AdmiralClient is initialized only under integration deploy mode
|
||||
// and can be passed to project manager as a parameter
|
||||
AdmiralClient *http.Client
|
||||
)
|
||||
|
||||
// Init configurations
|
||||
|
@ -104,9 +111,25 @@ func initProjectManager() {
|
|||
}
|
||||
|
||||
// integration with admiral
|
||||
// TODO create project manager based on pms using service account
|
||||
log.Info("initializing the project manager based on PMS...")
|
||||
GlobalProjectMgr = pms.NewProjectManager(AdmiralEndpoint(), "")
|
||||
// TODO read ca/cert file and pass it to the TLS config
|
||||
AdmiralClient = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
path := os.Getenv("SERVICE_TOKEN_FILE_PATH")
|
||||
if len(path) == 0 {
|
||||
path = defaultTokenFilePath
|
||||
}
|
||||
log.Infof("service token file path: %s", path)
|
||||
GlobalProjectMgr = pms.NewProjectManager(AdmiralClient,
|
||||
AdmiralEndpoint(), &pms.FileTokenReader{
|
||||
Path: path,
|
||||
})
|
||||
}
|
||||
|
||||
// Load configurations
|
||||
|
@ -339,7 +362,7 @@ func ClairEndpoint() 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")
|
||||
log.Errorf("Failed to get configuration, will return empty string as admiral's endpoint, error: %v", err)
|
||||
|
||||
return ""
|
||||
}
|
||||
|
@ -349,6 +372,30 @@ func AdmiralEndpoint() string {
|
|||
return cfg[common.AdmiralEndpoint].(string)
|
||||
}
|
||||
|
||||
// ScanAllPolicy returns the policy which controls the scan all.
|
||||
func ScanAllPolicy() models.ScanAllPolicy {
|
||||
var res models.ScanAllPolicy
|
||||
cfg, err := mg.Get()
|
||||
if err != nil {
|
||||
log.Errorf("Failed to get configuration, will return default scan all policy, error: %v", err)
|
||||
return models.DefaultScanAllPolicy
|
||||
}
|
||||
v, ok := cfg[common.ScanAllPolicy]
|
||||
if !ok {
|
||||
return models.DefaultScanAllPolicy
|
||||
}
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to Marshal the value in configuration for Scan All policy, error: %v, returning the default policy", err)
|
||||
return models.DefaultScanAllPolicy
|
||||
}
|
||||
if err := json.Unmarshal(b, &res); err != nil {
|
||||
log.Errorf("Failed to unmarshal the value in configuration for Scan All policy, error: %v, returning the default policy", err)
|
||||
return models.DefaultScanAllPolicy
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// WithAdmiral returns a bool to indicate if Harbor's deployed with admiral.
|
||||
func WithAdmiral() bool {
|
||||
return len(AdmiralEndpoint()) > 0
|
||||
|
|
|
@ -155,4 +155,9 @@ func TestConfig(t *testing.T) {
|
|||
if mode != "db_auth" {
|
||||
t.Errorf("unexpected mode: %s != %s", mode, "db_auth")
|
||||
}
|
||||
|
||||
if s := ScanAllPolicy(); s.Type != "daily" {
|
||||
t.Errorf("unexpected scan all policy %v", s)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -52,6 +52,7 @@ func Init() {
|
|||
reqCtxModifiers = []ReqCtxModifier{
|
||||
&secretReqCtxModifier{config.SecretStore},
|
||||
&tokenReqCtxModifier{},
|
||||
&basicAuthReqCtxModifier{},
|
||||
&unauthorizedReqCtxModifier{}}
|
||||
return
|
||||
}
|
||||
|
@ -123,7 +124,37 @@ func (b *basicAuthReqCtxModifier) Modify(ctx *beegoctx.Context) bool {
|
|||
if !ok {
|
||||
return false
|
||||
}
|
||||
log.Debug("got user information via basic auth")
|
||||
|
||||
// integration with admiral
|
||||
if config.WithAdmiral() {
|
||||
// Can't get a token from Admiral's login API, we can only
|
||||
// create a project manager with the token of the solution user.
|
||||
// That way may cause some wrong permission promotion in some API
|
||||
// calls, so we just handle the requests which are necessary
|
||||
if !filterReq(ctx.Request) {
|
||||
log.Debugf("basic auth is not supported for request %s %s, skip",
|
||||
ctx.Request.Method, ctx.Request.URL.Path)
|
||||
return false
|
||||
}
|
||||
|
||||
authCtx, err := authcontext.Login(config.AdmiralClient,
|
||||
config.AdmiralEndpoint(), username, password)
|
||||
if err != nil {
|
||||
log.Errorf("failed to authenticate %s: %v", username, err)
|
||||
return false
|
||||
}
|
||||
|
||||
log.Debug("using global project manager...")
|
||||
pm := config.GlobalProjectMgr
|
||||
log.Debug("creating admiral security context...")
|
||||
securCtx := admiral.NewSecurityContext(authCtx, pm)
|
||||
|
||||
setSecurCtxAndPM(ctx.Request, securCtx, pm)
|
||||
return true
|
||||
}
|
||||
|
||||
// standalone
|
||||
user, err := auth.Login(models.AuthModel{
|
||||
Principal: username,
|
||||
Password: password,
|
||||
|
@ -133,30 +164,27 @@ func (b *basicAuthReqCtxModifier) Modify(ctx *beegoctx.Context) bool {
|
|||
return false
|
||||
}
|
||||
if user == nil {
|
||||
log.Debug("basic auth user is nil")
|
||||
return false
|
||||
}
|
||||
|
||||
var securCtx security.Context
|
||||
var pm projectmanager.ProjectManager
|
||||
log.Debug("got user information via basic auth")
|
||||
if config.WithAdmiral() {
|
||||
// integration with admiral
|
||||
// we can add logic here to support basic auth in integration mode
|
||||
log.Debug("basic auth isn't supported in integration mode")
|
||||
return false
|
||||
}
|
||||
|
||||
// standalone
|
||||
log.Debug("using local database project manager")
|
||||
pm = config.GlobalProjectMgr
|
||||
pm := config.GlobalProjectMgr
|
||||
log.Debug("creating local database security context...")
|
||||
securCtx = local.NewSecurityContext(user, pm)
|
||||
securCtx := local.NewSecurityContext(user, pm)
|
||||
|
||||
setSecurCtxAndPM(ctx.Request, securCtx, pm)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func filterReq(req *http.Request) bool {
|
||||
path := req.URL.Path
|
||||
if path == "/api/projects" && req.Method == http.MethodPost ||
|
||||
path == "/service/token" && req.Method == http.MethodGet {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type sessionReqCtxModifier struct{}
|
||||
|
||||
func (s *sessionReqCtxModifier) Modify(ctx *beegoctx.Context) bool {
|
||||
|
@ -194,14 +222,18 @@ func (t *tokenReqCtxModifier) Modify(ctx *beegoctx.Context) bool {
|
|||
|
||||
log.Debug("got token from request")
|
||||
|
||||
authContext, err := authcontext.GetByToken(token)
|
||||
authContext, err := authcontext.GetAuthCtx(config.AdmiralClient,
|
||||
config.AdmiralEndpoint(), token)
|
||||
if err != nil {
|
||||
log.Errorf("failed to get auth context: %v", err)
|
||||
return false
|
||||
}
|
||||
|
||||
log.Debug("creating PMS project manager...")
|
||||
pm := pms.NewProjectManager(config.AdmiralEndpoint(), token)
|
||||
pm := pms.NewProjectManager(config.AdmiralClient,
|
||||
config.AdmiralEndpoint(), &pms.RawTokenReader{
|
||||
Token: token,
|
||||
})
|
||||
log.Debug("creating admiral security context...")
|
||||
securCtx := admiral.NewSecurityContext(authContext, pm)
|
||||
setSecurCtxAndPM(ctx.Request, securCtx, pm)
|
||||
|
@ -220,7 +252,8 @@ func (u *unauthorizedReqCtxModifier) Modify(ctx *beegoctx.Context) bool {
|
|||
if config.WithAdmiral() {
|
||||
// integration with admiral
|
||||
log.Debug("creating PMS project manager...")
|
||||
pm = pms.NewProjectManager(config.AdmiralEndpoint(), "")
|
||||
pm = pms.NewProjectManager(config.AdmiralClient,
|
||||
config.AdmiralEndpoint(), nil)
|
||||
log.Debug("creating admiral security context...")
|
||||
securCtx = admiral.NewSecurityContext(nil, pm)
|
||||
} else {
|
||||
|
|
|
@ -115,8 +115,12 @@ func (p *ProjectManager) GetPublic() ([]*models.Project, error) {
|
|||
// GetByMember returns all projects which the user is a member of
|
||||
func (p *ProjectManager) GetByMember(username string) (
|
||||
[]*models.Project, error) {
|
||||
if len(username) == 0 {
|
||||
return []*models.Project{}, nil
|
||||
}
|
||||
|
||||
return p.GetAll(&models.ProjectQueryParam{
|
||||
Member: &models.Member{
|
||||
Member: &models.MemberQuery{
|
||||
Name: username,
|
||||
},
|
||||
})
|
||||
|
|
|
@ -153,7 +153,13 @@ func TestGetPublic(t *testing.T) {
|
|||
|
||||
func TestGetByMember(t *testing.T) {
|
||||
pm := &ProjectManager{}
|
||||
projects, err := pm.GetByMember("admin")
|
||||
// empty username
|
||||
projects, err := pm.GetByMember("")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 0, len(projects))
|
||||
|
||||
//non-empty username
|
||||
projects, err = pm.GetByMember("admin")
|
||||
assert.Nil(t, err)
|
||||
assert.NotEqual(t, 0, len(projects))
|
||||
}
|
||||
|
|
|
@ -24,22 +24,20 @@ import (
|
|||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/vmware/harbor/src/common"
|
||||
"github.com/vmware/harbor/src/common/models"
|
||||
"github.com/vmware/harbor/src/common/security/authcontext"
|
||||
er "github.com/vmware/harbor/src/common/utils/error"
|
||||
"github.com/vmware/harbor/src/common/utils/log"
|
||||
)
|
||||
|
||||
var transport = &http.Transport{}
|
||||
|
||||
// ProjectManager implements projectmanager.ProjecdtManager interface
|
||||
// base on project management service
|
||||
type ProjectManager struct {
|
||||
endpoint string
|
||||
token string
|
||||
client *http.Client
|
||||
client *http.Client
|
||||
endpoint string
|
||||
tokenReader TokenReader
|
||||
}
|
||||
|
||||
type user struct {
|
||||
|
@ -54,17 +52,16 @@ type project struct {
|
|||
CustomProperties map[string]string `json:"customProperties"`
|
||||
Administrators []*user `json:"administrators"`
|
||||
Developers []*user `json:"members"`
|
||||
Guests []*user `json:"guests"` // TODO the json name needs to be modified according to the API
|
||||
Guests []*user `json:"viewers"`
|
||||
}
|
||||
|
||||
// NewProjectManager returns an instance of ProjectManager
|
||||
func NewProjectManager(endpoint, token string) *ProjectManager {
|
||||
func NewProjectManager(client *http.Client, endpoint string,
|
||||
tokenReader TokenReader) *ProjectManager {
|
||||
return &ProjectManager{
|
||||
endpoint: strings.TrimRight(endpoint, "/"),
|
||||
token: token,
|
||||
client: &http.Client{
|
||||
Transport: transport,
|
||||
},
|
||||
client: client,
|
||||
endpoint: strings.TrimRight(endpoint, "/"),
|
||||
tokenReader: tokenReader,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -80,7 +77,7 @@ func (p *ProjectManager) Get(projectIDOrName interface{}) (*models.Project, erro
|
|||
func (p *ProjectManager) get(projectIDOrName interface{}) (*project, error) {
|
||||
m := map[string]string{}
|
||||
if id, ok := projectIDOrName.(int64); ok {
|
||||
m["customProperties.__harborId"] = strconv.FormatInt(id, 10)
|
||||
m["customProperties.__projectIndex"] = strconv.FormatInt(id, 10)
|
||||
} else if name, ok := projectIDOrName.(string); ok {
|
||||
m["name"] = name
|
||||
} else {
|
||||
|
@ -117,6 +114,10 @@ func (p *ProjectManager) filter(m map[string]string) ([]*project, error) {
|
|||
query += fmt.Sprintf("$filter=%s eq '%s'", k, v)
|
||||
}
|
||||
|
||||
if len(query) == 0 {
|
||||
query = "?expand=true"
|
||||
}
|
||||
|
||||
path := "/projects" + query
|
||||
data, err := p.send(http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
|
@ -129,7 +130,6 @@ func (p *ProjectManager) filter(m map[string]string) ([]*project, error) {
|
|||
// parse the response of GET /projects?xxx to project list
|
||||
func parse(b []byte) ([]*project, error) {
|
||||
documents := &struct {
|
||||
//TotalCount int64 `json:"totalCount"`
|
||||
//DocumentCount int64 `json:"documentCount"`
|
||||
Projects map[string]*project `json:"documents"`
|
||||
}{}
|
||||
|
@ -158,14 +158,14 @@ func convert(p *project) (*models.Project, error) {
|
|||
project.Public = 1
|
||||
}
|
||||
|
||||
value := p.CustomProperties["__harborId"]
|
||||
value := p.CustomProperties["__projectIndex"]
|
||||
if len(value) == 0 {
|
||||
return nil, fmt.Errorf("property __harborId is null")
|
||||
return nil, fmt.Errorf("property __projectIndex is null")
|
||||
}
|
||||
|
||||
id, err := strconv.ParseInt(value, 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse __harborId %s to int64: %v", value, err)
|
||||
return nil, fmt.Errorf("failed to parse __projectIndex %s to int64: %v", value, err)
|
||||
}
|
||||
project.ProjectID = id
|
||||
|
||||
|
@ -227,8 +227,11 @@ func (p *ProjectManager) Exist(projectIDOrName interface{}) (bool, error) {
|
|||
return project != nil, nil
|
||||
}
|
||||
|
||||
// GetRoles ...
|
||||
// TODO empty this method after implementing security context with auth context
|
||||
// GetRoles gets roles that the user has to the project
|
||||
// This method is used in GET /projects API.
|
||||
// Jobservice calls GET /projects API to get information of source
|
||||
// project when trying to replicate the project. There is no auth
|
||||
// context in this use case, so the method is needed.
|
||||
func (p *ProjectManager) GetRoles(username string, projectIDOrName interface{}) ([]int, error) {
|
||||
if len(username) == 0 || projectIDOrName == nil {
|
||||
return nil, nil
|
||||
|
@ -292,31 +295,30 @@ func (p *ProjectManager) getIDbyHarborIDOrName(projectIDOrName interface{}) (str
|
|||
|
||||
// GetPublic ...
|
||||
func (p *ProjectManager) GetPublic() ([]*models.Project, error) {
|
||||
m := map[string]string{
|
||||
"isPublic": "true",
|
||||
}
|
||||
|
||||
projects, err := p.filter(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
list := []*models.Project{}
|
||||
for _, p := range projects {
|
||||
project, err := convert(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
list = append(list, project)
|
||||
}
|
||||
|
||||
return list, nil
|
||||
t := true
|
||||
return p.GetAll(&models.ProjectQueryParam{
|
||||
Public: &t,
|
||||
})
|
||||
}
|
||||
|
||||
// GetByMember ...
|
||||
func (p *ProjectManager) GetByMember(username string) ([]*models.Project, error) {
|
||||
// TODO add implement
|
||||
return nil, nil
|
||||
projects := []*models.Project{}
|
||||
ctx, err := authcontext.GetAuthCtxOfUser(p.client, p.endpoint, p.getToken(), username)
|
||||
if err != nil {
|
||||
return projects, err
|
||||
}
|
||||
|
||||
names := ctx.GetMyProjects()
|
||||
for _, name := range names {
|
||||
project, err := p.Get(name)
|
||||
if err != nil {
|
||||
return projects, err
|
||||
}
|
||||
projects = append(projects, project)
|
||||
}
|
||||
|
||||
return projects, nil
|
||||
}
|
||||
|
||||
// Create ...
|
||||
|
@ -331,9 +333,6 @@ func (p *ProjectManager) Create(pro *models.Project) (int64, error) {
|
|||
proj.CustomProperties["__preventVulnerableImagesFromRunningSeverity"] = pro.PreventVulnerableImagesFromRunningSeverity
|
||||
proj.CustomProperties["__automaticallyScanImagesOnPush"] = strconv.FormatBool(pro.AutomaticallyScanImagesOnPush)
|
||||
|
||||
// TODO remove the logic if Admiral generates the harborId
|
||||
proj.CustomProperties["__harborId"] = strconv.FormatInt(time.Now().UnixNano(), 10)
|
||||
|
||||
data, err := json.Marshal(proj)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
|
@ -375,19 +374,42 @@ func (p *ProjectManager) Update(projectIDOrName interface{}, project *models.Pro
|
|||
|
||||
// GetAll ...
|
||||
func (p *ProjectManager) GetAll(query *models.ProjectQueryParam, base ...*models.BaseProjectCollection) ([]*models.Project, error) {
|
||||
return nil, errors.New("get all projects is unsupported")
|
||||
m := map[string]string{}
|
||||
if query != nil {
|
||||
if len(query.Name) > 0 {
|
||||
m["name"] = query.Name
|
||||
}
|
||||
if query.Public != nil {
|
||||
m["isPublic"] = strconv.FormatBool(*query.Public)
|
||||
}
|
||||
}
|
||||
|
||||
projects, err := p.filter(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
list := []*models.Project{}
|
||||
for _, p := range projects {
|
||||
project, err := convert(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
list = append(list, project)
|
||||
}
|
||||
|
||||
return list, nil
|
||||
}
|
||||
|
||||
// GetTotal ...
|
||||
func (p *ProjectManager) GetTotal(query *models.ProjectQueryParam, base ...*models.BaseProjectCollection) (int64, error) {
|
||||
return 0, errors.New("get total of projects is unsupported")
|
||||
projects, err := p.GetAll(query)
|
||||
return int64(len(projects)), err
|
||||
}
|
||||
|
||||
// GetHasReadPerm returns all projects that user has read perm to
|
||||
// TODO maybe can be removed as search isn't implemented in integration mode
|
||||
// GetHasReadPerm ...
|
||||
func (p *ProjectManager) GetHasReadPerm(username ...string) ([]*models.Project, error) {
|
||||
// TODO add implement
|
||||
return nil, nil
|
||||
return nil, errors.New("GetHasReadPerm is unsupported")
|
||||
}
|
||||
|
||||
func (p *ProjectManager) send(method, path string, body io.Reader) ([]byte, error) {
|
||||
|
@ -396,7 +418,7 @@ func (p *ProjectManager) send(method, path string, body io.Reader) ([]byte, erro
|
|||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Add("x-xenon-auth-token", p.token)
|
||||
req.Header.Add("x-xenon-auth-token", p.getToken())
|
||||
|
||||
url := req.URL.String()
|
||||
|
||||
|
@ -423,3 +445,16 @@ func (p *ProjectManager) send(method, path string, body io.Reader) ([]byte, erro
|
|||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (p *ProjectManager) getToken() string {
|
||||
if p.tokenReader == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
token, err := p.tokenReader.ReadToken()
|
||||
if err != nil {
|
||||
token = ""
|
||||
log.Errorf("failed to read token: %v", err)
|
||||
}
|
||||
return token
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
package pms
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
|
@ -24,8 +25,11 @@ import (
|
|||
)
|
||||
|
||||
var (
|
||||
endpoint = "http://127.0.0.1:8282"
|
||||
token = ""
|
||||
client = http.DefaultClient
|
||||
endpoint = "http://127.0.0.1:8282"
|
||||
tokenReader = &RawTokenReader{
|
||||
Token: "",
|
||||
}
|
||||
)
|
||||
|
||||
func TestConvert(t *testing.T) {
|
||||
|
@ -34,16 +38,16 @@ func TestConvert(t *testing.T) {
|
|||
assert.Nil(t, err)
|
||||
assert.Nil(t, pro)
|
||||
|
||||
//project without property __harborId
|
||||
//project without property __projectIndex
|
||||
p := &project{}
|
||||
pro, err = convert(p)
|
||||
assert.NotNil(t, err)
|
||||
assert.Nil(t, pro)
|
||||
|
||||
//project with invalid __harborId
|
||||
//project with invalid __projectIndex
|
||||
p = &project{
|
||||
CustomProperties: map[string]string{
|
||||
"__harborId": "invalid_value",
|
||||
"__projectIndex": "invalid_value",
|
||||
},
|
||||
}
|
||||
pro, err = convert(p)
|
||||
|
@ -85,7 +89,7 @@ func TestConvert(t *testing.T) {
|
|||
Name: "test",
|
||||
Public: true,
|
||||
CustomProperties: map[string]string{
|
||||
"__harborId": "1",
|
||||
"__projectIndex": "1",
|
||||
"__enableContentTrust": "true",
|
||||
"__preventVulnerableImagesFromRunning": "true",
|
||||
"__preventVulnerableImagesFromRunningSeverity": "medium",
|
||||
|
@ -118,7 +122,7 @@ func TestParse(t *testing.T) {
|
|||
"id": "41427587-70e9-4671-9a9e-b9def0a07bb7",
|
||||
"name": "project02",
|
||||
"customProperties": {
|
||||
"__harborId": "2",
|
||||
"__projectIndex": "2",
|
||||
"__enableContentTrust": "true",
|
||||
"__preventVulnerableImagesFromRunning": "true",
|
||||
"__preventVulnerableImagesFromRunningSeverity": "medium",
|
||||
|
@ -140,7 +144,7 @@ func TestParse(t *testing.T) {
|
|||
"id": "default-project",
|
||||
"name": "default-project",
|
||||
"customProperties": {
|
||||
"__harborId": "2",
|
||||
"__projectIndex": "2",
|
||||
"__enableContentTrust": "true",
|
||||
"__preventVulnerableImagesFromRunning": "true",
|
||||
"__preventVulnerableImagesFromRunningSeverity": "medium",
|
||||
|
@ -177,17 +181,13 @@ func TestParse(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestGet(t *testing.T) {
|
||||
pm := NewProjectManager(endpoint, token)
|
||||
pm := NewProjectManager(client, endpoint, tokenReader)
|
||||
name := "project_for_test_get"
|
||||
id, err := pm.Create(&models.Project{
|
||||
Name: name,
|
||||
})
|
||||
require.Nil(t, err)
|
||||
defer func(id int64) {
|
||||
if err := pm.Delete(id); err != nil {
|
||||
require.Nil(t, err)
|
||||
}
|
||||
}(id)
|
||||
defer delete(t, id)
|
||||
|
||||
// get by invalid input type
|
||||
_, err = pm.Get([]string{})
|
||||
|
@ -215,7 +215,7 @@ func TestGet(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestIsPublic(t *testing.T) {
|
||||
pm := NewProjectManager(endpoint, token)
|
||||
pm := NewProjectManager(client, endpoint, tokenReader)
|
||||
|
||||
// invalid input type
|
||||
public, err := pm.IsPublic([]string{})
|
||||
|
@ -234,11 +234,7 @@ func TestIsPublic(t *testing.T) {
|
|||
Public: 1,
|
||||
})
|
||||
require.Nil(t, err)
|
||||
defer func(id int64) {
|
||||
if err := pm.Delete(id); err != nil {
|
||||
require.Nil(t, err)
|
||||
}
|
||||
}(id)
|
||||
defer delete(t, id)
|
||||
|
||||
public, err = pm.IsPublic(id)
|
||||
assert.Nil(t, err)
|
||||
|
@ -255,11 +251,7 @@ func TestIsPublic(t *testing.T) {
|
|||
Public: 0,
|
||||
})
|
||||
require.Nil(t, err)
|
||||
defer func(id int64) {
|
||||
if err := pm.Delete(id); err != nil {
|
||||
require.Nil(t, err)
|
||||
}
|
||||
}(id)
|
||||
defer delete(t, id)
|
||||
|
||||
public, err = pm.IsPublic(id)
|
||||
assert.Nil(t, err)
|
||||
|
@ -271,7 +263,7 @@ func TestIsPublic(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestExist(t *testing.T) {
|
||||
pm := NewProjectManager(endpoint, token)
|
||||
pm := NewProjectManager(client, endpoint, tokenReader)
|
||||
|
||||
// invalid input type
|
||||
exist, err := pm.Exist([]string{})
|
||||
|
@ -289,11 +281,7 @@ func TestExist(t *testing.T) {
|
|||
Name: name,
|
||||
})
|
||||
require.Nil(t, err)
|
||||
defer func(id int64) {
|
||||
if err := pm.Delete(id); err != nil {
|
||||
require.Nil(t, err)
|
||||
}
|
||||
}(id)
|
||||
defer delete(t, id)
|
||||
|
||||
exist, err = pm.Exist(id)
|
||||
assert.Nil(t, err)
|
||||
|
@ -305,7 +293,7 @@ func TestExist(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestGetRoles(t *testing.T) {
|
||||
pm := NewProjectManager(endpoint, token)
|
||||
pm := NewProjectManager(client, endpoint, tokenReader)
|
||||
|
||||
// nil username, nil project
|
||||
roles, err := pm.GetRoles("", nil)
|
||||
|
@ -322,11 +310,7 @@ func TestGetRoles(t *testing.T) {
|
|||
Name: name,
|
||||
})
|
||||
require.Nil(t, err)
|
||||
defer func(id int64) {
|
||||
if err := pm.Delete(id); err != nil {
|
||||
require.Nil(t, err)
|
||||
}
|
||||
}(id)
|
||||
defer delete(t, id)
|
||||
|
||||
roles, err = pm.GetRoles("user01", id)
|
||||
assert.Nil(t, err)
|
||||
|
@ -336,7 +320,7 @@ func TestGetRoles(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestGetPublic(t *testing.T) {
|
||||
pm := NewProjectManager(endpoint, token)
|
||||
pm := NewProjectManager(client, endpoint, tokenReader)
|
||||
|
||||
projects, err := pm.GetPublic()
|
||||
assert.Nil(t, nil)
|
||||
|
@ -348,11 +332,7 @@ func TestGetPublic(t *testing.T) {
|
|||
Public: 1,
|
||||
})
|
||||
require.Nil(t, err)
|
||||
defer func(id int64) {
|
||||
if err := pm.Delete(id); err != nil {
|
||||
require.Nil(t, err)
|
||||
}
|
||||
}(id)
|
||||
defer delete(t, id)
|
||||
|
||||
projects, err = pm.GetPublic()
|
||||
assert.Nil(t, nil)
|
||||
|
@ -374,7 +354,7 @@ func TestGetByMember(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestCreate(t *testing.T) {
|
||||
pm := NewProjectManager(endpoint, token)
|
||||
pm := NewProjectManager(client, endpoint, tokenReader)
|
||||
|
||||
name := "project_for_test_create"
|
||||
id, err := pm.Create(&models.Project{
|
||||
|
@ -386,11 +366,7 @@ func TestCreate(t *testing.T) {
|
|||
AutomaticallyScanImagesOnPush: true,
|
||||
})
|
||||
require.Nil(t, err)
|
||||
defer func(id int64) {
|
||||
if err := pm.Delete(id); err != nil {
|
||||
require.Nil(t, err)
|
||||
}
|
||||
}(id)
|
||||
defer delete(t, id)
|
||||
|
||||
project, err := pm.Get(id)
|
||||
assert.Nil(t, err)
|
||||
|
@ -403,7 +379,7 @@ func TestCreate(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestDelete(t *testing.T) {
|
||||
pm := NewProjectManager(endpoint, token)
|
||||
pm := NewProjectManager(client, endpoint, tokenReader)
|
||||
|
||||
// non-exist project
|
||||
err := pm.Delete(int64(0))
|
||||
|
@ -429,24 +405,102 @@ func TestDelete(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestUpdate(t *testing.T) {
|
||||
pm := NewProjectManager(endpoint, token)
|
||||
pm := NewProjectManager(client, endpoint, tokenReader)
|
||||
err := pm.Update(nil, nil)
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
func TestGetAll(t *testing.T) {
|
||||
pm := NewProjectManager(endpoint, token)
|
||||
_, err := pm.GetAll(nil)
|
||||
assert.NotNil(t, err)
|
||||
pm := NewProjectManager(client, endpoint, tokenReader)
|
||||
|
||||
name1 := "project_for_test_get_all_01"
|
||||
id1, err := pm.Create(&models.Project{
|
||||
Name: name1,
|
||||
})
|
||||
require.Nil(t, err)
|
||||
defer delete(t, id1)
|
||||
|
||||
name2 := "project_for_test_get_all_02"
|
||||
id2, err := pm.Create(&models.Project{
|
||||
Name: name2,
|
||||
Public: 1,
|
||||
})
|
||||
require.Nil(t, err)
|
||||
defer delete(t, id2)
|
||||
|
||||
// no filter
|
||||
projects, err := pm.GetAll(nil)
|
||||
require.Nil(t, err)
|
||||
found1 := false
|
||||
found2 := false
|
||||
for _, project := range projects {
|
||||
if project.ProjectID == id1 {
|
||||
found1 = true
|
||||
}
|
||||
if project.ProjectID == id2 {
|
||||
found2 = true
|
||||
}
|
||||
}
|
||||
assert.True(t, found1)
|
||||
assert.True(t, found2)
|
||||
|
||||
// filter by name
|
||||
projects, err = pm.GetAll(&models.ProjectQueryParam{
|
||||
Name: name1,
|
||||
})
|
||||
require.Nil(t, err)
|
||||
found1 = false
|
||||
for _, project := range projects {
|
||||
if project.ProjectID == id1 {
|
||||
found1 = true
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.True(t, found1)
|
||||
|
||||
// filter by public
|
||||
value := true
|
||||
projects, err = pm.GetAll(&models.ProjectQueryParam{
|
||||
Public: &value,
|
||||
})
|
||||
require.Nil(t, err)
|
||||
found2 = false
|
||||
for _, project := range projects {
|
||||
if project.ProjectID == id2 {
|
||||
found2 = true
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.True(t, found2)
|
||||
}
|
||||
|
||||
func TestGetTotal(t *testing.T) {
|
||||
pm := NewProjectManager(endpoint, token)
|
||||
_, err := pm.GetTotal(nil)
|
||||
pm := NewProjectManager(client, endpoint, tokenReader)
|
||||
|
||||
total1, err := pm.GetTotal(nil)
|
||||
require.Nil(t, err)
|
||||
|
||||
name := "project_for_test_get_total"
|
||||
id, err := pm.Create(&models.Project{
|
||||
Name: name,
|
||||
})
|
||||
require.Nil(t, err)
|
||||
defer delete(t, id)
|
||||
|
||||
total2, err := pm.GetTotal(nil)
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, total1+1, total2)
|
||||
}
|
||||
|
||||
func TestGetHasReadPerm(t *testing.T) {
|
||||
pm := NewProjectManager(client, endpoint, tokenReader)
|
||||
_, err := pm.GetHasReadPerm()
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
// TODO add test case
|
||||
func TestGetHasReadPerm(t *testing.T) {
|
||||
|
||||
func delete(t *testing.T, id int64) {
|
||||
pm := NewProjectManager(client, endpoint, tokenReader)
|
||||
if err := pm.Delete(id); err != nil {
|
||||
t.Logf("failed to delete project %d: %v", id, err)
|
||||
}
|
||||
}
|
||||
|
|
84
src/ui/projectmanager/pms/token.go
Normal file
84
src/ui/projectmanager/pms/token.go
Normal file
|
@ -0,0 +1,84 @@
|
|||
// Copyright (c) 2017 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 pms
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
key = "access_token"
|
||||
)
|
||||
|
||||
// TokenReader is an interface used to wrap the way how to get token
|
||||
type TokenReader interface {
|
||||
// ReadToken reads token
|
||||
ReadToken() (string, error)
|
||||
}
|
||||
|
||||
// RawTokenReader just returns the token contained by field Token
|
||||
type RawTokenReader struct {
|
||||
Token string
|
||||
}
|
||||
|
||||
// ReadToken ...
|
||||
func (r *RawTokenReader) ReadToken() (string, error) {
|
||||
return r.Token, nil
|
||||
}
|
||||
|
||||
// FileTokenReader reads token from file
|
||||
type FileTokenReader struct {
|
||||
Path string
|
||||
}
|
||||
|
||||
// ReadToken ...
|
||||
func (f *FileTokenReader) ReadToken() (string, error) {
|
||||
file, err := os.Open(f.Path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
return readToken(file)
|
||||
}
|
||||
|
||||
func readToken(reader io.Reader) (string, error) {
|
||||
if reader == nil {
|
||||
return "", fmt.Errorf("reader is nil")
|
||||
}
|
||||
|
||||
r := bufio.NewReader(reader)
|
||||
for {
|
||||
line, _, err := r.ReadLine()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
err = fmt.Errorf("%s not found", key)
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
strs := strings.SplitN(string(line), "=", 2)
|
||||
if len(strs) != 2 {
|
||||
continue
|
||||
}
|
||||
if strs[0] == key {
|
||||
return strs[1], nil
|
||||
}
|
||||
}
|
||||
}
|
98
src/ui/projectmanager/pms/token_test.go
Normal file
98
src/ui/projectmanager/pms/token_test.go
Normal file
|
@ -0,0 +1,98 @@
|
|||
// Copyright (c) 2017 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 pms
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRawTokenReader(t *testing.T) {
|
||||
raw := "token"
|
||||
reader := &RawTokenReader{
|
||||
Token: raw,
|
||||
}
|
||||
|
||||
token, err := reader.ReadToken()
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, raw, token)
|
||||
}
|
||||
|
||||
func TestReadToken(t *testing.T) {
|
||||
// nil reader
|
||||
_, err := readToken(nil)
|
||||
assert.NotNil(t, err)
|
||||
|
||||
// empty
|
||||
reader := bytes.NewReader([]byte{})
|
||||
_, err = readToken(reader)
|
||||
assert.NotNil(t, err)
|
||||
|
||||
// contains no "access_token"
|
||||
content := "key1=value\nkey2=value2"
|
||||
reader = bytes.NewReader([]byte(content))
|
||||
_, err = readToken(reader)
|
||||
assert.NotNil(t, err)
|
||||
|
||||
// contains "access_token" but no "="
|
||||
content = "access_token value\nkey2=value2"
|
||||
reader = bytes.NewReader([]byte(content))
|
||||
_, err = readToken(reader)
|
||||
assert.NotNil(t, err)
|
||||
|
||||
// contains "access_token" and "=", but no value
|
||||
content = "access_token=\nkey2=value2"
|
||||
reader = bytes.NewReader([]byte(content))
|
||||
token, err := readToken(reader)
|
||||
require.Nil(t, err)
|
||||
assert.Len(t, token, 0)
|
||||
|
||||
// valid "access_token"
|
||||
content = "access_token=token\nkey2=value2"
|
||||
reader = bytes.NewReader([]byte(content))
|
||||
token, err = readToken(reader)
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, "token", token)
|
||||
}
|
||||
|
||||
func TestFileTokenReader(t *testing.T) {
|
||||
// file not exist
|
||||
path := "/tmp/not_exist_file"
|
||||
reader := &FileTokenReader{
|
||||
Path: path,
|
||||
}
|
||||
|
||||
_, err := reader.ReadToken()
|
||||
assert.NotNil(t, err)
|
||||
|
||||
// file exist
|
||||
path = "/tmp/exist_file"
|
||||
err = ioutil.WriteFile(path, []byte("access_token=token"), 0x0666)
|
||||
require.Nil(t, err)
|
||||
defer os.Remove(path)
|
||||
|
||||
reader = &FileTokenReader{
|
||||
Path: path,
|
||||
}
|
||||
|
||||
token, err := reader.ReadToken()
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, "token", token)
|
||||
}
|
|
@ -3,6 +3,7 @@ package proxy
|
|||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/vmware/harbor/src/adminserver/client"
|
||||
"github.com/vmware/harbor/src/common"
|
||||
"github.com/vmware/harbor/src/common/models"
|
||||
notarytest "github.com/vmware/harbor/src/common/utils/notary/test"
|
||||
|
@ -19,6 +20,7 @@ import (
|
|||
var endpoint = "10.117.4.142"
|
||||
var notaryServer *httptest.Server
|
||||
var adminServer *httptest.Server
|
||||
var adminserverClient client.Client
|
||||
|
||||
var admiralEndpoint = "http://127.0.0.1:8282"
|
||||
var token = ""
|
||||
|
@ -43,6 +45,7 @@ func TestMain(m *testing.M) {
|
|||
if err := config.Init(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
adminserverClient = client.NewClient(adminServer.URL, nil)
|
||||
result := m.Run()
|
||||
if result != 0 {
|
||||
os.Exit(result)
|
||||
|
@ -95,51 +98,77 @@ func TestEnvPolicyChecker(t *testing.T) {
|
|||
if err := os.Setenv("PROJECT_CONTENT_TRUST", "1"); err != nil {
|
||||
t.Fatalf("Failed to set env variable: %v", err)
|
||||
}
|
||||
if err2 := os.Setenv("PROJECT_VULNERABLE", "1"); err2 != nil {
|
||||
t.Fatalf("Failed to set env variable: %v", err2)
|
||||
}
|
||||
if err3 := os.Setenv("PROJECT_SEVERITY", "negligible"); err3 != nil {
|
||||
t.Fatalf("Failed to set env variable: %v", err3)
|
||||
}
|
||||
contentTrustFlag := getPolicyChecker().contentTrustEnabled("whatever")
|
||||
vulFlag := getPolicyChecker().vulnerableEnabled("whatever")
|
||||
vulFlag, sev := getPolicyChecker().vulnerablePolicy("whatever")
|
||||
assert.True(contentTrustFlag)
|
||||
assert.False(vulFlag)
|
||||
assert.True(vulFlag)
|
||||
assert.Equal(sev, models.SevNone)
|
||||
}
|
||||
|
||||
func TestPMSPolicyChecker(t *testing.T) {
|
||||
pm := pms.NewProjectManager(admiralEndpoint, token)
|
||||
name := "project_for_test_get_true"
|
||||
var defaultConfigAdmiral = map[string]interface{}{
|
||||
common.ExtEndpoint: "https://" + endpoint,
|
||||
common.WithNotary: true,
|
||||
common.CfgExpiration: 5,
|
||||
common.AdmiralEndpoint: admiralEndpoint,
|
||||
}
|
||||
adminServer, err := utilstest.NewAdminserver(defaultConfigAdmiral)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer adminServer.Close()
|
||||
if err := os.Setenv("ADMIN_SERVER_URL", adminServer.URL); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := config.Init(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
pm := pms.NewProjectManager(http.DefaultClient,
|
||||
admiralEndpoint, nil)
|
||||
name := "project_for_test_get_sev_low"
|
||||
id, err := pm.Create(&models.Project{
|
||||
Name: name,
|
||||
EnableContentTrust: true,
|
||||
Name: name,
|
||||
EnableContentTrust: true,
|
||||
PreventVulnerableImagesFromRunning: false,
|
||||
PreventVulnerableImagesFromRunningSeverity: "low",
|
||||
})
|
||||
require.Nil(t, err)
|
||||
defer func(id int64) {
|
||||
if err := pm.Delete(id); err != nil {
|
||||
require.Nil(t, err)
|
||||
t.Logf("failed to delete project %d: %v", id, err)
|
||||
}
|
||||
}(id)
|
||||
project, err := pm.Get(id)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, id, project.ProjectID)
|
||||
server, err2 := utilstest.NewAdminserver(nil)
|
||||
if err2 != nil {
|
||||
t.Fatalf("failed to create a mock admin server: %v", err2)
|
||||
}
|
||||
defer server.Close()
|
||||
contentTrustFlag := getPolicyChecker().contentTrustEnabled("project_for_test_get_true")
|
||||
|
||||
contentTrustFlag := getPolicyChecker().contentTrustEnabled("project_for_test_get_sev_low")
|
||||
assert.True(t, contentTrustFlag)
|
||||
projectVulnerableEnabled, projectVulnerableSeverity := getPolicyChecker().vulnerablePolicy("project_for_test_get_sev_low")
|
||||
assert.False(t, projectVulnerableEnabled)
|
||||
assert.Equal(t, projectVulnerableSeverity, models.SevLow)
|
||||
}
|
||||
|
||||
func TestMatchNotaryDigest(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
//The data from common/utils/notary/helper_test.go
|
||||
img1 := imageInfo{"notary-demo/busybox", "1.0", "notary-demo"}
|
||||
img2 := imageInfo{"notary-demo/busybox", "2.0", "notary-demo"}
|
||||
res1, err := matchNotaryDigest(img1, "sha256:1359608115b94599e5641638bac5aef1ddfaa79bb96057ebf41ebc8d33acf8a7")
|
||||
img1 := imageInfo{"notary-demo/busybox", "1.0", "notary-demo", "sha256:1359608115b94599e5641638bac5aef1ddfaa79bb96057ebf41ebc8d33acf8a7"}
|
||||
img2 := imageInfo{"notary-demo/busybox", "2.0", "notary-demo", "sha256:12345678"}
|
||||
|
||||
res1, err := matchNotaryDigest(img1)
|
||||
assert.Nil(err, "Unexpected error: %v, image: %#v", err, img1)
|
||||
assert.True(res1)
|
||||
res2, err := matchNotaryDigest(img1, "sha256:1359608115b94599e5641638bac5aef1ddfaa79bb96057ebf41ebc8d33acf8a8")
|
||||
assert.Nil(err, "Unexpected error: %v, image: %#v, take 2", err, img1)
|
||||
|
||||
res2, err := matchNotaryDigest(img2)
|
||||
assert.Nil(err, "Unexpected error: %v, image: %#v, take 2", err, img2)
|
||||
assert.False(res2)
|
||||
res3, err := matchNotaryDigest(img2, "sha256:1359608115b94599e5641638bac5aef1ddfaa79bb96057ebf41ebc8d33acf8a7")
|
||||
assert.Nil(err, "Unexpected error: %v, image: %#v", err, img2)
|
||||
assert.False(res3)
|
||||
}
|
||||
|
||||
func TestCopyResp(t *testing.T) {
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
package proxy
|
||||
|
||||
import (
|
||||
// "github.com/vmware/harbor/src/ui/api"
|
||||
"github.com/vmware/harbor/src/common/dao"
|
||||
"github.com/vmware/harbor/src/common/models"
|
||||
"github.com/vmware/harbor/src/common/utils/clair"
|
||||
"github.com/vmware/harbor/src/common/utils/log"
|
||||
"github.com/vmware/harbor/src/common/utils/notary"
|
||||
// "github.com/vmware/harbor/src/ui/api"
|
||||
"github.com/vmware/harbor/src/ui/config"
|
||||
"github.com/vmware/harbor/src/ui/projectmanager"
|
||||
"github.com/vmware/harbor/src/ui/projectmanager/pms"
|
||||
|
||||
"context"
|
||||
"fmt"
|
||||
|
@ -26,6 +28,9 @@ const (
|
|||
tokenUsername = "admin"
|
||||
)
|
||||
|
||||
// Record the docker deamon raw response.
|
||||
var rec *httptest.ResponseRecorder
|
||||
|
||||
// NotaryEndpoint , exported for testing.
|
||||
var NotaryEndpoint = config.InternalNotaryEndpoint()
|
||||
|
||||
|
@ -51,8 +56,8 @@ func MatchPullManifest(req *http.Request) (bool, string, string) {
|
|||
type policyChecker interface {
|
||||
// contentTrustEnabled returns whether a project has enabled content trust.
|
||||
contentTrustEnabled(name string) bool
|
||||
// vulnerableEnabled returns whether a project has enabled content trust.
|
||||
vulnerableEnabled(name string) bool
|
||||
// vulnerablePolicy returns whether a project has enabled vulnerable, and the project's severity.
|
||||
vulnerablePolicy(name string) (bool, models.Severity)
|
||||
}
|
||||
|
||||
//For testing
|
||||
|
@ -61,9 +66,8 @@ type envPolicyChecker struct{}
|
|||
func (ec envPolicyChecker) contentTrustEnabled(name string) bool {
|
||||
return os.Getenv("PROJECT_CONTENT_TRUST") == "1"
|
||||
}
|
||||
func (ec envPolicyChecker) vulnerableEnabled(name string) bool {
|
||||
// TODO: May need get more information in vulnerable policies.
|
||||
return os.Getenv("PROJECT_VULNERABBLE") == "1"
|
||||
func (ec envPolicyChecker) vulnerablePolicy(name string) (bool, models.Severity) {
|
||||
return os.Getenv("PROJECT_VULNERABLE") == "1", clair.ParseClairSev(os.Getenv("PROJECT_SEVERITY"))
|
||||
}
|
||||
|
||||
type pmsPolicyChecker struct {
|
||||
|
@ -78,8 +82,13 @@ func (pc pmsPolicyChecker) contentTrustEnabled(name string) bool {
|
|||
}
|
||||
return project.EnableContentTrust
|
||||
}
|
||||
func (pc pmsPolicyChecker) vulnerableEnabled(name string) bool {
|
||||
return true
|
||||
func (pc pmsPolicyChecker) vulnerablePolicy(name string) (bool, models.Severity) {
|
||||
project, err := pc.pm.Get(name)
|
||||
if err != nil {
|
||||
log.Errorf("Unexpected error when getting the project, error: %v", err)
|
||||
return true, models.SevUnknown
|
||||
}
|
||||
return project.PreventVulnerableImagesFromRunning, clair.ParseClairSev(project.PreventVulnerableImagesFromRunningSeverity)
|
||||
}
|
||||
|
||||
// newPMSPolicyChecker returns an instance of an pmsPolicyChecker
|
||||
|
@ -92,7 +101,7 @@ func newPMSPolicyChecker(pm projectmanager.ProjectManager) policyChecker {
|
|||
// TODO: Get project manager with PM factory.
|
||||
func getPolicyChecker() policyChecker {
|
||||
if config.WithAdmiral() {
|
||||
return newPMSPolicyChecker(pms.NewProjectManager(config.AdmiralEndpoint(), ""))
|
||||
return newPMSPolicyChecker(config.GlobalProjectMgr)
|
||||
}
|
||||
return EnvChecker
|
||||
}
|
||||
|
@ -101,7 +110,7 @@ type imageInfo struct {
|
|||
repository string
|
||||
tag string
|
||||
projectName string
|
||||
// digest string
|
||||
digest string
|
||||
}
|
||||
|
||||
type urlHandler struct {
|
||||
|
@ -120,38 +129,22 @@ func (uh urlHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
|||
http.Error(rw, fmt.Sprintf("Bad repository name: %s", repository), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
/*
|
||||
//Need to get digest of the image.
|
||||
endpoint, err := config.RegistryURL()
|
||||
if err != nil {
|
||||
log.Errorf("Error getting Registry URL: %v", err)
|
||||
http.Error(rw, fmt.Sprintf("Failed due to internal Error: %v", err), http.StatusInternalError)
|
||||
return
|
||||
}
|
||||
rc, err := api.NewRepositoryClient(endpoint, false, username, repository, "repository", repository, "pull")
|
||||
if err != nil {
|
||||
log.Errorf("Error creating repository Client: %v", err)
|
||||
http.Error(rw, fmt.Sprintf("Failed due to internal Error: %v", err), http.StatusInternalError)
|
||||
return
|
||||
}
|
||||
digest, exist, err := rc.ManifestExist(tag)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to get digest for tag: %s, error: %v", tag, err)
|
||||
http.Error(rw, fmt.Sprintf("Failed due to internal Error: %v", err), http.StatusInternalError)
|
||||
return
|
||||
}
|
||||
*/
|
||||
|
||||
rec = httptest.NewRecorder()
|
||||
uh.next.ServeHTTP(rec, req)
|
||||
if rec.Result().StatusCode != http.StatusOK {
|
||||
copyResp(rec, rw)
|
||||
return
|
||||
}
|
||||
digest := rec.Header().Get(http.CanonicalHeaderKey("Docker-Content-Digest"))
|
||||
img := imageInfo{
|
||||
repository: repository,
|
||||
tag: tag,
|
||||
projectName: components[0],
|
||||
digest: digest,
|
||||
}
|
||||
log.Debugf("image info of the request: %#v", img)
|
||||
|
||||
ctx := context.WithValue(req.Context(), imageInfoCtxKey, img)
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
}
|
||||
uh.next.ServeHTTP(rw, req)
|
||||
}
|
||||
|
@ -171,31 +164,70 @@ func (cth contentTrustHandler) ServeHTTP(rw http.ResponseWriter, req *http.Reque
|
|||
cth.next.ServeHTTP(rw, req)
|
||||
return
|
||||
}
|
||||
//May need to update status code, let's use recorder
|
||||
rec := httptest.NewRecorder()
|
||||
cth.next.ServeHTTP(rec, req)
|
||||
if rec.Result().StatusCode != http.StatusOK {
|
||||
copyResp(rec, rw)
|
||||
return
|
||||
}
|
||||
log.Debugf("showing digest")
|
||||
digest := rec.Header().Get(http.CanonicalHeaderKey("Docker-Content-Digest"))
|
||||
log.Debugf("digest: %s", digest)
|
||||
match, err := matchNotaryDigest(img, digest)
|
||||
match, err := matchNotaryDigest(img)
|
||||
if err != nil {
|
||||
http.Error(rw, "Failed in communication with Notary please check the log", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if match {
|
||||
log.Debugf("Passing the response to outter responseWriter")
|
||||
copyResp(rec, rw)
|
||||
} else {
|
||||
if !match {
|
||||
log.Debugf("digest mismatch, failing the response.")
|
||||
http.Error(rw, "The image is not signed in Notary.", http.StatusPreconditionFailed)
|
||||
return
|
||||
}
|
||||
cth.next.ServeHTTP(rw, req)
|
||||
}
|
||||
|
||||
func matchNotaryDigest(img imageInfo, digest string) (bool, error) {
|
||||
type vulnerableHandler struct {
|
||||
next http.Handler
|
||||
}
|
||||
|
||||
func (vh vulnerableHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
imgRaw := req.Context().Value(imageInfoCtxKey)
|
||||
if imgRaw == nil || !config.WithClair() {
|
||||
vh.next.ServeHTTP(rw, req)
|
||||
return
|
||||
}
|
||||
img, _ := req.Context().Value(imageInfoCtxKey).(imageInfo)
|
||||
projectVulnerableEnabled, projectVulnerableSeverity := getPolicyChecker().vulnerablePolicy(img.projectName)
|
||||
if !projectVulnerableEnabled {
|
||||
vh.next.ServeHTTP(rw, req)
|
||||
return
|
||||
}
|
||||
overview, err := dao.GetImgScanOverview(img.digest)
|
||||
if err != nil {
|
||||
log.Errorf("failed to get ImgScanOverview with repo: %s, tag: %s, digest: %s. Error: %v", img.repository, img.tag, img.digest, err)
|
||||
http.Error(rw, "Failed to get ImgScanOverview.", http.StatusPreconditionFailed)
|
||||
return
|
||||
}
|
||||
if overview == nil {
|
||||
log.Debugf("cannot get the image scan overview info, failing the response.")
|
||||
http.Error(rw, "Cannot get the image scan overview info.", http.StatusPreconditionFailed)
|
||||
return
|
||||
}
|
||||
imageSev := overview.Sev
|
||||
if imageSev > int(projectVulnerableSeverity) {
|
||||
log.Debugf("the image severity is higher then project setting, failing the response.")
|
||||
http.Error(rw, "The image scan result doesn't pass the project setting.", http.StatusPreconditionFailed)
|
||||
return
|
||||
}
|
||||
vh.next.ServeHTTP(rw, req)
|
||||
}
|
||||
|
||||
type funnelHandler struct {
|
||||
next http.Handler
|
||||
}
|
||||
|
||||
func (fu funnelHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
imgRaw := req.Context().Value(imageInfoCtxKey)
|
||||
if imgRaw != nil {
|
||||
log.Debugf("Return the original response as no the interceptor takes action.")
|
||||
copyResp(rec, rw)
|
||||
return
|
||||
}
|
||||
fu.next.ServeHTTP(rw, req)
|
||||
}
|
||||
|
||||
func matchNotaryDigest(img imageInfo) (bool, error) {
|
||||
targets, err := notary.GetInternalTargets(NotaryEndpoint, tokenUsername, img.repository)
|
||||
if err != nil {
|
||||
return false, err
|
||||
|
@ -207,7 +239,7 @@ func matchNotaryDigest(img imageInfo, digest string) (bool, error) {
|
|||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return digest == d, nil
|
||||
return img.digest == d, nil
|
||||
}
|
||||
}
|
||||
log.Debugf("image: %#v, not found in notary", img)
|
||||
|
|
|
@ -42,7 +42,7 @@ func Init(urls ...string) error {
|
|||
}
|
||||
Proxy = httputil.NewSingleHostReverseProxy(targetURL)
|
||||
//TODO: add vulnerable interceptor.
|
||||
handlers = handlerChain{head: urlHandler{next: contentTrustHandler{next: Proxy}}}
|
||||
handlers = handlerChain{head: urlHandler{next: contentTrustHandler{next: vulnerableHandler{next: funnelHandler{next: Proxy}}}}}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -40,10 +40,10 @@ func initRouters() {
|
|||
beego.Router("/harbor/sign-up", &controllers.IndexController{})
|
||||
beego.Router("/harbor/dashboard", &controllers.IndexController{})
|
||||
beego.Router("/harbor/projects", &controllers.IndexController{})
|
||||
beego.Router("/harbor/projects/:id/repository", &controllers.IndexController{})
|
||||
beego.Router("/harbor/projects/:id/replication", &controllers.IndexController{})
|
||||
beego.Router("/harbor/projects/:id/member", &controllers.IndexController{})
|
||||
beego.Router("/harbor/projects/:id/log", &controllers.IndexController{})
|
||||
beego.Router("/harbor/projects/:id/repositories", &controllers.IndexController{})
|
||||
beego.Router("/harbor/projects/:id/replications", &controllers.IndexController{})
|
||||
beego.Router("/harbor/projects/:id/members", &controllers.IndexController{})
|
||||
beego.Router("/harbor/projects/:id/logs", &controllers.IndexController{})
|
||||
beego.Router("/harbor/tags/:id/*", &controllers.IndexController{})
|
||||
|
||||
beego.Router("/harbor/users", &controllers.IndexController{})
|
||||
|
@ -73,6 +73,7 @@ func initRouters() {
|
|||
beego.Router("/api/users/:id([0-9]+)/password", &api.UserAPI{}, "put:ChangePassword")
|
||||
beego.Router("/api/internal/syncregistry", &api.InternalAPI{}, "post:SyncRegistry")
|
||||
beego.Router("/api/repositories", &api.RepositoryAPI{}, "get:Get")
|
||||
beego.Router("/api/repositories/scanAll", &api.RepositoryAPI{}, "post:ScanAll")
|
||||
beego.Router("/api/repositories/*", &api.RepositoryAPI{}, "delete:Delete")
|
||||
beego.Router("/api/repositories/*/tags/:tag", &api.RepositoryAPI{}, "delete:Delete;get:GetTag")
|
||||
beego.Router("/api/repositories/*/tags", &api.RepositoryAPI{}, "get:GetTags")
|
||||
|
|
|
@ -27,7 +27,7 @@ import (
|
|||
"github.com/vmware/harbor/src/common/utils/log"
|
||||
"github.com/vmware/harbor/src/ui/api"
|
||||
"github.com/vmware/harbor/src/ui/config"
|
||||
"github.com/vmware/harbor/src/ui/projectmanager/pms"
|
||||
uiutils "github.com/vmware/harbor/src/ui/utils"
|
||||
)
|
||||
|
||||
// NotificationHandler handles request on /service/notifications/, which listens to registry's events.
|
||||
|
@ -65,7 +65,7 @@ func (n *NotificationHandler) Post() {
|
|||
user = "anonymous"
|
||||
}
|
||||
|
||||
pro, err := n.ProjectMgr.Get(project)
|
||||
pro, err := config.GlobalProjectMgr.Get(project)
|
||||
if err != nil {
|
||||
log.Errorf("failed to get project by name %s: %v", project, err)
|
||||
return
|
||||
|
@ -102,7 +102,7 @@ func (n *NotificationHandler) Post() {
|
|||
|
||||
go api.TriggerReplicationByRepository(pro.ProjectID, repository, []string{tag}, models.RepOpTransfer)
|
||||
if autoScanEnabled(project) {
|
||||
if err := api.TriggerImageScan(repository, tag); err != nil {
|
||||
if err := uiutils.TriggerImageScan(repository, tag); err != nil {
|
||||
log.Warningf("Failed to scan image, repository: %s, tag: %s, error: %v", repository, tag, err)
|
||||
}
|
||||
}
|
||||
|
@ -160,9 +160,7 @@ func autoScanEnabled(projectName string) bool {
|
|||
return false
|
||||
}
|
||||
if config.WithAdmiral() {
|
||||
//TODO get a project manager based on service account.
|
||||
var pm *pms.ProjectManager = pms.NewProjectManager("", "")
|
||||
p, err := pm.Get(projectName)
|
||||
p, err := config.GlobalProjectMgr.Get(projectName)
|
||||
if err != nil {
|
||||
log.Warningf("failed to get project, error: %v", err)
|
||||
return false
|
||||
|
|
141
src/ui/utils/utils.go
Normal file
141
src/ui/utils/utils.go
Normal file
|
@ -0,0 +1,141 @@
|
|||
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package utils contains methods to support security, cache, and webhook functions.
|
||||
package utils
|
||||
|
||||
import (
|
||||
"github.com/vmware/harbor/src/common/dao"
|
||||
"github.com/vmware/harbor/src/common/models"
|
||||
"github.com/vmware/harbor/src/common/utils/log"
|
||||
"github.com/vmware/harbor/src/common/utils/registry"
|
||||
"github.com/vmware/harbor/src/common/utils/registry/auth"
|
||||
"github.com/vmware/harbor/src/ui/config"
|
||||
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// ScanAllImages scans all images of Harbor by submiting jobs to jobservice, the whole process will move one if failed to subit any job of a single image.
|
||||
func ScanAllImages() error {
|
||||
regURL, err := config.RegistryURL()
|
||||
if err != nil {
|
||||
log.Errorf("Failed to load registry url")
|
||||
return err
|
||||
}
|
||||
repos, err := dao.GetAllRepositories()
|
||||
if err != nil {
|
||||
log.Errorf("Failed to list all repositories, error: %v", err)
|
||||
return err
|
||||
}
|
||||
log.Infof("Rescanning all images.")
|
||||
|
||||
go func() {
|
||||
var repoClient *registry.Repository
|
||||
var err error
|
||||
var tags []string
|
||||
for _, r := range repos {
|
||||
repoClient, err = NewRepositoryClientForUI(regURL, true, "harbor-ui", r.Name, "pull")
|
||||
if err != nil {
|
||||
log.Errorf("Failed to initialize client for repository: %s, error: %v, skip scanning", r.Name, err)
|
||||
continue
|
||||
}
|
||||
tags, err = repoClient.ListTag()
|
||||
if err != nil {
|
||||
log.Errorf("Failed to get tags for repository: %s, error: %v, skip scanning.", r.Name, err)
|
||||
continue
|
||||
}
|
||||
for _, t := range tags {
|
||||
if err = TriggerImageScan(r.Name, t); err != nil {
|
||||
log.Errorf("Failed to scan image with repository: %s, tag: %s, error: %v.", r.Name, t, err)
|
||||
} else {
|
||||
log.Debugf("Triggered scan for image with repository: %s, tag: %s", r.Name, t)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
// RequestAsUI is a shortcut to make a request attach UI secret and send the request.
|
||||
// Do not use this when you want to handle the response
|
||||
// TODO: add a response handler to replace expectSC *when needed*
|
||||
func RequestAsUI(method, url string, body io.Reader, expectSC int) error {
|
||||
req, err := http.NewRequest(method, url, body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
AddUISecret(req)
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != expectSC {
|
||||
b, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fmt.Errorf("Unexpected status code: %d, text: %s", resp.StatusCode, string(b))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
//AddUISecret add secret cookie to a request
|
||||
func AddUISecret(req *http.Request) {
|
||||
if req != nil {
|
||||
req.AddCookie(&http.Cookie{
|
||||
Name: models.UISecretCookie,
|
||||
Value: config.UISecret(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TriggerImageScan triggers an image scan job on jobservice.
|
||||
func TriggerImageScan(repository string, tag string) error {
|
||||
data := &models.ImageScanReq{
|
||||
Repo: repository,
|
||||
Tag: tag,
|
||||
}
|
||||
b, err := json.Marshal(&data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
url := fmt.Sprintf("%s/api/jobs/scan", config.InternalJobServiceURL())
|
||||
return RequestAsUI("POST", url, bytes.NewBuffer(b), http.StatusOK)
|
||||
}
|
||||
|
||||
// NewRepositoryClientForUI ...
|
||||
// TODO need a registry client which accept a raw token as param
|
||||
func NewRepositoryClientForUI(endpoint string, insecure bool, username, repository string,
|
||||
scopeActions ...string) (*registry.Repository, error) {
|
||||
|
||||
authorizer := auth.NewRegistryUsernameTokenAuthorizer(username, "repository", repository, scopeActions...)
|
||||
store, err := auth.NewAuthorizerStore(endpoint, insecure, authorizer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client, err := registry.NewRepositoryWithModifiers(repository, endpoint, insecure, store)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client, nil
|
||||
}
|
|
@ -12,10 +12,10 @@
|
|||
</div>
|
||||
<div class="statistic-column-block" style="margin-left: 16px;">
|
||||
<div>
|
||||
<statistics [data]='originalCopy.my_project_count' [label]='"STATISTICS.INDEX_PRIVATE" | translate'></statistics>
|
||||
<statistics [data]='originalCopy.private_project_count' [label]='"STATISTICS.INDEX_PRIVATE" | translate'></statistics>
|
||||
</div>
|
||||
<div>
|
||||
<statistics [data]='originalCopy.my_repo_count' [label]='"STATISTICS.INDEX_PRIVATE" | translate'></statistics>
|
||||
<statistics [data]='originalCopy.private_repo_count' [label]='"STATISTICS.INDEX_PRIVATE" | translate'></statistics>
|
||||
</div>
|
||||
</div>
|
||||
<div class="statistic-column-block" style="margin-left: 28px;">
|
||||
|
|
|
@ -14,8 +14,8 @@
|
|||
export class Statistics {
|
||||
constructor() {}
|
||||
|
||||
my_project_count: number;
|
||||
my_repo_count: number;
|
||||
private_project_count: number;
|
||||
private_repo_count: number;
|
||||
public_project_count: number;
|
||||
public_repo_count: number;
|
||||
total_project_count: number;
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
/*
|
||||
/*
|
||||
* Harbor API
|
||||
*
|
||||
* These APIs provide services for manipulating Harbor project.
|
||||
*
|
||||
* OpenAPI spec version: 0.3.0
|
||||
*
|
||||
*
|
||||
* Generated by: https://github.com/swagger-api/swagger-codegen.git
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
@ -24,11 +24,11 @@ package apilib
|
|||
|
||||
type StatisticMap struct {
|
||||
|
||||
// The count of the projects which the user is a member of.
|
||||
MyProjectCount int32 `json:"my_project_count,omitempty"`
|
||||
// The count of the private projects which the user is a member of.
|
||||
PrivateProjectCount int32 `json:"private_project_count,omitempty"`
|
||||
|
||||
// The count of the repositories belonging to the projects which the user is a member of.
|
||||
MyRepoCount int32 `json:"my_repo_count,omitempty"`
|
||||
// The count of the private repositories belonging to the projects which the user is a member of.
|
||||
PrivateRepoCount int32 `json:"private_repo_count,omitempty"`
|
||||
|
||||
// The count of the public projects.
|
||||
PublicProjectCount int32 `json:"public_project_count,omitempty"`
|
||||
|
|
|
@ -17,12 +17,12 @@ fi
|
|||
# Create CA certificate
|
||||
openssl req \
|
||||
-newkey rsa:4096 -nodes -sha256 -keyout harbor_ca.key \
|
||||
-x509 -days 365 -out harbor_ca.crt -subj '/C=CN/ST=PEK/L=Bei Jing/O=VMware/CN=Harbor CA'
|
||||
-x509 -days 365 -out harbor_ca.crt -subj '/C=CN/ST=PEK/L=Bei Jing/O=VMware/CN=HarborCA'
|
||||
|
||||
# Generate a Certificate Signing Request
|
||||
openssl req \
|
||||
-newkey rsa:4096 -nodes -sha256 -keyout $IP.key \
|
||||
-out $IP.csr -subj '/C=CN/ST=PEK/L=Bei Jing/O=VMware/CN=Harbor CA'
|
||||
-out $IP.csr -subj '/C=CN/ST=PEK/L=Bei Jing/O=VMware/CN=HarborManager'
|
||||
|
||||
# Generate the certificate of local registry host
|
||||
echo subjectAltName = IP:$IP > extfile.cnf
|
||||
|
@ -32,4 +32,4 @@ openssl x509 -req -days 365 -in $IP.csr -CA harbor_ca.crt \
|
|||
# Copy to harbor default location
|
||||
mkdir -p /data/cert
|
||||
cp $IP.crt /data/cert/server.crt
|
||||
cp $IP.key /data/cert/server.key
|
||||
cp $IP.key /data/cert/server.key
|
||||
|
|
|
@ -139,4 +139,40 @@ class Repository(Base):
|
|||
creation_time = sa.Column(mysql.TIMESTAMP, server_default = sa.text("CURRENT_TIMESTAMP"))
|
||||
update_time = sa.Column(mysql.TIMESTAMP, server_default = sa.text("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP"))
|
||||
|
||||
class AccessLog(Base):
|
||||
__tablename__ = "access_log"
|
||||
|
||||
user_id = sa.Column(sa.Integer, nullable=False)
|
||||
log_id = sa.Column(sa.Integer, primary_key=True)
|
||||
username = sa.Column(sa.String(32), nullable=False)
|
||||
project_id = sa.Column(sa.Integer, nullable=False)
|
||||
repo_name = sa.Column(sa.String(256))
|
||||
repo_tag = sa.Column(sa.String(128))
|
||||
GUID = sa.Column(sa.String(64))
|
||||
operation = sa.Column(sa.String(20))
|
||||
op_time = sa.Column(mysql.TIMESTAMP)
|
||||
update_time = sa.Column(mysql.TIMESTAMP, server_default = sa.text("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP"))
|
||||
|
||||
__table_args__ = (sa.Index('project_id', "op_time"),)
|
||||
|
||||
class ImageScanJob(Base):
|
||||
__tablename__ = "img_scan_job"
|
||||
|
||||
id = sa.Column(sa.Integer, nullable=False, primary_key=True)
|
||||
status = sa.Column(sa.String(64), nullable=False)
|
||||
repository = sa.Column(sa.String(256), nullable=False)
|
||||
tag = sa.Column(sa.String(128), nullable=False)
|
||||
digest = sa.Column(sa.String(128))
|
||||
creation_time = sa.Column(mysql.TIMESTAMP, server_default = sa.text("CURRENT_TIMESTAMP"))
|
||||
update_time = sa.Column(mysql.TIMESTAMP, server_default = sa.text("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP"))
|
||||
|
||||
class ImageScanOverview(Base):
|
||||
__tablename__ = "img_scan_overview"
|
||||
|
||||
scan_job_id = sa.Column(sa.Integer, nullable=False)
|
||||
image_digest = sa.Column(sa.String(128), nullable=False, primary_key=True)
|
||||
severity = sa.Column(sa.Integer, nullable=False, server_default=sa.text("'0'"))
|
||||
components_overview = sa.Column(sa.String(2048))
|
||||
details_key = sa.Column(sa.String(128))
|
||||
creation_time = sa.Column(mysql.TIMESTAMP, server_default = sa.text("CURRENT_TIMESTAMP"))
|
||||
update_time = sa.Column(mysql.TIMESTAMP, server_default = sa.text("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP"))
|
68
tools/migration/migration_harbor/versions/1_2_0.py
Normal file
68
tools/migration/migration_harbor/versions/1_2_0.py
Normal file
|
@ -0,0 +1,68 @@
|
|||
# Copyright (c) 2008-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.
|
||||
|
||||
"""0.4.0 to 1.2.0
|
||||
|
||||
Revision ID: 0.4.0
|
||||
Revises:
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '1.2.0'
|
||||
down_revision = '0.4.0'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
from alembic import op
|
||||
from db_meta import *
|
||||
|
||||
from sqlalchemy.dialects import mysql
|
||||
|
||||
Session = sessionmaker()
|
||||
|
||||
def upgrade():
|
||||
"""
|
||||
update schema&data
|
||||
"""
|
||||
bind = op.get_bind()
|
||||
session = Session(bind=bind)
|
||||
|
||||
#delete column access_log.user_id(access_log_ibfk_1), access_log.project_id(access_log_ibfk_2)
|
||||
op.drop_constraint('access_log_ibfk_1', 'access_log', type_='foreignkey')
|
||||
op.drop_constraint('access_log_ibfk_2', 'access_log', type_='foreignkey')
|
||||
|
||||
#add colume username to access_log
|
||||
op.add_column('access_log', sa.Column('username', mysql.VARCHAR(32), nullable=False))
|
||||
|
||||
#init username
|
||||
session.query(AccessLog).update({AccessLog.username: ""})
|
||||
|
||||
#update access_log username
|
||||
user_all = session.query(User).all()
|
||||
for user in user_all:
|
||||
session.query(AccessLog).filter(AccessLog.user_id == user.user_id).update({AccessLog.username: user.username}, synchronize_session='fetch')
|
||||
|
||||
op.drop_column("access_log", "user_id")
|
||||
op.drop_column("repository", "owner_id")
|
||||
|
||||
#create tables: img_scan_job, img_scan_overview
|
||||
ImageScanJob.__table__.create(bind)
|
||||
ImageScanOverview.__table__.create(bind)
|
||||
|
||||
def downgrade():
|
||||
"""
|
||||
Downgrade has been disabled.
|
||||
"""
|
||||
pass
|
Loading…
Reference in New Issue
Block a user