mirror of
https://github.com/goharbor/harbor.git
synced 2024-11-22 10:15:35 +01:00
refactor(api): generate project apis by go-swagger
Signed-off-by: He Weiwei <hweiwei@vmware.com>
This commit is contained in:
parent
6c168174bd
commit
f309896f2f
2
Makefile
2
Makefile
@ -310,7 +310,7 @@ endif
|
|||||||
SWAGGER_IMAGENAME=goharbor/swagger
|
SWAGGER_IMAGENAME=goharbor/swagger
|
||||||
SWAGGER_VERSION=v0.21.0
|
SWAGGER_VERSION=v0.21.0
|
||||||
SWAGGER=$(DOCKERCMD) run --rm -u $(shell id -u):$(shell id -g) -v $(BUILDPATH):$(BUILDPATH) -w $(BUILDPATH) ${SWAGGER_IMAGENAME}:${SWAGGER_VERSION}
|
SWAGGER=$(DOCKERCMD) run --rm -u $(shell id -u):$(shell id -g) -v $(BUILDPATH):$(BUILDPATH) -w $(BUILDPATH) ${SWAGGER_IMAGENAME}:${SWAGGER_VERSION}
|
||||||
SWAGGER_GENERATE_SERVER=${SWAGGER} generate server --template-dir=$(TOOLSPATH)/swagger/templates --exclude-main
|
SWAGGER_GENERATE_SERVER=${SWAGGER} generate server --template-dir=$(TOOLSPATH)/swagger/templates --exclude-main --additional-initialism=CVE
|
||||||
SWAGGER_IMAGE_BUILD_CMD=${DOCKERBUILD} -f ${TOOLSPATH}/swagger/Dockerfile --build-arg SWAGGER_VERSION=${SWAGGER_VERSION} -t ${SWAGGER_IMAGENAME}:$(SWAGGER_VERSION) .
|
SWAGGER_IMAGE_BUILD_CMD=${DOCKERBUILD} -f ${TOOLSPATH}/swagger/Dockerfile --build-arg SWAGGER_VERSION=${SWAGGER_VERSION} -t ${SWAGGER_IMAGENAME}:$(SWAGGER_VERSION) .
|
||||||
|
|
||||||
SWAGGER_IMAGENAME:
|
SWAGGER_IMAGENAME:
|
||||||
|
@ -53,214 +53,6 @@ paths:
|
|||||||
$ref: '#/definitions/Search'
|
$ref: '#/definitions/Search'
|
||||||
'500':
|
'500':
|
||||||
description: Unexpected internal errors.
|
description: Unexpected internal errors.
|
||||||
/projects:
|
|
||||||
get:
|
|
||||||
summary: List projects
|
|
||||||
description: |
|
|
||||||
This endpoint returns all projects created by Harbor, and can be filtered by project name.
|
|
||||||
parameters:
|
|
||||||
- name: name
|
|
||||||
in: query
|
|
||||||
description: The name of project.
|
|
||||||
required: false
|
|
||||||
type: string
|
|
||||||
- name: public
|
|
||||||
in: query
|
|
||||||
description: The project is public or private.
|
|
||||||
required: false
|
|
||||||
type: boolean
|
|
||||||
format: int32
|
|
||||||
- name: owner
|
|
||||||
in: query
|
|
||||||
description: The name of project owner.
|
|
||||||
required: false
|
|
||||||
type: string
|
|
||||||
- name: page
|
|
||||||
in: query
|
|
||||||
type: integer
|
|
||||||
format: int32
|
|
||||||
required: false
|
|
||||||
description: 'The page number, default is 1.'
|
|
||||||
- name: page_size
|
|
||||||
in: query
|
|
||||||
type: integer
|
|
||||||
format: int32
|
|
||||||
required: false
|
|
||||||
description: 'The size of per page, default is 10, maximum is 100.'
|
|
||||||
tags:
|
|
||||||
- Products
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: Return all matched projects.
|
|
||||||
schema:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
$ref: '#/definitions/Project'
|
|
||||||
headers:
|
|
||||||
X-Total-Count:
|
|
||||||
description: The total count of projects
|
|
||||||
type: integer
|
|
||||||
Link:
|
|
||||||
description: Link refers to the previous page and next page
|
|
||||||
type: string
|
|
||||||
'401':
|
|
||||||
description: User need to log in first.
|
|
||||||
'500':
|
|
||||||
description: Internal errors.
|
|
||||||
head:
|
|
||||||
summary: Check if the project name user provided already exists.
|
|
||||||
description: |
|
|
||||||
This endpoint is used to check if the project name user provided already exist.
|
|
||||||
parameters:
|
|
||||||
- name: project_name
|
|
||||||
in: query
|
|
||||||
description: Project name for checking exists.
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
tags:
|
|
||||||
- Products
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: Project name exists.
|
|
||||||
'404':
|
|
||||||
description: Project name does not exist.
|
|
||||||
'500':
|
|
||||||
description: Unexpected internal errors.
|
|
||||||
post:
|
|
||||||
summary: Create a new project.
|
|
||||||
description: |
|
|
||||||
This endpoint is for user to create a new project.
|
|
||||||
parameters:
|
|
||||||
- name: project
|
|
||||||
in: body
|
|
||||||
description: New created project.
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/ProjectReq'
|
|
||||||
tags:
|
|
||||||
- Products
|
|
||||||
responses:
|
|
||||||
'201':
|
|
||||||
description: Project created successfully.
|
|
||||||
'400':
|
|
||||||
description: Unsatisfied with constraints of the project creation.
|
|
||||||
'401':
|
|
||||||
description: User need to log in first.
|
|
||||||
'409':
|
|
||||||
description: Project name already exists.
|
|
||||||
'415':
|
|
||||||
$ref: '#/responses/UnsupportedMediaType'
|
|
||||||
'500':
|
|
||||||
description: Unexpected internal errors.
|
|
||||||
'/projects/{project_id}':
|
|
||||||
get:
|
|
||||||
summary: Return specific project detail information
|
|
||||||
description: |
|
|
||||||
This endpoint returns specific project information by project ID.
|
|
||||||
parameters:
|
|
||||||
- name: project_id
|
|
||||||
in: path
|
|
||||||
description: Project ID for filtering results.
|
|
||||||
required: true
|
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
tags:
|
|
||||||
- Products
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: Return matched project information.
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/Project'
|
|
||||||
'401':
|
|
||||||
description: User need to log in first.
|
|
||||||
'500':
|
|
||||||
description: Internal errors.
|
|
||||||
put:
|
|
||||||
summary: Update properties for a selected project.
|
|
||||||
description: |
|
|
||||||
This endpoint is aimed to update the properties of a project.
|
|
||||||
parameters:
|
|
||||||
- name: project_id
|
|
||||||
in: path
|
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
required: true
|
|
||||||
description: Selected project ID.
|
|
||||||
- name: project
|
|
||||||
in: body
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/ProjectReq'
|
|
||||||
description: Updates of project.
|
|
||||||
tags:
|
|
||||||
- Products
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: Updated project properties successfully.
|
|
||||||
'400':
|
|
||||||
description: Illegal format of provided ID value.
|
|
||||||
'401':
|
|
||||||
description: User need to log in first.
|
|
||||||
'403':
|
|
||||||
description: User does not have permission to the project.
|
|
||||||
'404':
|
|
||||||
description: Project ID does not exist.
|
|
||||||
'500':
|
|
||||||
description: Unexpected internal errors.
|
|
||||||
delete:
|
|
||||||
summary: Delete project by projectID
|
|
||||||
description: |
|
|
||||||
This endpoint is aimed to delete project by project ID.
|
|
||||||
parameters:
|
|
||||||
- name: project_id
|
|
||||||
in: path
|
|
||||||
description: Project ID of project which will be deleted.
|
|
||||||
required: true
|
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
tags:
|
|
||||||
- Products
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: Project is deleted successfully.
|
|
||||||
'400':
|
|
||||||
description: Invalid project id.
|
|
||||||
'403':
|
|
||||||
description: User need to log in first.
|
|
||||||
'404':
|
|
||||||
description: Project does not exist.
|
|
||||||
'412':
|
|
||||||
description: 'Project contains policies, can not be deleted.'
|
|
||||||
'500':
|
|
||||||
description: Internal errors.
|
|
||||||
'/projects/{project_id}/summary':
|
|
||||||
get:
|
|
||||||
summary: Get summary of the project.
|
|
||||||
description: Get summary of the project.
|
|
||||||
parameters:
|
|
||||||
- name: project_id
|
|
||||||
in: path
|
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
required: true
|
|
||||||
description: Relevant project ID
|
|
||||||
tags:
|
|
||||||
- Products
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: Get summary of the project successfully.
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/ProjectSummary'
|
|
||||||
'400':
|
|
||||||
description: Illegal format of provided ID value.
|
|
||||||
'401':
|
|
||||||
description: User need to log in first.
|
|
||||||
'403':
|
|
||||||
description: User does not have permission to get summary of the project.
|
|
||||||
'404':
|
|
||||||
description: Project ID does not exist.
|
|
||||||
'500':
|
|
||||||
description: Unexpected internal errors.
|
|
||||||
'/projects/{project_id}/metadatas':
|
'/projects/{project_id}/metadatas':
|
||||||
get:
|
get:
|
||||||
summary: Get project metadata.
|
summary: Get project metadata.
|
||||||
|
@ -20,6 +20,213 @@ security:
|
|||||||
- basic: []
|
- basic: []
|
||||||
- {}
|
- {}
|
||||||
paths:
|
paths:
|
||||||
|
/projects:
|
||||||
|
get:
|
||||||
|
summary: List projects
|
||||||
|
description: This endpoint returns projects created by Harbor.
|
||||||
|
tags:
|
||||||
|
- project
|
||||||
|
operationId: listProjects
|
||||||
|
parameters:
|
||||||
|
- $ref: '#/parameters/requestId'
|
||||||
|
- $ref: '#/parameters/page'
|
||||||
|
- $ref: '#/parameters/pageSize'
|
||||||
|
- name: name
|
||||||
|
in: query
|
||||||
|
description: The name of project.
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
- name: public
|
||||||
|
in: query
|
||||||
|
description: The project is public or private.
|
||||||
|
required: false
|
||||||
|
type: boolean
|
||||||
|
format: int32
|
||||||
|
- name: owner
|
||||||
|
in: query
|
||||||
|
description: The name of project owner.
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Return all matched projects.
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/Project'
|
||||||
|
headers:
|
||||||
|
X-Total-Count:
|
||||||
|
description: The total count of projects
|
||||||
|
type: integer
|
||||||
|
Link:
|
||||||
|
description: Link refers to the previous page and next page
|
||||||
|
type: string
|
||||||
|
'401':
|
||||||
|
$ref: '#/responses/401'
|
||||||
|
'500':
|
||||||
|
$ref: '#/responses/500'
|
||||||
|
head:
|
||||||
|
summary: Check if the project name user provided already exists.
|
||||||
|
description: This endpoint is used to check if the project name provided already exist.
|
||||||
|
tags:
|
||||||
|
- project
|
||||||
|
operationId: headProject
|
||||||
|
parameters:
|
||||||
|
- $ref: '#/parameters/requestId'
|
||||||
|
- name: project_name
|
||||||
|
in: query
|
||||||
|
description: Project name for checking exists.
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
$ref: '#/responses/200'
|
||||||
|
'404':
|
||||||
|
$ref: '#/responses/404'
|
||||||
|
'500':
|
||||||
|
$ref: '#/responses/500'
|
||||||
|
post:
|
||||||
|
summary: Create a new project.
|
||||||
|
description: This endpoint is for user to create a new project.
|
||||||
|
tags:
|
||||||
|
- project
|
||||||
|
operationId: createProject
|
||||||
|
parameters:
|
||||||
|
- $ref: '#/parameters/requestId'
|
||||||
|
- name: project
|
||||||
|
in: body
|
||||||
|
description: New created project.
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/ProjectReq'
|
||||||
|
responses:
|
||||||
|
'201':
|
||||||
|
$ref: '#/responses/201'
|
||||||
|
'400':
|
||||||
|
$ref: '#/responses/400'
|
||||||
|
'401':
|
||||||
|
$ref: '#/responses/401'
|
||||||
|
'409':
|
||||||
|
$ref: '#/responses/409'
|
||||||
|
'500':
|
||||||
|
$ref: '#/responses/500'
|
||||||
|
'/projects/{project_id}':
|
||||||
|
get:
|
||||||
|
summary: Return specific project detail information
|
||||||
|
description: This endpoint returns specific project information by project ID.
|
||||||
|
tags:
|
||||||
|
- project
|
||||||
|
operationId: getProject
|
||||||
|
parameters:
|
||||||
|
- $ref: '#/parameters/requestId'
|
||||||
|
- $ref: '#/parameters/projectId'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Return matched project information.
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/Project'
|
||||||
|
'401':
|
||||||
|
$ref: '#/responses/401'
|
||||||
|
'500':
|
||||||
|
$ref: '#/responses/500'
|
||||||
|
put:
|
||||||
|
summary: Update properties for a selected project.
|
||||||
|
description: This endpoint is aimed to update the properties of a project.
|
||||||
|
tags:
|
||||||
|
- project
|
||||||
|
operationId: updateProject
|
||||||
|
parameters:
|
||||||
|
- $ref: '#/parameters/requestId'
|
||||||
|
- $ref: '#/parameters/projectId'
|
||||||
|
- name: project
|
||||||
|
in: body
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/ProjectReq'
|
||||||
|
description: Updates of project.
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
$ref: '#/responses/200'
|
||||||
|
'400':
|
||||||
|
$ref: '#/responses/400'
|
||||||
|
'401':
|
||||||
|
$ref: '#/responses/401'
|
||||||
|
'403':
|
||||||
|
$ref: '#/responses/403'
|
||||||
|
'404':
|
||||||
|
$ref: '#/responses/404'
|
||||||
|
'500':
|
||||||
|
$ref: '#/responses/500'
|
||||||
|
delete:
|
||||||
|
summary: Delete project by projectID
|
||||||
|
description: This endpoint is aimed to delete project by project ID.
|
||||||
|
tags:
|
||||||
|
- project
|
||||||
|
operationId: deleteProject
|
||||||
|
parameters:
|
||||||
|
- $ref: '#/parameters/requestId'
|
||||||
|
- $ref: '#/parameters/projectId'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
$ref: '#/responses/200'
|
||||||
|
'400':
|
||||||
|
$ref: '#/responses/400'
|
||||||
|
'403':
|
||||||
|
$ref: '#/responses/403'
|
||||||
|
'404':
|
||||||
|
$ref: '#/responses/404'
|
||||||
|
'412':
|
||||||
|
$ref: '#/responses/412'
|
||||||
|
'500':
|
||||||
|
$ref: '#/responses/500'
|
||||||
|
/projects/{project_id}/_deletable:
|
||||||
|
get:
|
||||||
|
summary: Get the deletable status of the project
|
||||||
|
description: Get the deletable status of the project
|
||||||
|
tags:
|
||||||
|
- project
|
||||||
|
operationId: getProjectDeletable
|
||||||
|
parameters:
|
||||||
|
- $ref: '#/parameters/requestId'
|
||||||
|
- $ref: '#/parameters/projectId'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Return deletable status of the project.
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/ProjectDeletable'
|
||||||
|
'401':
|
||||||
|
$ref: '#/responses/401'
|
||||||
|
'403':
|
||||||
|
$ref: '#/responses/403'
|
||||||
|
'404':
|
||||||
|
$ref: '#/responses/404'
|
||||||
|
'500':
|
||||||
|
$ref: '#/responses/500'
|
||||||
|
'/projects/{project_id}/summary':
|
||||||
|
get:
|
||||||
|
summary: Get summary of the project.
|
||||||
|
description: Get summary of the project.
|
||||||
|
tags:
|
||||||
|
- project
|
||||||
|
operationId: getProjectSummary
|
||||||
|
parameters:
|
||||||
|
- $ref: '#/parameters/requestId'
|
||||||
|
- $ref: '#/parameters/projectId'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Get summary of the project successfully.
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/ProjectSummary'
|
||||||
|
'400':
|
||||||
|
$ref: '#/responses/400'
|
||||||
|
'401':
|
||||||
|
$ref: '#/responses/401'
|
||||||
|
'403':
|
||||||
|
$ref: '#/responses/403'
|
||||||
|
'404':
|
||||||
|
$ref: '#/responses/404'
|
||||||
|
'500':
|
||||||
|
$ref: '#/responses/500'
|
||||||
/projects/{project_name}/repositories:
|
/projects/{project_name}/repositories:
|
||||||
get:
|
get:
|
||||||
summary: List repositories
|
summary: List repositories
|
||||||
@ -1252,7 +1459,8 @@ parameters:
|
|||||||
in: path
|
in: path
|
||||||
description: The ID of the project
|
description: The ID of the project
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: integer
|
||||||
|
format: int64
|
||||||
repositoryName:
|
repositoryName:
|
||||||
name: repository_name
|
name: repository_name
|
||||||
in: path
|
in: path
|
||||||
@ -1293,6 +1501,7 @@ parameters:
|
|||||||
required: false
|
required: false
|
||||||
description: The size of per page
|
description: The size of per page
|
||||||
default: 10
|
default: 10
|
||||||
|
maximum: 100
|
||||||
instanceName:
|
instanceName:
|
||||||
name: preheat_instance_name
|
name: preheat_instance_name
|
||||||
in: path
|
in: path
|
||||||
@ -1387,6 +1596,14 @@ responses:
|
|||||||
type: string
|
type: string
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/Errors'
|
$ref: '#/definitions/Errors'
|
||||||
|
'412':
|
||||||
|
description: Precondition failed
|
||||||
|
headers:
|
||||||
|
X-Request-Id:
|
||||||
|
description: The ID of the corresponding request for the response
|
||||||
|
type: string
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/Errors'
|
||||||
'500':
|
'500':
|
||||||
description: Internal server error
|
description: Internal server error
|
||||||
headers:
|
headers:
|
||||||
@ -1958,3 +2175,243 @@ definitions:
|
|||||||
content:
|
content:
|
||||||
type: string
|
type: string
|
||||||
description: The base64 encoded content of the icon
|
description: The base64 encoded content of the icon
|
||||||
|
ProjectReq:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
project_name:
|
||||||
|
type: string
|
||||||
|
description: The name of the project.
|
||||||
|
public:
|
||||||
|
type: boolean
|
||||||
|
description: deprecated, reserved for project creation in replication
|
||||||
|
x-nullable: true
|
||||||
|
metadata:
|
||||||
|
description: The metadata of the project.
|
||||||
|
$ref: '#/definitions/ProjectMetadata'
|
||||||
|
cve_allowlist:
|
||||||
|
description: The CVE allowlist of the project.
|
||||||
|
$ref: '#/definitions/CVEAllowlist'
|
||||||
|
storage_limit:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
description: The storage quota of the project.
|
||||||
|
x-nullable: true
|
||||||
|
registry_id:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
description: The ID of referenced registry when creating the proxy cache project
|
||||||
|
x-nullable: true
|
||||||
|
Project:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
project_id:
|
||||||
|
type: integer
|
||||||
|
format: int32
|
||||||
|
description: Project ID
|
||||||
|
owner_id:
|
||||||
|
type: integer
|
||||||
|
format: int32
|
||||||
|
description: The owner ID of the project always means the creator of the project.
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
description: The name of the project.
|
||||||
|
registry_id:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
description: The ID of referenced registry when the project is a proxy cache project.
|
||||||
|
creation_time:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
description: The creation time of the project.
|
||||||
|
update_time:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
description: The update time of the project.
|
||||||
|
deleted:
|
||||||
|
type: boolean
|
||||||
|
description: A deletion mark of the project.
|
||||||
|
owner_name:
|
||||||
|
type: string
|
||||||
|
description: The owner name of the project.
|
||||||
|
togglable:
|
||||||
|
type: boolean
|
||||||
|
description: Correspond to the UI about whether the project's publicity is updatable (for UI)
|
||||||
|
current_user_role_id:
|
||||||
|
type: integer
|
||||||
|
description: The role ID with highest permission of the current user who triggered the API (for UI). This attribute is deprecated and will be removed in future versions.
|
||||||
|
current_user_role_ids:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: integer
|
||||||
|
format: int32
|
||||||
|
description: The list of role ID of the current user who triggered the API (for UI)
|
||||||
|
repo_count:
|
||||||
|
type: integer
|
||||||
|
description: The number of the repositories under this project.
|
||||||
|
chart_count:
|
||||||
|
type: integer
|
||||||
|
description: The total number of charts under this project.
|
||||||
|
metadata:
|
||||||
|
description: The metadata of the project.
|
||||||
|
$ref: '#/definitions/ProjectMetadata'
|
||||||
|
cve_allowlist:
|
||||||
|
description: The CVE allowlist of this project.
|
||||||
|
$ref: '#/definitions/CVEAllowlist'
|
||||||
|
ProjectDeletable:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
deletable:
|
||||||
|
type: boolean
|
||||||
|
description: Whether the project can be deleted.
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
description: The detail message when the project can not be deleted.
|
||||||
|
ProjectMetadata:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
public:
|
||||||
|
type: string
|
||||||
|
description: 'The public status of the project. The valid values are "true", "false".'
|
||||||
|
enable_content_trust:
|
||||||
|
type: string
|
||||||
|
description: 'Whether content trust is enabled or not. If it is enabled, user can''t pull unsigned images from this project. The valid values are "true", "false".'
|
||||||
|
x-nullable: true
|
||||||
|
prevent_vul:
|
||||||
|
type: string
|
||||||
|
description: 'Whether prevent the vulnerable images from running. The valid values are "true", "false".'
|
||||||
|
x-nullable: true
|
||||||
|
severity:
|
||||||
|
type: string
|
||||||
|
description: 'If the vulnerability is high than severity defined here, the images can''t be pulled. The valid values are "none", "low", "medium", "high", "critical".'
|
||||||
|
x-nullable: true
|
||||||
|
auto_scan:
|
||||||
|
type: string
|
||||||
|
description: 'Whether scan images automatically when pushing. The valid values are "true", "false".'
|
||||||
|
x-nullable: true
|
||||||
|
reuse_sys_cve_allowlist:
|
||||||
|
type: string
|
||||||
|
description: 'Whether this project reuse the system level CVE allowlist as the allowlist of its own. The valid values are "true", "false".
|
||||||
|
If it is set to "true" the actual allowlist associate with this project, if any, will be ignored.'
|
||||||
|
x-nullable: true
|
||||||
|
retention_id:
|
||||||
|
type: string
|
||||||
|
description: 'The ID of the tag retention policy for the project'
|
||||||
|
x-nullable: true
|
||||||
|
ProjectSummary:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
repo_count:
|
||||||
|
type: integer
|
||||||
|
description: The number of the repositories under this project.
|
||||||
|
chart_count:
|
||||||
|
type: integer
|
||||||
|
description: The total number of charts under this project.
|
||||||
|
project_admin_count:
|
||||||
|
type: integer
|
||||||
|
description: The total number of project admin members.
|
||||||
|
maintainer_count:
|
||||||
|
type: integer
|
||||||
|
description: The total number of maintainer members.
|
||||||
|
developer_count:
|
||||||
|
type: integer
|
||||||
|
description: The total number of developer members.
|
||||||
|
guest_count:
|
||||||
|
type: integer
|
||||||
|
description: The total number of guest members.
|
||||||
|
limited_guest_count:
|
||||||
|
type: integer
|
||||||
|
description: The total number of limited guest members.
|
||||||
|
quota:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
hard:
|
||||||
|
$ref: "#/definitions/ResourceList"
|
||||||
|
description: The hard limits of the quota
|
||||||
|
used:
|
||||||
|
$ref: "#/definitions/ResourceList"
|
||||||
|
description: The used status of the quota
|
||||||
|
registry:
|
||||||
|
$ref: "#/definitions/Registry"
|
||||||
|
CVEAllowlist:
|
||||||
|
type: object
|
||||||
|
description: The CVE Allowlist for system or project
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: integer
|
||||||
|
description: ID of the allowlist
|
||||||
|
project_id:
|
||||||
|
type: integer
|
||||||
|
description: ID of the project which the allowlist belongs to. For system level allowlist this attribute is zero.
|
||||||
|
expires_at:
|
||||||
|
type: integer
|
||||||
|
description: the time for expiration of the allowlist, in the form of seconds since epoch. This is an optional attribute, if it's not set the CVE allowlist does not expire.
|
||||||
|
x-nullable: true
|
||||||
|
items:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/definitions/CVEAllowlistItem"
|
||||||
|
creation_time:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
description: The creation time of the allowlist.
|
||||||
|
update_time:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
description: The update time of the allowlist.
|
||||||
|
CVEAllowlistItem:
|
||||||
|
type: object
|
||||||
|
description: The item in CVE allowlist
|
||||||
|
properties:
|
||||||
|
cve_id:
|
||||||
|
type: string
|
||||||
|
description: The ID of the CVE, such as "CVE-2019-10164"
|
||||||
|
RegistryCredential:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
type:
|
||||||
|
type: string
|
||||||
|
description: Credential type, such as 'basic', 'oauth'.
|
||||||
|
access_key:
|
||||||
|
type: string
|
||||||
|
description: Access key, e.g. user name when credential type is 'basic'.
|
||||||
|
access_secret:
|
||||||
|
type: string
|
||||||
|
description: Access secret, e.g. password when credential type is 'basic'.
|
||||||
|
Registry:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
description: The registry ID.
|
||||||
|
url:
|
||||||
|
type: string
|
||||||
|
description: The registry URL string.
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
description: The registry name.
|
||||||
|
credential:
|
||||||
|
$ref: '#/definitions/RegistryCredential'
|
||||||
|
type:
|
||||||
|
type: string
|
||||||
|
description: Type of the registry, e.g. 'harbor'.
|
||||||
|
insecure:
|
||||||
|
type: boolean
|
||||||
|
description: Whether or not the certificate will be verified when Harbor tries to access the server.
|
||||||
|
description:
|
||||||
|
type: string
|
||||||
|
description: Description of the registry.
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
description: Health status of the registry.
|
||||||
|
creation_time:
|
||||||
|
type: string
|
||||||
|
description: The create time of the policy.
|
||||||
|
update_time:
|
||||||
|
type: string
|
||||||
|
description: The update time of the policy.
|
||||||
|
ResourceList:
|
||||||
|
type: object
|
||||||
|
additionalProperties:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
@ -17,6 +17,8 @@ package dao
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/goharbor/harbor/src/common/models"
|
"github.com/goharbor/harbor/src/common/models"
|
||||||
"github.com/goharbor/harbor/src/lib/log"
|
"github.com/goharbor/harbor/src/lib/log"
|
||||||
)
|
)
|
||||||
@ -24,6 +26,9 @@ import (
|
|||||||
// CreateCVEAllowlist creates the CVE allowlist
|
// CreateCVEAllowlist creates the CVE allowlist
|
||||||
func CreateCVEAllowlist(l models.CVEAllowlist) (int64, error) {
|
func CreateCVEAllowlist(l models.CVEAllowlist) (int64, error) {
|
||||||
o := GetOrmer()
|
o := GetOrmer()
|
||||||
|
now := time.Now()
|
||||||
|
l.CreationTime = now
|
||||||
|
l.UpdateTime = now
|
||||||
itemsBytes, _ := json.Marshal(l.Items)
|
itemsBytes, _ := json.Marshal(l.Items)
|
||||||
l.ItemsText = string(itemsBytes)
|
l.ItemsText = string(itemsBytes)
|
||||||
return o.Insert(&l)
|
return o.Insert(&l)
|
||||||
@ -32,6 +37,8 @@ func CreateCVEAllowlist(l models.CVEAllowlist) (int64, error) {
|
|||||||
// UpdateCVEAllowlist Updates the vulnerability white list to DB
|
// UpdateCVEAllowlist Updates the vulnerability white list to DB
|
||||||
func UpdateCVEAllowlist(l models.CVEAllowlist) (int64, error) {
|
func UpdateCVEAllowlist(l models.CVEAllowlist) (int64, error) {
|
||||||
o := GetOrmer()
|
o := GetOrmer()
|
||||||
|
now := time.Now()
|
||||||
|
l.UpdateTime = now
|
||||||
itemsBytes, _ := json.Marshal(l.Items)
|
itemsBytes, _ := json.Marshal(l.Items)
|
||||||
l.ItemsText = string(itemsBytes)
|
l.ItemsText = string(itemsBytes)
|
||||||
id, err := o.InsertOrUpdate(&l, "project_id")
|
id, err := o.InsertOrUpdate(&l, "project_id")
|
||||||
|
@ -22,7 +22,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/astaxie/beego/orm"
|
"github.com/astaxie/beego/orm"
|
||||||
"github.com/goharbor/harbor/src/lib/q"
|
|
||||||
"github.com/goharbor/harbor/src/pkg/quota/types"
|
"github.com/goharbor/harbor/src/pkg/quota/types"
|
||||||
"github.com/goharbor/harbor/src/replication/model"
|
"github.com/goharbor/harbor/src/replication/model"
|
||||||
)
|
)
|
||||||
@ -154,6 +153,10 @@ func (p *Project) FilterByMember(ctx context.Context, qs orm.QuerySeter, key str
|
|||||||
subQuery = fmt.Sprintf("%s AND pm.role = %d", subQuery, query.Role)
|
subQuery = fmt.Sprintf("%s AND pm.role = %d", subQuery, query.Role)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if query.WithPublic {
|
||||||
|
subQuery = fmt.Sprintf("(%s) UNION (SELECT project_id FROM project_metadata WHERE name = 'public' AND value = 'true')", subQuery)
|
||||||
|
}
|
||||||
|
|
||||||
if len(query.GroupIDs) > 0 {
|
if len(query.GroupIDs) > 0 {
|
||||||
var elems []string
|
var elems []string
|
||||||
for _, groupID := range query.GroupIDs {
|
for _, groupID := range query.GroupIDs {
|
||||||
@ -210,41 +213,13 @@ type ProjectQueryParam struct {
|
|||||||
ProjectIDs []int64 // project ID list
|
ProjectIDs []int64 // project ID list
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToQuery returns q.Query from param
|
|
||||||
func (param *ProjectQueryParam) ToQuery() *q.Query {
|
|
||||||
kw := q.KeyWords{}
|
|
||||||
if param.Name != "" {
|
|
||||||
kw["name"] = q.FuzzyMatchValue{Value: param.Name}
|
|
||||||
}
|
|
||||||
if param.Owner != "" {
|
|
||||||
kw["owner"] = param.Owner
|
|
||||||
}
|
|
||||||
if param.Public != nil {
|
|
||||||
kw["public"] = *param.Public
|
|
||||||
}
|
|
||||||
if param.RegistryID != 0 {
|
|
||||||
kw["registry_id"] = param.RegistryID
|
|
||||||
}
|
|
||||||
if len(param.ProjectIDs) > 0 {
|
|
||||||
kw["project_id__in"] = param.ProjectIDs
|
|
||||||
}
|
|
||||||
if param.Member != nil {
|
|
||||||
kw["member"] = param.Member
|
|
||||||
}
|
|
||||||
|
|
||||||
query := q.New(kw)
|
|
||||||
if param.Pagination != nil {
|
|
||||||
query.PageNumber = param.Pagination.Page
|
|
||||||
query.PageSize = param.Pagination.Size
|
|
||||||
}
|
|
||||||
return query
|
|
||||||
}
|
|
||||||
|
|
||||||
// MemberQuery filter by member's username and role
|
// MemberQuery filter by member's username and role
|
||||||
type MemberQuery struct {
|
type MemberQuery struct {
|
||||||
Name string // the username of member
|
Name string // the username of member
|
||||||
Role int // the role of the member has to the project
|
Role int // the role of the member has to the project
|
||||||
GroupIDs []int // the group ID of current user belongs to
|
GroupIDs []int // the group ID of current user belongs to
|
||||||
|
|
||||||
|
WithPublic bool // include the public projects for the member
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pagination ...
|
// Pagination ...
|
||||||
|
41
src/controller/event/operator/operator.go
Normal file
41
src/controller/event/operator/operator.go
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
// Copyright Project Harbor Authors
|
||||||
|
//
|
||||||
|
// 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 operator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/goharbor/harbor/src/common/security"
|
||||||
|
"github.com/goharbor/harbor/src/lib/log"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/user"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FromContext return the event operator from context
|
||||||
|
func FromContext(ctx context.Context) string {
|
||||||
|
sc, ok := security.FromContext(ctx)
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if sc.IsSolutionUser() {
|
||||||
|
user, err := user.Mgr.Get(ctx, 1)
|
||||||
|
if err == nil {
|
||||||
|
return user.Username
|
||||||
|
}
|
||||||
|
log.G(ctx).Errorf("failed to get operator for security %s, error: %v", sc.Name(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sc.GetUsername()
|
||||||
|
}
|
@ -539,7 +539,7 @@ func (de *defaultEnforcer) toCandidates(ctx context.Context, p *models.Project,
|
|||||||
// getProject gets the full metadata of the specified project
|
// getProject gets the full metadata of the specified project
|
||||||
func (de *defaultEnforcer) getProject(ctx context.Context, id int64) (*models.Project, error) {
|
func (de *defaultEnforcer) getProject(ctx context.Context, id int64) (*models.Project, error) {
|
||||||
// Get project info with CVE allow list and metadata
|
// Get project info with CVE allow list and metadata
|
||||||
return de.proCtl.Get(ctx, id, project.CVEAllowlist(true), project.Metadata(true))
|
return de.proCtl.Get(ctx, id, project.WithEffectCVEAllowlist())
|
||||||
}
|
}
|
||||||
|
|
||||||
// enforceError is a wrap error
|
// enforceError is a wrap error
|
||||||
|
@ -17,10 +17,13 @@ package project
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
event "github.com/goharbor/harbor/src/controller/event/metadata"
|
||||||
|
"github.com/goharbor/harbor/src/controller/event/operator"
|
||||||
"github.com/goharbor/harbor/src/lib/errors"
|
"github.com/goharbor/harbor/src/lib/errors"
|
||||||
"github.com/goharbor/harbor/src/lib/log"
|
"github.com/goharbor/harbor/src/lib/log"
|
||||||
"github.com/goharbor/harbor/src/lib/orm"
|
"github.com/goharbor/harbor/src/lib/orm"
|
||||||
"github.com/goharbor/harbor/src/lib/q"
|
"github.com/goharbor/harbor/src/lib/q"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/notification"
|
||||||
"github.com/goharbor/harbor/src/pkg/project"
|
"github.com/goharbor/harbor/src/pkg/project"
|
||||||
"github.com/goharbor/harbor/src/pkg/project/metadata"
|
"github.com/goharbor/harbor/src/pkg/project/metadata"
|
||||||
"github.com/goharbor/harbor/src/pkg/project/models"
|
"github.com/goharbor/harbor/src/pkg/project/models"
|
||||||
@ -33,6 +36,12 @@ var (
|
|||||||
Ctl = NewController()
|
Ctl = NewController()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Project alias to models.Project
|
||||||
|
type Project = models.Project
|
||||||
|
|
||||||
|
// MemberQuery alias to models.MemberQuery
|
||||||
|
type MemberQuery = models.MemberQuery
|
||||||
|
|
||||||
// Controller defines the operations related with blobs
|
// Controller defines the operations related with blobs
|
||||||
type Controller interface {
|
type Controller interface {
|
||||||
// Create create project instance
|
// Create create project instance
|
||||||
@ -46,7 +55,9 @@ type Controller interface {
|
|||||||
// GetByName get the project by project name
|
// GetByName get the project by project name
|
||||||
GetByName(ctx context.Context, projectName string, options ...Option) (*models.Project, error)
|
GetByName(ctx context.Context, projectName string, options ...Option) (*models.Project, error)
|
||||||
// List list projects
|
// List list projects
|
||||||
List(ctx context.Context, query *models.ProjectQueryParam, options ...Option) ([]*models.Project, error)
|
List(ctx context.Context, query *q.Query, options ...Option) ([]*models.Project, error)
|
||||||
|
// Update update the project
|
||||||
|
Update(ctx context.Context, project *models.Project) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewController creates an instance of the default project controller
|
// NewController creates an instance of the default project controller
|
||||||
@ -92,6 +103,14 @@ func (c *controller) Create(ctx context.Context, project *models.Project) (int64
|
|||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fire event
|
||||||
|
e := &event.CreateProjectEventMetadata{
|
||||||
|
ProjectID: projectID,
|
||||||
|
Project: project.Name,
|
||||||
|
Operator: operator.FromContext(ctx),
|
||||||
|
}
|
||||||
|
notification.AddEvent(ctx, e)
|
||||||
|
|
||||||
return projectID, nil
|
return projectID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,7 +119,23 @@ func (c *controller) Count(ctx context.Context, query *q.Query) (int64, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *controller) Delete(ctx context.Context, id int64) error {
|
func (c *controller) Delete(ctx context.Context, id int64) error {
|
||||||
return c.projectMgr.Delete(ctx, id)
|
proj, err := c.Get(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.projectMgr.Delete(ctx, id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
e := &event.DeleteProjectEventMetadata{
|
||||||
|
ProjectID: proj.ProjectID,
|
||||||
|
Project: proj.Name,
|
||||||
|
Operator: operator.FromContext(ctx),
|
||||||
|
}
|
||||||
|
notification.AddEvent(ctx, e)
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *controller) Get(ctx context.Context, projectID int64, options ...Option) (*models.Project, error) {
|
func (c *controller) Get(ctx context.Context, projectID int64, options ...Option) (*models.Project, error) {
|
||||||
@ -109,13 +144,11 @@ func (c *controller) Get(ctx context.Context, projectID int64, options ...Option
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := newOptions(options...)
|
if err := c.assembleProjects(ctx, models.Projects{p}, options...); err != nil {
|
||||||
if opts.WithOwner {
|
return nil, err
|
||||||
if err := c.loadOwners(ctx, models.Projects{p}); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return c.assembleProject(ctx, p, opts)
|
|
||||||
|
return p, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *controller) GetByName(ctx context.Context, projectName string, options ...Option) (*models.Project, error) {
|
func (c *controller) GetByName(ctx context.Context, projectName string, options ...Option) (*models.Project, error) {
|
||||||
@ -128,38 +161,164 @@ func (c *controller) GetByName(ctx context.Context, projectName string, options
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := newOptions(options...)
|
if err := c.assembleProjects(ctx, models.Projects{p}, options...); err != nil {
|
||||||
if opts.WithOwner {
|
return nil, err
|
||||||
if err := c.loadOwners(ctx, models.Projects{p}); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return c.assembleProject(ctx, p, newOptions(options...))
|
|
||||||
|
return p, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *controller) List(ctx context.Context, query *models.ProjectQueryParam, options ...Option) ([]*models.Project, error) {
|
func (c *controller) List(ctx context.Context, query *q.Query, options ...Option) ([]*models.Project, error) {
|
||||||
projects, err := c.projectMgr.List(ctx, query)
|
projects, err := c.projectMgr.List(ctx, query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := newOptions(options...)
|
if len(projects) == 0 {
|
||||||
if opts.WithOwner {
|
return projects, nil
|
||||||
if err := c.loadOwners(ctx, projects); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, p := range projects {
|
if err := c.assembleProjects(ctx, projects, options...); err != nil {
|
||||||
if _, err := c.assembleProject(ctx, p, opts); err != nil {
|
return nil, err
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return projects, nil
|
return projects, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *controller) Update(ctx context.Context, p *models.Project) error {
|
||||||
|
// currently, allowlist manager not use the ormer from the context,
|
||||||
|
// the SQL executed in the allowlist manager will not be in the transaction with metadata manager,
|
||||||
|
// we will update the metadata of the project first so that we can be rollback the operations for the metadata
|
||||||
|
// when set allowlist for the project failed
|
||||||
|
if len(p.Metadata) > 0 {
|
||||||
|
meta, err := c.metaMgr.Get(ctx, p.ProjectID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if meta == nil {
|
||||||
|
meta = map[string]string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
metaNeedUpdated := map[string]string{}
|
||||||
|
metaNeedCreated := map[string]string{}
|
||||||
|
for key, value := range p.Metadata {
|
||||||
|
_, exist := meta[key]
|
||||||
|
if exist {
|
||||||
|
metaNeedUpdated[key] = value
|
||||||
|
} else {
|
||||||
|
metaNeedCreated[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err = c.metaMgr.Add(ctx, p.ProjectID, metaNeedCreated); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = c.metaMgr.Update(ctx, p.ProjectID, metaNeedUpdated); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.CVEAllowlist.ProjectID == p.ProjectID {
|
||||||
|
if err := c.allowlistMgr.Set(p.ProjectID, p.CVEAllowlist); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *controller) assembleProjects(ctx context.Context, projects models.Projects, options ...Option) error {
|
||||||
|
opts := newOptions(options...)
|
||||||
|
if opts.WithMetadata {
|
||||||
|
if err := c.loadMetadatas(ctx, projects); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.WithEffectCVEAllowlist {
|
||||||
|
if err := c.loadEffectCVEAllowlists(ctx, projects); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else if opts.WithCVEAllowlist {
|
||||||
|
if err := c.loadCVEAllowlists(ctx, projects); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.WithOwner {
|
||||||
|
if err := c.loadOwners(ctx, projects); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *controller) loadCVEAllowlists(ctx context.Context, projects models.Projects) error {
|
||||||
|
if len(projects) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range projects {
|
||||||
|
wl, err := c.allowlistMgr.Get(p.ProjectID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
p.CVEAllowlist = *wl
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *controller) loadEffectCVEAllowlists(ctx context.Context, projects models.Projects) error {
|
||||||
|
if len(projects) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range projects {
|
||||||
|
if p.ReuseSysCVEAllowlist() {
|
||||||
|
wl, err := c.allowlistMgr.GetSys()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("get system CVE allowlist failed, error: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
wl.ProjectID = p.ProjectID
|
||||||
|
p.CVEAllowlist = *wl
|
||||||
|
} else {
|
||||||
|
wl, err := c.allowlistMgr.Get(p.ProjectID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
p.CVEAllowlist = *wl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *controller) loadMetadatas(ctx context.Context, projects models.Projects) error {
|
||||||
|
if len(projects) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range projects {
|
||||||
|
meta, err := c.metaMgr.Get(ctx, p.ProjectID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p.Metadata = meta
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *controller) loadOwners(ctx context.Context, projects models.Projects) error {
|
func (c *controller) loadOwners(ctx context.Context, projects models.Projects) error {
|
||||||
|
if len(projects) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
owners, err := c.userMgr.List(ctx, q.New(q.KeyWords{"user_id__in": projects.OwnerIDs()}))
|
owners, err := c.userMgr.List(ctx, q.New(q.KeyWords{"user_id__in": projects.OwnerIDs()}))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -177,42 +336,3 @@ func (c *controller) loadOwners(ctx context.Context, projects models.Projects) e
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *controller) assembleProject(ctx context.Context, p *models.Project, opts *Options) (*models.Project, error) {
|
|
||||||
if opts.Metadata {
|
|
||||||
meta, err := c.metaMgr.Get(ctx, p.ProjectID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if len(p.Metadata) == 0 {
|
|
||||||
p.Metadata = make(map[string]string)
|
|
||||||
}
|
|
||||||
|
|
||||||
for k, v := range meta {
|
|
||||||
p.Metadata[k] = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if opts.CVEAllowlist {
|
|
||||||
if p.ReuseSysCVEAllowlist() {
|
|
||||||
wl, err := c.allowlistMgr.GetSys()
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("get system CVE allowlist failed, error: %v", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
wl.ProjectID = p.ProjectID
|
|
||||||
p.CVEAllowlist = *wl
|
|
||||||
} else {
|
|
||||||
wl, err := c.allowlistMgr.Get(p.ProjectID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
p.CVEAllowlist = *wl
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return p, nil
|
|
||||||
}
|
|
||||||
|
@ -22,6 +22,7 @@ import (
|
|||||||
commonmodels "github.com/goharbor/harbor/src/common/models"
|
commonmodels "github.com/goharbor/harbor/src/common/models"
|
||||||
"github.com/goharbor/harbor/src/lib/errors"
|
"github.com/goharbor/harbor/src/lib/errors"
|
||||||
"github.com/goharbor/harbor/src/lib/orm"
|
"github.com/goharbor/harbor/src/lib/orm"
|
||||||
|
"github.com/goharbor/harbor/src/lib/q"
|
||||||
"github.com/goharbor/harbor/src/pkg/project/models"
|
"github.com/goharbor/harbor/src/pkg/project/models"
|
||||||
usermodels "github.com/goharbor/harbor/src/pkg/user/models"
|
usermodels "github.com/goharbor/harbor/src/pkg/user/models"
|
||||||
ormtesting "github.com/goharbor/harbor/src/testing/lib/orm"
|
ormtesting "github.com/goharbor/harbor/src/testing/lib/orm"
|
||||||
@ -102,8 +103,8 @@ func (suite *ControllerTestSuite) TestGetByName() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
allowlistMgr.On("GetSys").Return(&commonmodels.CVEAllowlist{}, nil)
|
allowlistMgr.On("Get", mock.Anything).Return(&commonmodels.CVEAllowlist{ProjectID: 1}, nil)
|
||||||
p, err := c.GetByName(ctx, "library", CVEAllowlist(true))
|
p, err := c.GetByName(ctx, "library", WithCVEAllowlist())
|
||||||
suite.Nil(err)
|
suite.Nil(err)
|
||||||
suite.Equal("library", p.Name)
|
suite.Equal("library", p.Name)
|
||||||
suite.Equal(p.ProjectID, p.CVEAllowlist.ProjectID)
|
suite.Equal(p.ProjectID, p.CVEAllowlist.ProjectID)
|
||||||
@ -140,8 +141,7 @@ func (suite *ControllerTestSuite) TestWithOwner() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
param := &models.ProjectQueryParam{ProjectIDs: []int64{1}}
|
projects, err := c.List(ctx, q.New(q.KeyWords{"project_id__in": []int64{1}}), Metadata(false), WithOwner())
|
||||||
projects, err := c.List(ctx, param, Metadata(false), WithOwner())
|
|
||||||
suite.Nil(err)
|
suite.Nil(err)
|
||||||
suite.Len(projects, 1)
|
suite.Len(projects, 1)
|
||||||
suite.Equal("admin", projects[0].OwnerName)
|
suite.Equal("admin", projects[0].OwnerName)
|
||||||
|
@ -19,22 +19,31 @@ type Option func(*Options)
|
|||||||
|
|
||||||
// Options options used by `Get` method of `Controller`
|
// Options options used by `Get` method of `Controller`
|
||||||
type Options struct {
|
type Options struct {
|
||||||
CVEAllowlist bool // get project with cve allowlist
|
WithCVEAllowlist bool // get project with cve allowlist
|
||||||
Metadata bool // get project with metadata
|
WithEffectCVEAllowlist bool // get project with effect cve allowlist
|
||||||
WithOwner bool
|
WithMetadata bool // get project with metadata
|
||||||
|
WithOwner bool // get project with owner name
|
||||||
}
|
}
|
||||||
|
|
||||||
// CVEAllowlist set CVEAllowlist for the Options
|
// WithCVEAllowlist set WithCVEAllowlist for the Options
|
||||||
func CVEAllowlist(allowlist bool) Option {
|
func WithCVEAllowlist() Option {
|
||||||
return func(opts *Options) {
|
return func(opts *Options) {
|
||||||
opts.CVEAllowlist = allowlist
|
opts.WithCVEAllowlist = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Metadata set Metadata for the Options
|
// WithEffectCVEAllowlist set WithEffectCVEAllowlist for the Options
|
||||||
|
func WithEffectCVEAllowlist() Option {
|
||||||
|
return func(opts *Options) {
|
||||||
|
opts.WithMetadata = true // we need `reuse_sys_cve_allowlist` value in the metadata of project
|
||||||
|
opts.WithEffectCVEAllowlist = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metadata set WithMetadata for the Options
|
||||||
func Metadata(metadata bool) Option {
|
func Metadata(metadata bool) Option {
|
||||||
return func(opts *Options) {
|
return func(opts *Options) {
|
||||||
opts.Metadata = metadata
|
opts.WithMetadata = metadata
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,7 +56,7 @@ func WithOwner() Option {
|
|||||||
|
|
||||||
func newOptions(options ...Option) *Options {
|
func newOptions(options ...Option) *Options {
|
||||||
opts := &Options{
|
opts := &Options{
|
||||||
Metadata: true, // default get project with metadata
|
WithMetadata: true, // default get project with metadata
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, f := range options {
|
for _, f := range options {
|
||||||
|
64
src/controller/project/util.go
Normal file
64
src/controller/project/util.go
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
// Copyright Project Harbor Authors
|
||||||
|
//
|
||||||
|
// 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 project
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/goharbor/harbor/src/lib/q"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/project/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Result the result for ListAll func
|
||||||
|
type Result struct {
|
||||||
|
Data *models.Project
|
||||||
|
Error error
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListAll returns all projects with chunk support
|
||||||
|
func ListAll(ctx context.Context, chunkSize int, query *q.Query, options ...Option) <-chan Result {
|
||||||
|
ch := make(chan Result, chunkSize)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer close(ch)
|
||||||
|
|
||||||
|
query = q.MustClone(query)
|
||||||
|
query.PageNumber = 1
|
||||||
|
query.PageSize = int64(chunkSize)
|
||||||
|
|
||||||
|
for {
|
||||||
|
projects, err := Ctl.List(ctx, query, options...)
|
||||||
|
if err != nil {
|
||||||
|
format := "failed to list projects at page %d with page size %d, error :%v"
|
||||||
|
ch <- Result{Error: fmt.Errorf(format, query.PageNumber, query.PageSize, err)}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range projects {
|
||||||
|
ch <- Result{Data: p}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(projects) < chunkSize {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
query.PageNumber++
|
||||||
|
}
|
||||||
|
|
||||||
|
}()
|
||||||
|
|
||||||
|
return ch
|
||||||
|
}
|
@ -21,6 +21,7 @@ import (
|
|||||||
"github.com/goharbor/harbor/src/common/dao"
|
"github.com/goharbor/harbor/src/common/dao"
|
||||||
"github.com/goharbor/harbor/src/common/models"
|
"github.com/goharbor/harbor/src/common/models"
|
||||||
"github.com/goharbor/harbor/src/lib/errors"
|
"github.com/goharbor/harbor/src/lib/errors"
|
||||||
|
"github.com/goharbor/harbor/src/lib/q"
|
||||||
"github.com/goharbor/harbor/src/pkg/project"
|
"github.com/goharbor/harbor/src/pkg/project"
|
||||||
"github.com/graph-gophers/dataloader"
|
"github.com/graph-gophers/dataloader"
|
||||||
)
|
)
|
||||||
@ -43,7 +44,7 @@ func getProjectsBatchFn(ctx context.Context, keys dataloader.Keys) []*dataloader
|
|||||||
projectIDs = append(projectIDs, id)
|
projectIDs = append(projectIDs, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
projects, err := project.Mgr.List(ctx, &models.ProjectQueryParam{ProjectIDs: projectIDs})
|
projects, err := project.Mgr.List(ctx, q.New(q.KeyWords{"project_id__in": projectIDs}))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return handleError(err)
|
return handleError(err)
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/goharbor/harbor/src/common/models"
|
|
||||||
"github.com/goharbor/harbor/src/controller/project"
|
"github.com/goharbor/harbor/src/controller/project"
|
||||||
"github.com/goharbor/harbor/src/lib/errors"
|
"github.com/goharbor/harbor/src/lib/errors"
|
||||||
"github.com/goharbor/harbor/src/lib/log"
|
"github.com/goharbor/harbor/src/lib/log"
|
||||||
@ -55,40 +54,14 @@ func RefreshForProjects(ctx context.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
projects := func(chunkSize int) <-chan *models.Project {
|
chunkSize := 50 // default chunk size is 50
|
||||||
ch := make(chan *models.Project, chunkSize)
|
for result := range project.ListAll(ctx, chunkSize, nil, project.Metadata(false)) {
|
||||||
|
if result.Error != nil {
|
||||||
|
log.Errorf("refresh quota for all projects got error: %v", result.Error)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
go func() {
|
p := result.Data
|
||||||
defer close(ch)
|
|
||||||
|
|
||||||
params := &models.ProjectQueryParam{
|
|
||||||
Pagination: &models.Pagination{Page: 1, Size: int64(chunkSize)},
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
|
||||||
results, err := project.Ctl.List(ctx, params, project.Metadata(false))
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("list projects failed, error: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, p := range results {
|
|
||||||
ch <- p
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(results) < chunkSize {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
params.Pagination.Page++
|
|
||||||
}
|
|
||||||
|
|
||||||
}()
|
|
||||||
|
|
||||||
return ch
|
|
||||||
}(50) // default chunk size is 50
|
|
||||||
|
|
||||||
for p := range projects {
|
|
||||||
referenceID := ReferenceID(p.ProjectID)
|
referenceID := ReferenceID(p.ProjectID)
|
||||||
|
|
||||||
_, err := Ctl.GetByRef(ctx, ProjectReference, referenceID)
|
_, err := Ctl.GetByRef(ctx, ProjectReference, referenceID)
|
||||||
|
@ -23,6 +23,7 @@ import (
|
|||||||
"github.com/goharbor/harbor/src/common/models"
|
"github.com/goharbor/harbor/src/common/models"
|
||||||
"github.com/goharbor/harbor/src/controller/project"
|
"github.com/goharbor/harbor/src/controller/project"
|
||||||
"github.com/goharbor/harbor/src/lib/orm"
|
"github.com/goharbor/harbor/src/lib/orm"
|
||||||
|
"github.com/goharbor/harbor/src/lib/q"
|
||||||
"github.com/goharbor/harbor/src/pkg/quota"
|
"github.com/goharbor/harbor/src/pkg/quota"
|
||||||
"github.com/goharbor/harbor/src/pkg/quota/driver"
|
"github.com/goharbor/harbor/src/pkg/quota/driver"
|
||||||
"github.com/goharbor/harbor/src/pkg/quota/types"
|
"github.com/goharbor/harbor/src/pkg/quota/types"
|
||||||
@ -89,7 +90,7 @@ func (suite *RefreshForProjectsTestSuite) TestRefreshForProjects() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
page := 1
|
page := 1
|
||||||
mock.OnAnything(suite.projectCtl, "List").Return(func(context.Context, *models.ProjectQueryParam, ...project.Option) []*models.Project {
|
mock.OnAnything(suite.projectCtl, "List").Return(func(context.Context, *q.Query, ...project.Option) []*models.Project {
|
||||||
defer func() {
|
defer func() {
|
||||||
page++
|
page++
|
||||||
}()
|
}()
|
||||||
|
@ -51,6 +51,11 @@ var (
|
|||||||
retentionController retention.APIController
|
retentionController retention.APIController
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// GetRetentionController returns the retention API controller
|
||||||
|
func GetRetentionController() retention.APIController {
|
||||||
|
return retentionController
|
||||||
|
}
|
||||||
|
|
||||||
// BaseController ...
|
// BaseController ...
|
||||||
type BaseController struct {
|
type BaseController struct {
|
||||||
api.BaseAPI
|
api.BaseAPI
|
||||||
|
@ -97,16 +97,12 @@ func init() {
|
|||||||
|
|
||||||
beego.Router("/api/health", &HealthAPI{}, "get:CheckHealth")
|
beego.Router("/api/health", &HealthAPI{}, "get:CheckHealth")
|
||||||
beego.Router("/api/search/", &SearchAPI{})
|
beego.Router("/api/search/", &SearchAPI{})
|
||||||
beego.Router("/api/projects/", &ProjectAPI{}, "get:List;post:Post;head:Head")
|
|
||||||
beego.Router("/api/projects/:id", &ProjectAPI{}, "delete:Delete;get:Get;put:Put")
|
|
||||||
beego.Router("/api/users/:id", &UserAPI{}, "get:Get")
|
beego.Router("/api/users/:id", &UserAPI{}, "get:Get")
|
||||||
beego.Router("/api/users", &UserAPI{}, "get:List;post:Post;delete:Delete;put:Put")
|
beego.Router("/api/users", &UserAPI{}, "get:List;post:Post;delete:Delete;put:Put")
|
||||||
beego.Router("/api/users/search", &UserAPI{}, "get:Search")
|
beego.Router("/api/users/search", &UserAPI{}, "get:Search")
|
||||||
beego.Router("/api/users/:id([0-9]+)/password", &UserAPI{}, "put:ChangePassword")
|
beego.Router("/api/users/:id([0-9]+)/password", &UserAPI{}, "put:ChangePassword")
|
||||||
beego.Router("/api/users/:id/permissions", &UserAPI{}, "get:ListUserPermissions")
|
beego.Router("/api/users/:id/permissions", &UserAPI{}, "get:ListUserPermissions")
|
||||||
beego.Router("/api/users/:id/sysadmin", &UserAPI{}, "put:ToggleUserAdminRole")
|
beego.Router("/api/users/:id/sysadmin", &UserAPI{}, "put:ToggleUserAdminRole")
|
||||||
beego.Router("/api/projects/:id([0-9]+)/summary", &ProjectAPI{}, "get:Summary")
|
|
||||||
beego.Router("/api/projects/:id([0-9]+)/_deletable", &ProjectAPI{}, "get:Deletable")
|
|
||||||
beego.Router("/api/projects/:id([0-9]+)/metadatas/?:name", &MetadataAPI{}, "get:Get")
|
beego.Router("/api/projects/:id([0-9]+)/metadatas/?:name", &MetadataAPI{}, "get:Get")
|
||||||
beego.Router("/api/projects/:id([0-9]+)/metadatas/", &MetadataAPI{}, "post:Post")
|
beego.Router("/api/projects/:id([0-9]+)/metadatas/", &MetadataAPI{}, "post:Post")
|
||||||
beego.Router("/api/projects/:id([0-9]+)/metadatas/:name", &MetadataAPI{}, "put:Put;delete:Delete")
|
beego.Router("/api/projects/:id([0-9]+)/metadatas/:name", &MetadataAPI{}, "put:Put;delete:Delete")
|
||||||
|
@ -1,720 +0,0 @@
|
|||||||
// Copyright 2018 Project Harbor Authors
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"regexp"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/goharbor/harbor/src/common"
|
|
||||||
"github.com/goharbor/harbor/src/common/dao"
|
|
||||||
pro "github.com/goharbor/harbor/src/common/dao/project"
|
|
||||||
"github.com/goharbor/harbor/src/common/models"
|
|
||||||
"github.com/goharbor/harbor/src/common/rbac"
|
|
||||||
"github.com/goharbor/harbor/src/common/security/local"
|
|
||||||
"github.com/goharbor/harbor/src/common/utils"
|
|
||||||
errutil "github.com/goharbor/harbor/src/common/utils/error"
|
|
||||||
"github.com/goharbor/harbor/src/controller/event/metadata"
|
|
||||||
"github.com/goharbor/harbor/src/controller/project"
|
|
||||||
"github.com/goharbor/harbor/src/controller/quota"
|
|
||||||
"github.com/goharbor/harbor/src/core/config"
|
|
||||||
"github.com/goharbor/harbor/src/lib/errors"
|
|
||||||
"github.com/goharbor/harbor/src/lib/log"
|
|
||||||
evt "github.com/goharbor/harbor/src/pkg/notifier/event"
|
|
||||||
"github.com/goharbor/harbor/src/pkg/quota/types"
|
|
||||||
"github.com/goharbor/harbor/src/pkg/retention/policy"
|
|
||||||
"github.com/goharbor/harbor/src/pkg/scan/vuln"
|
|
||||||
"github.com/goharbor/harbor/src/replication"
|
|
||||||
)
|
|
||||||
|
|
||||||
type deletableResp struct {
|
|
||||||
Deletable bool `json:"deletable"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ProjectAPI handles request to /api/projects/{} /api/projects/{}/logs
|
|
||||||
type ProjectAPI struct {
|
|
||||||
BaseController
|
|
||||||
project *models.Project
|
|
||||||
}
|
|
||||||
|
|
||||||
const projectNameMaxLen int = 255
|
|
||||||
const projectNameMinLen int = 1
|
|
||||||
const restrictedNameChars = `[a-z0-9]+(?:[._-][a-z0-9]+)*`
|
|
||||||
const defaultDaysToRetention = 7
|
|
||||||
|
|
||||||
// Prepare validates the URL and the user
|
|
||||||
func (p *ProjectAPI) Prepare() {
|
|
||||||
p.BaseController.Prepare()
|
|
||||||
if len(p.GetStringFromPath(":id")) != 0 {
|
|
||||||
id, err := p.GetInt64FromPath(":id")
|
|
||||||
if err != nil || id <= 0 {
|
|
||||||
text := "invalid project ID: "
|
|
||||||
if err != nil {
|
|
||||||
text += err.Error()
|
|
||||||
} else {
|
|
||||||
text += fmt.Sprintf("%d", id)
|
|
||||||
}
|
|
||||||
p.SendBadRequestError(errors.New(text))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
project, err := p.ProjectMgr.Get(id)
|
|
||||||
if err != nil {
|
|
||||||
p.ParseAndHandleError(fmt.Sprintf("failed to get project %d", id), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if project == nil {
|
|
||||||
p.handleProjectNotFound(id)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
p.project = project
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *ProjectAPI) requireAccess(action rbac.Action, subresource ...rbac.Resource) bool {
|
|
||||||
if len(subresource) == 0 {
|
|
||||||
subresource = append(subresource, rbac.ResourceSelf)
|
|
||||||
}
|
|
||||||
|
|
||||||
return p.RequireProjectAccess(p.project.ProjectID, action, subresource...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Post ...
|
|
||||||
func (p *ProjectAPI) Post() {
|
|
||||||
if !p.SecurityCtx.IsAuthenticated() {
|
|
||||||
p.SendUnAuthorizedError(errors.New("Unauthorized"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
onlyAdmin, err := config.OnlyAdminCreateProject()
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("failed to determine whether only admin can create projects: %v", err)
|
|
||||||
p.SendInternalServerError(fmt.Errorf("failed to determine whether only admin can create projects: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if onlyAdmin && !(p.SecurityCtx.IsSysAdmin() || p.SecurityCtx.IsSolutionUser()) {
|
|
||||||
log.Errorf("Only sys admin can create project")
|
|
||||||
p.SendForbiddenError(errors.New("Only system admin can create project"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var pro *models.ProjectRequest
|
|
||||||
if err := p.DecodeJSONReq(&pro); err != nil {
|
|
||||||
p.SendBadRequestError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = validateProjectReq(pro)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Invalid project request, error: %v", err)
|
|
||||||
p.SendBadRequestError(fmt.Errorf("invalid request: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// trying to create a proxy cache project
|
|
||||||
if pro.RegistryID > 0 {
|
|
||||||
// only system admin can create the proxy cache project
|
|
||||||
if !p.SecurityCtx.IsSysAdmin() {
|
|
||||||
p.SendForbiddenError(errors.New("Only system admin can create proxy cache project"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
registry, err := replication.RegistryMgr.Get(pro.RegistryID)
|
|
||||||
if err != nil {
|
|
||||||
p.SendInternalServerError(fmt.Errorf("failed to get the registry %d: %v", pro.RegistryID, err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if registry == nil {
|
|
||||||
p.SendNotFoundError(fmt.Errorf("registry %d not found", pro.RegistryID))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
permitted := false
|
|
||||||
for _, t := range config.GetPermittedRegistryTypesForProxyCache() {
|
|
||||||
if string(registry.Type) == t {
|
|
||||||
permitted = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !permitted {
|
|
||||||
p.SendBadRequestError(fmt.Errorf("unsupported registry type %s", string(registry.Type)))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var hardLimits types.ResourceList
|
|
||||||
if config.QuotaPerProjectEnable() {
|
|
||||||
setting, err := config.QuotaSetting()
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("failed to get quota setting: %v", err)
|
|
||||||
p.SendInternalServerError(fmt.Errorf("failed to get quota setting: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !p.SecurityCtx.IsSysAdmin() {
|
|
||||||
pro.StorageLimit = &setting.StoragePerProject
|
|
||||||
}
|
|
||||||
|
|
||||||
hardLimits, err = projectQuotaHardLimits(p.Ctx.Request.Context(), pro, setting)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Invalid project request, error: %v", err)
|
|
||||||
p.SendBadRequestError(fmt.Errorf("invalid request: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exist, err := p.ProjectMgr.Exists(pro.Name)
|
|
||||||
if err != nil {
|
|
||||||
p.ParseAndHandleError(fmt.Sprintf("failed to check the existence of project %s",
|
|
||||||
pro.Name), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if exist {
|
|
||||||
p.SendConflictError(errors.New("conflict project"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if pro.Metadata == nil {
|
|
||||||
pro.Metadata = map[string]string{}
|
|
||||||
}
|
|
||||||
// accept the "public" property to make replication work well with old versions(<=1.2.0)
|
|
||||||
if pro.Public != nil && len(pro.Metadata[models.ProMetaPublic]) == 0 {
|
|
||||||
pro.Metadata[models.ProMetaPublic] = strconv.FormatBool(*pro.Public == 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// populate public metadata as false if it isn't set
|
|
||||||
if _, ok := pro.Metadata[models.ProMetaPublic]; !ok {
|
|
||||||
pro.Metadata[models.ProMetaPublic] = strconv.FormatBool(false)
|
|
||||||
}
|
|
||||||
// populate
|
|
||||||
|
|
||||||
owner := p.SecurityCtx.GetUsername()
|
|
||||||
// set the owner as the system admin when the API being called by replication
|
|
||||||
// it's a solution to workaround the restriction of project creation API:
|
|
||||||
// only normal users can create projects
|
|
||||||
if p.SecurityCtx.IsSolutionUser() {
|
|
||||||
user, err := dao.GetUser(models.User{
|
|
||||||
UserID: 1,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
p.SendInternalServerError(fmt.Errorf("failed to get the user 1: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
owner = user.Username
|
|
||||||
}
|
|
||||||
projectID, err := p.ProjectMgr.Create(&models.Project{
|
|
||||||
Name: pro.Name,
|
|
||||||
OwnerName: owner,
|
|
||||||
Metadata: pro.Metadata,
|
|
||||||
RegistryID: pro.RegistryID,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
if err == errutil.ErrDupProject {
|
|
||||||
log.Debugf("conflict %s", pro.Name)
|
|
||||||
p.SendConflictError(fmt.Errorf("conflict %s", pro.Name))
|
|
||||||
} else {
|
|
||||||
p.ParseAndHandleError("failed to add project", err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.QuotaPerProjectEnable() {
|
|
||||||
ctx := p.Ctx.Request.Context()
|
|
||||||
referenceID := quota.ReferenceID(projectID)
|
|
||||||
if _, err := quota.Ctl.Create(ctx, quota.ProjectReference, referenceID, hardLimits); err != nil {
|
|
||||||
p.SendInternalServerError(fmt.Errorf("failed to create quota for project: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// create a default retention policy for proxy project
|
|
||||||
if pro.RegistryID > 0 {
|
|
||||||
if err := p.addRetentionPolicyForProxy(projectID); err != nil {
|
|
||||||
p.SendInternalServerError(fmt.Errorf("failed to add tag retention policy for project: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// fire event
|
|
||||||
evt.BuildAndPublish(&metadata.CreateProjectEventMetadata{
|
|
||||||
ProjectID: projectID,
|
|
||||||
Project: pro.Name,
|
|
||||||
Operator: owner,
|
|
||||||
})
|
|
||||||
|
|
||||||
p.Redirect(http.StatusCreated, strconv.FormatInt(projectID, 10))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *ProjectAPI) addRetentionPolicyForProxy(projID int64) error {
|
|
||||||
plc := policy.WithNDaysSinceLastPull(projID, defaultDaysToRetention)
|
|
||||||
retID, err := retentionController.CreateRetention(plc)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := p.ProjectMgr.GetMetadataManager().Add(projID, map[string]string{"retention_id": strconv.FormatInt(retID, 10)}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Head ...
|
|
||||||
func (p *ProjectAPI) Head() {
|
|
||||||
|
|
||||||
if !p.SecurityCtx.IsAuthenticated() {
|
|
||||||
p.SendUnAuthorizedError(errors.New("Unauthorized"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
name := p.GetString("project_name")
|
|
||||||
if len(name) == 0 {
|
|
||||||
p.SendBadRequestError(errors.New("project_name is needed"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
project, err := p.ProjectMgr.Get(name)
|
|
||||||
if err != nil {
|
|
||||||
p.ParseAndHandleError(fmt.Sprintf("failed to get project %s", name), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if project == nil {
|
|
||||||
p.SendNotFoundError(fmt.Errorf("project %s not found", name))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get ...
|
|
||||||
func (p *ProjectAPI) Get() {
|
|
||||||
if !p.requireAccess(rbac.ActionRead) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err := p.populateProperties(p.project)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("populate project properties failed with : %+v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
p.Data["json"] = p.project
|
|
||||||
p.ServeJSON()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete ...
|
|
||||||
func (p *ProjectAPI) Delete() {
|
|
||||||
if !p.requireAccess(rbac.ActionDelete) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := p.deletable(p.project.ProjectID)
|
|
||||||
if err != nil {
|
|
||||||
p.SendInternalServerError(fmt.Errorf(
|
|
||||||
"failed to check the deletable of project %d: %v", p.project.ProjectID, err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !result.Deletable {
|
|
||||||
p.SendPreconditionFailedError(errors.New(result.Message))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := p.Ctx.Request.Context()
|
|
||||||
|
|
||||||
if err := project.Ctl.Delete(ctx, p.project.ProjectID); err != nil {
|
|
||||||
p.ParseAndHandleError(fmt.Sprintf("failed to delete project %d", p.project.ProjectID), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
referenceID := quota.ReferenceID(p.project.ProjectID)
|
|
||||||
q, err := quota.Ctl.GetByRef(ctx, quota.ProjectReference, referenceID)
|
|
||||||
if err != nil {
|
|
||||||
log.Warningf("failed to get quota for project %s, error: %v", p.project.Name, err)
|
|
||||||
} else {
|
|
||||||
if err := quota.Ctl.Delete(ctx, q.ID); err != nil {
|
|
||||||
p.SendInternalServerError(fmt.Errorf("failed to delete quota for project: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// fire event
|
|
||||||
evt.BuildAndPublish(&metadata.DeleteProjectEventMetadata{
|
|
||||||
ProjectID: p.project.ProjectID,
|
|
||||||
Project: p.project.Name,
|
|
||||||
Operator: p.SecurityCtx.GetUsername(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deletable ...
|
|
||||||
func (p *ProjectAPI) Deletable() {
|
|
||||||
if !p.requireAccess(rbac.ActionDelete) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := p.deletable(p.project.ProjectID)
|
|
||||||
if err != nil {
|
|
||||||
p.SendInternalServerError(fmt.Errorf(
|
|
||||||
"failed to check the deletable of project %d: %v", p.project.ProjectID, err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
p.Data["json"] = result
|
|
||||||
p.ServeJSON()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *ProjectAPI) deletable(projectID int64) (*deletableResp, error) {
|
|
||||||
count, err := dao.GetTotalOfRepositories(&models.RepositoryQuery{
|
|
||||||
ProjectIDs: []int64{projectID},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if count > 0 {
|
|
||||||
return &deletableResp{
|
|
||||||
Deletable: false,
|
|
||||||
Message: "the project contains repositories, can not be deleted",
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check helm charts number
|
|
||||||
if config.WithChartMuseum() {
|
|
||||||
charts, err := chartController.ListCharts(p.project.Name)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(charts) > 0 {
|
|
||||||
return &deletableResp{
|
|
||||||
Deletable: false,
|
|
||||||
Message: "the project contains helm charts, can not be deleted",
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &deletableResp{
|
|
||||||
Deletable: true,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// List ...
|
|
||||||
func (p *ProjectAPI) List() {
|
|
||||||
// query strings
|
|
||||||
page, size, err := p.GetPaginationParams()
|
|
||||||
if err != nil {
|
|
||||||
p.SendBadRequestError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
query := &models.ProjectQueryParam{
|
|
||||||
Name: p.GetString("name"),
|
|
||||||
Owner: p.GetString("owner"),
|
|
||||||
Pagination: &models.Pagination{
|
|
||||||
Page: page,
|
|
||||||
Size: size,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
public := p.GetString("public")
|
|
||||||
if len(public) > 0 {
|
|
||||||
pub, err := strconv.ParseBool(public)
|
|
||||||
if err != nil {
|
|
||||||
p.SendBadRequestError(fmt.Errorf("invalid public: %s", public))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
query.Public = &pub
|
|
||||||
}
|
|
||||||
|
|
||||||
var projects []*models.Project
|
|
||||||
if !p.SecurityCtx.IsAuthenticated() {
|
|
||||||
// not login, only get public projects
|
|
||||||
pros, err := p.ProjectMgr.GetPublic()
|
|
||||||
if err != nil {
|
|
||||||
p.SendInternalServerError(fmt.Errorf("failed to get public projects: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
projects = []*models.Project{}
|
|
||||||
projects = append(projects, pros...)
|
|
||||||
} else {
|
|
||||||
if !(p.SecurityCtx.IsSysAdmin() || p.SecurityCtx.IsSolutionUser()) {
|
|
||||||
projects = []*models.Project{}
|
|
||||||
// login, but not system admin or solution user, get public projects and
|
|
||||||
// projects that the user is member of
|
|
||||||
pros, err := p.ProjectMgr.GetPublic()
|
|
||||||
if err != nil {
|
|
||||||
p.SendInternalServerError(fmt.Errorf("failed to get public projects: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
projects = append(projects, pros...)
|
|
||||||
if sc, ok := p.SecurityCtx.(*local.SecurityContext); ok {
|
|
||||||
mps, err := p.ProjectMgr.GetAuthorized(sc.User())
|
|
||||||
if err != nil {
|
|
||||||
p.SendInternalServerError(fmt.Errorf("failed to list authorized projects: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
projects = append(projects, mps...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Query projects by user group
|
|
||||||
|
|
||||||
if projects != nil {
|
|
||||||
projectIDs := []int64{}
|
|
||||||
for _, project := range projects {
|
|
||||||
projectIDs = append(projectIDs, project.ProjectID)
|
|
||||||
}
|
|
||||||
query.ProjectIDs = projectIDs
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := p.ProjectMgr.List(query)
|
|
||||||
if err != nil {
|
|
||||||
p.ParseAndHandleError("failed to list projects", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, project := range result.Projects {
|
|
||||||
err = p.populateProperties(project)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("populate project properties failed %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
p.SetPaginationHeader(result.Total, page, size)
|
|
||||||
p.Data["json"] = result.Projects
|
|
||||||
p.ServeJSON()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *ProjectAPI) populateProperties(project *models.Project) error {
|
|
||||||
// Transform the severity to severity of CVSS v3.0 Ratings
|
|
||||||
if severity, ok := project.GetMetadata(models.ProMetaSeverity); ok {
|
|
||||||
project.SetMetadata(models.ProMetaSeverity, strings.ToLower(vuln.ParseSeverityVersion3(severity).String()))
|
|
||||||
}
|
|
||||||
|
|
||||||
if sc, ok := p.SecurityCtx.(*local.SecurityContext); ok {
|
|
||||||
roles, err := pro.ListRoles(sc.User(), project.ProjectID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
project.RoleList = roles
|
|
||||||
project.Role = highestRole(roles)
|
|
||||||
}
|
|
||||||
|
|
||||||
total, err := dao.GetTotalOfRepositories(&models.RepositoryQuery{
|
|
||||||
ProjectIDs: []int64{project.ProjectID},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
err = errors.Wrap(err, fmt.Sprintf("get repo count of project %d failed", project.ProjectID))
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
project.RepoCount = total
|
|
||||||
|
|
||||||
// Populate chart count property
|
|
||||||
if config.WithChartMuseum() {
|
|
||||||
count, err := chartController.GetCountOfCharts([]string{project.Name})
|
|
||||||
if err != nil {
|
|
||||||
err = errors.Wrap(err, fmt.Sprintf("get chart count of project %d failed", project.ProjectID))
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
project.ChartCount = count
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Put ...
|
|
||||||
func (p *ProjectAPI) Put() {
|
|
||||||
if !p.requireAccess(rbac.ActionUpdate) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var req *models.ProjectRequest
|
|
||||||
if err := p.DecodeJSONReq(&req); err != nil {
|
|
||||||
p.SendBadRequestError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := p.ProjectMgr.Update(p.project.ProjectID,
|
|
||||||
&models.Project{
|
|
||||||
Metadata: req.Metadata,
|
|
||||||
CVEAllowlist: req.CVEAllowlist,
|
|
||||||
}); err != nil {
|
|
||||||
p.ParseAndHandleError(fmt.Sprintf("failed to update project %d",
|
|
||||||
p.project.ProjectID), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Summary returns the summary of the project
|
|
||||||
func (p *ProjectAPI) Summary() {
|
|
||||||
if !p.requireAccess(rbac.ActionRead) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := p.populateProperties(p.project); err != nil {
|
|
||||||
log.Warningf("populate project properties failed with : %+v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
summary := &models.ProjectSummary{
|
|
||||||
RepoCount: p.project.RepoCount,
|
|
||||||
ChartCount: p.project.ChartCount,
|
|
||||||
}
|
|
||||||
|
|
||||||
var fetchSummaries []func(context.Context, int64, *models.ProjectSummary)
|
|
||||||
|
|
||||||
if hasPerm, _ := p.HasProjectPermission(p.project.ProjectID, rbac.ActionRead, rbac.ResourceQuota); hasPerm {
|
|
||||||
fetchSummaries = append(fetchSummaries, getProjectQuotaSummary)
|
|
||||||
}
|
|
||||||
|
|
||||||
if hasPerm, _ := p.HasProjectPermission(p.project.ProjectID, rbac.ActionList, rbac.ResourceMember); hasPerm {
|
|
||||||
fetchSummaries = append(fetchSummaries, getProjectMemberSummary)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := p.Ctx.Request.Context()
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
for _, fn := range fetchSummaries {
|
|
||||||
fn := fn
|
|
||||||
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
fn(ctx, p.project.ProjectID, summary)
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
if p.project.RegistryID > 0 {
|
|
||||||
registry, err := replication.RegistryMgr.Get(p.project.RegistryID)
|
|
||||||
if err != nil {
|
|
||||||
log.Warningf("failed to get registry %d: %v", p.project.RegistryID, err)
|
|
||||||
} else {
|
|
||||||
if registry != nil {
|
|
||||||
registry.Credential = nil
|
|
||||||
summary.Registry = registry
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
p.Data["json"] = summary
|
|
||||||
p.ServeJSON()
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO move this to pa ckage models
|
|
||||||
func validateProjectReq(req *models.ProjectRequest) error {
|
|
||||||
pn := req.Name
|
|
||||||
if utils.IsIllegalLength(pn, projectNameMinLen, projectNameMaxLen) {
|
|
||||||
return fmt.Errorf("Project name %s is illegal in length. (greater than %d or less than %d)", pn, projectNameMaxLen, projectNameMinLen)
|
|
||||||
}
|
|
||||||
validProjectName := regexp.MustCompile(`^` + restrictedNameChars + `$`)
|
|
||||||
legal := validProjectName.MatchString(pn)
|
|
||||||
if !legal {
|
|
||||||
return fmt.Errorf("project name is not in lower case or contains illegal characters")
|
|
||||||
}
|
|
||||||
|
|
||||||
metas, err := validateProjectMetadata(req.Metadata)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
req.Metadata = metas
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func projectQuotaHardLimits(ctx context.Context, req *models.ProjectRequest, setting *models.QuotaSetting) (types.ResourceList, error) {
|
|
||||||
hardLimits := types.ResourceList{}
|
|
||||||
|
|
||||||
if req.StorageLimit != nil {
|
|
||||||
hardLimits[types.ResourceStorage] = *req.StorageLimit
|
|
||||||
} else {
|
|
||||||
hardLimits[types.ResourceStorage] = setting.StoragePerProject
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := quota.Validate(ctx, quota.ProjectReference, hardLimits); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return hardLimits, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getProjectQuotaSummary(ctx context.Context, projectID int64, summary *models.ProjectSummary) {
|
|
||||||
if !config.QuotaPerProjectEnable() {
|
|
||||||
log.Debug("Quota per project disabled")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
q, err := quota.Ctl.GetByRef(ctx, quota.ProjectReference, quota.ReferenceID(projectID))
|
|
||||||
if err != nil {
|
|
||||||
log.Debugf("failed to get quota for project: %d", projectID)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
summary.Quota = &models.QuotaSummary{}
|
|
||||||
summary.Quota.Hard, _ = types.NewResourceList(q.Hard)
|
|
||||||
summary.Quota.Used, _ = types.NewResourceList(q.Used)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getProjectMemberSummary(ctx context.Context, projectID int64, summary *models.ProjectSummary) {
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
|
|
||||||
for _, e := range []struct {
|
|
||||||
role int
|
|
||||||
count *int64
|
|
||||||
}{
|
|
||||||
{common.RoleProjectAdmin, &summary.ProjectAdminCount},
|
|
||||||
{common.RoleMaintainer, &summary.MaintainerCount},
|
|
||||||
{common.RoleDeveloper, &summary.DeveloperCount},
|
|
||||||
{common.RoleGuest, &summary.GuestCount},
|
|
||||||
{common.RoleLimitedGuest, &summary.LimitedGuestCount},
|
|
||||||
} {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(role int, count *int64) {
|
|
||||||
defer wg.Done()
|
|
||||||
|
|
||||||
total, err := pro.GetTotalOfProjectMembers(projectID, role)
|
|
||||||
if err != nil {
|
|
||||||
log.Debugf("failed to get total of project members of role %d", role)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
*count = total
|
|
||||||
}(e.role, e.count)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns the highest role in the role list.
|
|
||||||
// This func should be removed once we deprecate the "current_user_role_id" in project API
|
|
||||||
// A user can have multiple roles and they may not have a strict ranking relationship
|
|
||||||
func highestRole(roles []int) int {
|
|
||||||
if roles == nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
rolePower := map[int]int{
|
|
||||||
common.RoleProjectAdmin: 50,
|
|
||||||
common.RoleMaintainer: 40,
|
|
||||||
common.RoleDeveloper: 30,
|
|
||||||
common.RoleGuest: 20,
|
|
||||||
common.RoleLimitedGuest: 10,
|
|
||||||
}
|
|
||||||
var highest, highestPower int
|
|
||||||
for _, role := range roles {
|
|
||||||
if p, ok := rolePower[role]; ok && p > highestPower {
|
|
||||||
highest = role
|
|
||||||
highestPower = p
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return highest
|
|
||||||
}
|
|
@ -1,535 +0,0 @@
|
|||||||
// Copyright 2018 Project Harbor Authors
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/goharbor/harbor/src/common"
|
|
||||||
"github.com/goharbor/harbor/src/common/dao"
|
|
||||||
"github.com/goharbor/harbor/src/common/models"
|
|
||||||
"github.com/goharbor/harbor/src/testing/apitests/apilib"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
var addProject *apilib.ProjectReq
|
|
||||||
var addPID int
|
|
||||||
|
|
||||||
func addProjectByName(apiTest *testapi, projectName string) (int32, error) {
|
|
||||||
req := apilib.ProjectReq{ProjectName: projectName}
|
|
||||||
code, err := apiTest.ProjectsPost(*admin, req)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
if code != http.StatusCreated {
|
|
||||||
return 0, fmt.Errorf("created failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
code, projects, err := apiTest.ProjectsGet(&apilib.ProjectQuery{Name: projectName}, *admin)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
if code != http.StatusOK {
|
|
||||||
return 0, fmt.Errorf("get failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(projects) == 0 {
|
|
||||||
return 0, fmt.Errorf("oops")
|
|
||||||
}
|
|
||||||
|
|
||||||
return projects[0].ProjectId, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func deleteProjectByIDs(apiTest *testapi, projectIDs ...int32) error {
|
|
||||||
for _, projectID := range projectIDs {
|
|
||||||
_, err := apiTest.ProjectsDelete(*admin, fmt.Sprintf("%d", projectID))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func InitAddPro() {
|
|
||||||
addProject = &apilib.ProjectReq{ProjectName: "add_project", Metadata: map[string]string{models.ProMetaPublic: "true"}}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAddProject(t *testing.T) {
|
|
||||||
|
|
||||||
fmt.Println("\nTesting Add Project(ProjectsPost) API")
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
apiTest := newHarborAPI()
|
|
||||||
|
|
||||||
// prepare for test
|
|
||||||
InitAddPro()
|
|
||||||
|
|
||||||
// case 1: admin not login, expect project creation fail.
|
|
||||||
|
|
||||||
result, err := apiTest.ProjectsPost(*unknownUsr, *addProject)
|
|
||||||
if err != nil {
|
|
||||||
t.Error("Error while creat project", err.Error())
|
|
||||||
t.Log(err)
|
|
||||||
} else {
|
|
||||||
assert.Equal(int(401), result, "Case 1: Project creation status should be 401")
|
|
||||||
// t.Log(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
// case 2: admin successful login, expect project creation success.
|
|
||||||
fmt.Println("case 2: admin successful login, expect project creation success.")
|
|
||||||
|
|
||||||
result, err = apiTest.ProjectsPost(*admin, *addProject)
|
|
||||||
if err != nil {
|
|
||||||
t.Error("Error while creat project", err.Error())
|
|
||||||
t.Log(err)
|
|
||||||
} else {
|
|
||||||
assert.Equal(int(201), result, "Case 2: Project creation status should be 201")
|
|
||||||
// t.Log(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
// case 3: duplicate project name, create project fail
|
|
||||||
fmt.Println("case 3: duplicate project name, create project fail")
|
|
||||||
|
|
||||||
result, err = apiTest.ProjectsPost(*admin, *addProject)
|
|
||||||
if err != nil {
|
|
||||||
t.Error("Error while creat project", err.Error())
|
|
||||||
t.Log(err)
|
|
||||||
} else {
|
|
||||||
assert.Equal(int(409), result, "Case 3: Project creation status should be 409")
|
|
||||||
// t.Log(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
// case 4: response code = 400 : Project name is illegal in length
|
|
||||||
fmt.Println("case 4 : response code = 400 : Project name is illegal in length ")
|
|
||||||
|
|
||||||
result, err = apiTest.ProjectsPost(*admin, apilib.ProjectReq{ProjectName: "", Metadata: map[string]string{models.ProMetaPublic: "true"}})
|
|
||||||
if err != nil {
|
|
||||||
t.Error("Error while creat project", err.Error())
|
|
||||||
t.Log(err)
|
|
||||||
} else {
|
|
||||||
assert.Equal(int(400), result, "case 4 : response code = 400 : Project name is illegal in length ")
|
|
||||||
}
|
|
||||||
|
|
||||||
// case 5: response code = 201 : expect project creation with quota success.
|
|
||||||
fmt.Println("case 5 : response code = 201 : expect project creation with quota success ")
|
|
||||||
|
|
||||||
var countLimit, storageLimit int64
|
|
||||||
countLimit, storageLimit = 100, 10
|
|
||||||
result, err = apiTest.ProjectsPost(*admin, apilib.ProjectReq{ProjectName: "with_quota", CountLimit: &countLimit, StorageLimit: &storageLimit})
|
|
||||||
if err != nil {
|
|
||||||
t.Error("Error while creat project", err.Error())
|
|
||||||
t.Log(err)
|
|
||||||
} else {
|
|
||||||
assert.Equal(int(201), result, "case 5 : response code = 201 : expect project creation with quota success ")
|
|
||||||
}
|
|
||||||
|
|
||||||
// case 6: response code = 400 : bad quota value, create project fail
|
|
||||||
fmt.Println("case 6: response code = 400 : bad quota value, create project fail")
|
|
||||||
|
|
||||||
countLimit, storageLimit = 100, -2
|
|
||||||
result, err = apiTest.ProjectsPost(*admin, apilib.ProjectReq{ProjectName: "with_quota", CountLimit: &countLimit, StorageLimit: &storageLimit})
|
|
||||||
if err != nil {
|
|
||||||
t.Error("Error while creat project", err.Error())
|
|
||||||
t.Log(err)
|
|
||||||
} else {
|
|
||||||
assert.Equal(int(400), result, "case 6: response code = 400 : bad quota value, create project fail")
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("\n")
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestListProjects(t *testing.T) {
|
|
||||||
fmt.Println("\nTest for Project GET API by project name")
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
apiTest := newHarborAPI()
|
|
||||||
var result []apilib.Project
|
|
||||||
|
|
||||||
cMockServer, oldCtrl, err := mockChartController()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
cMockServer.Close()
|
|
||||||
chartController = oldCtrl
|
|
||||||
}()
|
|
||||||
|
|
||||||
// ----------------------------case 1 : Response Code=200----------------------------//
|
|
||||||
fmt.Println("case 1: response code:200")
|
|
||||||
httpStatusCode, result, err := apiTest.ProjectsGet(
|
|
||||||
&apilib.ProjectQuery{
|
|
||||||
Name: addProject.ProjectName,
|
|
||||||
Owner: admin.Name,
|
|
||||||
Public: true,
|
|
||||||
})
|
|
||||||
assert.Nil(err)
|
|
||||||
assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200")
|
|
||||||
assert.Equal(addProject.ProjectName, result[0].ProjectName, "Project name is wrong")
|
|
||||||
assert.Equal("true", result[0].Metadata[models.ProMetaPublic], "Public is wrong")
|
|
||||||
|
|
||||||
// find add projectID
|
|
||||||
addPID = int(result[0].ProjectId)
|
|
||||||
|
|
||||||
// -------------------case 3 : check admin project role------------------------//
|
|
||||||
httpStatusCode, result, err = apiTest.ProjectsGet(
|
|
||||||
&apilib.ProjectQuery{
|
|
||||||
Name: addProject.ProjectName,
|
|
||||||
Owner: admin.Name,
|
|
||||||
Public: true,
|
|
||||||
}, *admin)
|
|
||||||
if err != nil {
|
|
||||||
t.Error("Error while search project by proName and isPublic", err.Error())
|
|
||||||
t.Log(err)
|
|
||||||
} else {
|
|
||||||
assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200")
|
|
||||||
assert.Equal(addProject.ProjectName, result[0].ProjectName, "Project name is wrong")
|
|
||||||
assert.Equal("true", result[0].Metadata[models.ProMetaPublic], "Public is wrong")
|
|
||||||
assert.Equal(int32(1), result[0].CurrentUserRoleId, "User project role is wrong")
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------case 4 : add project member and check his role ------------------------//
|
|
||||||
CommonAddUser()
|
|
||||||
member := &models.MemberReq{
|
|
||||||
Role: 2,
|
|
||||||
MemberUser: models.User{
|
|
||||||
Username: TestUserName,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
projectID := strconv.Itoa(addPID)
|
|
||||||
var memberID int
|
|
||||||
httpStatusCode, memberID, err = apiTest.AddProjectMember(*admin, projectID, member)
|
|
||||||
if err != nil {
|
|
||||||
t.Error("Error whihle add project role member", err.Error())
|
|
||||||
t.Log(err)
|
|
||||||
} else {
|
|
||||||
assert.Equal(int(201), httpStatusCode, "httpStatusCode should be 201")
|
|
||||||
}
|
|
||||||
httpStatusCode, result, err = apiTest.ProjectsGet(
|
|
||||||
&apilib.ProjectQuery{
|
|
||||||
Name: addProject.ProjectName,
|
|
||||||
}, *testUser)
|
|
||||||
if err != nil {
|
|
||||||
t.Error("Error while search project by proName and isPublic", err.Error())
|
|
||||||
t.Log(err)
|
|
||||||
} else {
|
|
||||||
assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200")
|
|
||||||
assert.Equal(addProject.ProjectName, result[0].ProjectName, "Project name is wrong")
|
|
||||||
assert.Equal("true", result[0].Metadata[models.ProMetaPublic], "Public is wrong")
|
|
||||||
assert.Equal(int32(2), result[0].CurrentUserRoleId, "User project role is wrong")
|
|
||||||
}
|
|
||||||
httpStatusCode, err = apiTest.DeleteProjectMember(*admin, projectID, strconv.Itoa(memberID))
|
|
||||||
if err != nil {
|
|
||||||
t.Error("Error whihle add project role member", err.Error())
|
|
||||||
t.Log(err)
|
|
||||||
} else {
|
|
||||||
assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200")
|
|
||||||
}
|
|
||||||
CommonDelUser()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get project by proID
|
|
||||||
func TestProGetByID(t *testing.T) {
|
|
||||||
fmt.Println("\nTest for Project GET API by project id")
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
apiTest := newHarborAPI()
|
|
||||||
var result apilib.Project
|
|
||||||
projectID := strconv.Itoa(addPID)
|
|
||||||
|
|
||||||
cMockServer, oldCtrl, err := mockChartController()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
cMockServer.Close()
|
|
||||||
chartController = oldCtrl
|
|
||||||
}()
|
|
||||||
|
|
||||||
// ----------------------------case 1 : Response Code=200----------------------------//
|
|
||||||
fmt.Println("case 1: response code:200")
|
|
||||||
httpStatusCode, result, err := apiTest.ProjectsGetByPID(projectID)
|
|
||||||
if err != nil {
|
|
||||||
t.Error("Error while search project by proID", err.Error())
|
|
||||||
t.Log(err)
|
|
||||||
} else {
|
|
||||||
assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200")
|
|
||||||
assert.Equal(addProject.ProjectName, result.ProjectName, "ProjectName is wrong")
|
|
||||||
assert.Equal("true", result.Metadata[models.ProMetaPublic], "Public is wrong")
|
|
||||||
}
|
|
||||||
fmt.Printf("\n")
|
|
||||||
}
|
|
||||||
func TestDeleteProject(t *testing.T) {
|
|
||||||
|
|
||||||
fmt.Println("\nTesting Delete Project(ProjectsPost) API")
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
apiTest := newHarborAPI()
|
|
||||||
|
|
||||||
projectID := strconv.Itoa(addPID)
|
|
||||||
|
|
||||||
// --------------------------case 1: Response Code=401,User need to log in first.-----------------------//
|
|
||||||
fmt.Println("case 1: Response Code=401,User need to log in first.")
|
|
||||||
httpStatusCode, err := apiTest.ProjectsDelete(*unknownUsr, projectID)
|
|
||||||
if err != nil {
|
|
||||||
t.Error("Error while delete project", err.Error())
|
|
||||||
t.Log(err)
|
|
||||||
} else {
|
|
||||||
assert.Equal(int(401), httpStatusCode, "Case 1: Project deletion status should be 401")
|
|
||||||
}
|
|
||||||
|
|
||||||
// --------------------------case 2: Response Code=200---------------------------------//
|
|
||||||
fmt.Println("case2: response code:200")
|
|
||||||
httpStatusCode, err = apiTest.ProjectsDelete(*admin, projectID)
|
|
||||||
if err != nil {
|
|
||||||
t.Error("Error while delete project", err.Error())
|
|
||||||
t.Log(err)
|
|
||||||
} else {
|
|
||||||
assert.Equal(int(200), httpStatusCode, "Case 2: Project deletion status should be 200")
|
|
||||||
}
|
|
||||||
|
|
||||||
// --------------------------case 3: Response Code=404,Project does not exist---------------------------------//
|
|
||||||
fmt.Println("case 3: Response Code=404,Project does not exist")
|
|
||||||
projectID = "11"
|
|
||||||
httpStatusCode, err = apiTest.ProjectsDelete(*admin, projectID)
|
|
||||||
if err != nil {
|
|
||||||
t.Error("Error while delete project", err.Error())
|
|
||||||
t.Log(err)
|
|
||||||
} else {
|
|
||||||
assert.Equal(int(404), httpStatusCode, "Case 3: Project deletion status should be 404")
|
|
||||||
}
|
|
||||||
|
|
||||||
// --------------------------case 4: Response Code=400,Invalid project id.---------------------------------//
|
|
||||||
fmt.Println("case 4: Response Code=400,Invalid project id.")
|
|
||||||
projectID = "cc"
|
|
||||||
httpStatusCode, err = apiTest.ProjectsDelete(*admin, projectID)
|
|
||||||
if err != nil {
|
|
||||||
t.Error("Error while delete project", err.Error())
|
|
||||||
t.Log(err)
|
|
||||||
} else {
|
|
||||||
assert.Equal(int(400), httpStatusCode, "Case 4: Project deletion status should be 400")
|
|
||||||
}
|
|
||||||
fmt.Printf("\n")
|
|
||||||
|
|
||||||
}
|
|
||||||
func TestProHead(t *testing.T) {
|
|
||||||
t.Log("\nTest for Project HEAD API")
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
apiTest := newHarborAPI()
|
|
||||||
|
|
||||||
// ----------------------------case 1 : Response Code=200----------------------------//
|
|
||||||
t.Log("case 1: response code:200")
|
|
||||||
httpStatusCode, err := apiTest.ProjectsHead(*admin, "library")
|
|
||||||
if err != nil {
|
|
||||||
t.Error("Error while search project by proName", err.Error())
|
|
||||||
t.Log(err)
|
|
||||||
} else {
|
|
||||||
assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200")
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------case 2 : Response Code=404:Project name does not exist.----------------------------//
|
|
||||||
t.Log("case 2: response code:404,Project name does not exist.")
|
|
||||||
httpStatusCode, err = apiTest.ProjectsHead(*admin, "libra")
|
|
||||||
if err != nil {
|
|
||||||
t.Error("Error while search project by proName", err.Error())
|
|
||||||
t.Log(err)
|
|
||||||
} else {
|
|
||||||
assert.Equal(int(404), httpStatusCode, "httpStatusCode should be 404")
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Log("case 3: response code:401. Project exist with unauthenticated user")
|
|
||||||
httpStatusCode, err = apiTest.ProjectsHead(*unknownUsr, "library")
|
|
||||||
if err != nil {
|
|
||||||
t.Error("Error while search project by proName", err.Error())
|
|
||||||
t.Log(err)
|
|
||||||
} else {
|
|
||||||
assert.Equal(int(401), httpStatusCode, "httpStatusCode should be 404")
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Log("case 4: response code:401. Project name does not exist with unauthenticated user")
|
|
||||||
httpStatusCode, err = apiTest.ProjectsHead(*unknownUsr, "libra")
|
|
||||||
if err != nil {
|
|
||||||
t.Error("Error while search project by proName", err.Error())
|
|
||||||
t.Log(err)
|
|
||||||
} else {
|
|
||||||
assert.Equal(int(401), httpStatusCode, "httpStatusCode should be 404")
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPut(t *testing.T) {
|
|
||||||
fmt.Println("\nTest for Project PUT API: Update properties for a selected project")
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
apiTest := newHarborAPI()
|
|
||||||
|
|
||||||
project := &models.Project{
|
|
||||||
Metadata: map[string]string{
|
|
||||||
models.ProMetaPublic: "true",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println("case 1: response code:200")
|
|
||||||
code, err := apiTest.ProjectsPut(*admin, "1", project)
|
|
||||||
require.Nil(t, err)
|
|
||||||
assert.Equal(int(200), code)
|
|
||||||
|
|
||||||
fmt.Println("case 2: response code:401, User need to log in first.")
|
|
||||||
code, err = apiTest.ProjectsPut(*unknownUsr, "1", project)
|
|
||||||
require.Nil(t, err)
|
|
||||||
assert.Equal(int(401), code)
|
|
||||||
|
|
||||||
fmt.Println("case 3: response code:400, Invalid project id")
|
|
||||||
code, err = apiTest.ProjectsPut(*admin, "cc", project)
|
|
||||||
require.Nil(t, err)
|
|
||||||
assert.Equal(int(400), code)
|
|
||||||
|
|
||||||
fmt.Println("case 4: response code:404, Not found the project")
|
|
||||||
code, err = apiTest.ProjectsPut(*admin, "1234", project)
|
|
||||||
require.Nil(t, err)
|
|
||||||
assert.Equal(int(404), code)
|
|
||||||
|
|
||||||
fmt.Printf("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDeletable(t *testing.T) {
|
|
||||||
apiTest := newHarborAPI()
|
|
||||||
chServer, oldController, err := mockChartController()
|
|
||||||
require.Nil(t, err)
|
|
||||||
require.NotNil(t, chServer)
|
|
||||||
defer chServer.Close()
|
|
||||||
defer func() {
|
|
||||||
chartController = oldController
|
|
||||||
}()
|
|
||||||
|
|
||||||
project := models.Project{
|
|
||||||
Name: "project_for_test_deletable",
|
|
||||||
OwnerID: 1,
|
|
||||||
}
|
|
||||||
id, err := dao.AddProject(project)
|
|
||||||
require.Nil(t, err)
|
|
||||||
|
|
||||||
// non-exist project
|
|
||||||
code, del, err := apiTest.ProjectDeletable(*admin, 1000)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
assert.Equal(t, http.StatusNotFound, code)
|
|
||||||
|
|
||||||
// unauthorized
|
|
||||||
code, del, err = apiTest.ProjectDeletable(*unknownUsr, id)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
assert.Equal(t, http.StatusUnauthorized, code)
|
|
||||||
|
|
||||||
// can be deleted
|
|
||||||
code, del, err = apiTest.ProjectDeletable(*admin, id)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
assert.Equal(t, http.StatusOK, code)
|
|
||||||
assert.True(t, del)
|
|
||||||
|
|
||||||
err = dao.AddRepository(models.RepoRecord{
|
|
||||||
Name: project.Name + "/golang",
|
|
||||||
ProjectID: id,
|
|
||||||
})
|
|
||||||
require.Nil(t, err)
|
|
||||||
|
|
||||||
// can not be deleted as contains repository
|
|
||||||
code, del, err = apiTest.ProjectDeletable(*admin, id)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
assert.Equal(t, http.StatusOK, code)
|
|
||||||
assert.False(t, del)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestProjectSummary(t *testing.T) {
|
|
||||||
fmt.Println("\nTest for Project Summary API")
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
apiTest := newHarborAPI()
|
|
||||||
|
|
||||||
projectID, err := addProjectByName(apiTest, "project-summary")
|
|
||||||
assert.Nil(err)
|
|
||||||
defer func() {
|
|
||||||
deleteProjectByIDs(apiTest, projectID)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// ----------------------------case 1 : Response Code=200----------------------------//
|
|
||||||
fmt.Println("case 1: response code:200")
|
|
||||||
httpStatusCode, summary, err := apiTest.ProjectSummary(*admin, fmt.Sprintf("%d", projectID))
|
|
||||||
if err != nil {
|
|
||||||
t.Error("Error while search project by proName", err.Error())
|
|
||||||
t.Log(err)
|
|
||||||
} else {
|
|
||||||
assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200")
|
|
||||||
assert.Equal(int64(1), summary.ProjectAdminCount)
|
|
||||||
assert.Equal(map[string]int64{"storage": -1}, summary.Quota.Hard)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHighestRole(t *testing.T) {
|
|
||||||
cases := []struct {
|
|
||||||
input []int
|
|
||||||
expect int
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
[]int{},
|
|
||||||
0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
[]int{
|
|
||||||
common.RoleDeveloper,
|
|
||||||
common.RoleMaintainer,
|
|
||||||
common.RoleLimitedGuest,
|
|
||||||
},
|
|
||||||
common.RoleMaintainer,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
[]int{
|
|
||||||
common.RoleProjectAdmin,
|
|
||||||
common.RoleMaintainer,
|
|
||||||
common.RoleMaintainer,
|
|
||||||
},
|
|
||||||
common.RoleProjectAdmin,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
[]int{
|
|
||||||
99,
|
|
||||||
33,
|
|
||||||
common.RoleLimitedGuest,
|
|
||||||
},
|
|
||||||
common.RoleLimitedGuest,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
[]int{
|
|
||||||
99,
|
|
||||||
99,
|
|
||||||
99,
|
|
||||||
},
|
|
||||||
0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
nil,
|
|
||||||
0,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, c := range cases {
|
|
||||||
assert.Equal(t, c.expect, highestRole(c.input))
|
|
||||||
}
|
|
||||||
}
|
|
@ -20,6 +20,7 @@ import (
|
|||||||
|
|
||||||
"helm.sh/helm/v3/cmd/helm/search"
|
"helm.sh/helm/v3/cmd/helm/search"
|
||||||
|
|
||||||
|
"github.com/goharbor/harbor/src/common"
|
||||||
"github.com/goharbor/harbor/src/common/dao"
|
"github.com/goharbor/harbor/src/common/dao"
|
||||||
pro "github.com/goharbor/harbor/src/common/dao/project"
|
pro "github.com/goharbor/harbor/src/common/dao/project"
|
||||||
"github.com/goharbor/harbor/src/common/models"
|
"github.com/goharbor/harbor/src/common/models"
|
||||||
@ -205,3 +206,27 @@ func filterRepositories(projects []*models.Project, keyword string) (
|
|||||||
}
|
}
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns the highest role in the role list.
|
||||||
|
// This func should be removed once we deprecate the "current_user_role_id" in project API
|
||||||
|
// A user can have multiple roles and they may not have a strict ranking relationship
|
||||||
|
func highestRole(roles []int) int {
|
||||||
|
if roles == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
rolePower := map[int]int{
|
||||||
|
common.RoleProjectAdmin: 50,
|
||||||
|
common.RoleMaintainer: 40,
|
||||||
|
common.RoleDeveloper: 30,
|
||||||
|
common.RoleGuest: 20,
|
||||||
|
common.RoleLimitedGuest: 10,
|
||||||
|
}
|
||||||
|
var highest, highestPower int
|
||||||
|
for _, role := range roles {
|
||||||
|
if p, ok := rolePower[role]; ok && p > highestPower {
|
||||||
|
highest = role
|
||||||
|
highestPower = p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return highest
|
||||||
|
}
|
||||||
|
@ -18,20 +18,18 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/goharbor/harbor/src/lib/errors"
|
|
||||||
redislib "github.com/goharbor/harbor/src/lib/redis"
|
|
||||||
"github.com/goharbor/harbor/src/pkg/artifactrash/model"
|
|
||||||
blob_models "github.com/goharbor/harbor/src/pkg/blob/models"
|
|
||||||
|
|
||||||
"github.com/goharbor/harbor/src/common/models"
|
|
||||||
"github.com/goharbor/harbor/src/common/registryctl"
|
"github.com/goharbor/harbor/src/common/registryctl"
|
||||||
"github.com/goharbor/harbor/src/controller/artifact"
|
"github.com/goharbor/harbor/src/controller/artifact"
|
||||||
"github.com/goharbor/harbor/src/controller/project"
|
"github.com/goharbor/harbor/src/controller/project"
|
||||||
"github.com/goharbor/harbor/src/jobservice/job"
|
"github.com/goharbor/harbor/src/jobservice/job"
|
||||||
"github.com/goharbor/harbor/src/jobservice/logger"
|
"github.com/goharbor/harbor/src/jobservice/logger"
|
||||||
|
"github.com/goharbor/harbor/src/lib/errors"
|
||||||
"github.com/goharbor/harbor/src/lib/q"
|
"github.com/goharbor/harbor/src/lib/q"
|
||||||
|
redislib "github.com/goharbor/harbor/src/lib/redis"
|
||||||
"github.com/goharbor/harbor/src/pkg/artifactrash"
|
"github.com/goharbor/harbor/src/pkg/artifactrash"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/artifactrash/model"
|
||||||
"github.com/goharbor/harbor/src/pkg/blob"
|
"github.com/goharbor/harbor/src/pkg/blob"
|
||||||
|
blob_models "github.com/goharbor/harbor/src/pkg/blob/models"
|
||||||
"github.com/goharbor/harbor/src/registryctl/client"
|
"github.com/goharbor/harbor/src/registryctl/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -52,7 +50,6 @@ type GarbageCollector struct {
|
|||||||
artCtl artifact.Controller
|
artCtl artifact.Controller
|
||||||
artrashMgr artifactrash.Manager
|
artrashMgr artifactrash.Manager
|
||||||
blobMgr blob.Manager
|
blobMgr blob.Manager
|
||||||
projectCtl project.Controller
|
|
||||||
registryCtlClient client.Client
|
registryCtlClient client.Client
|
||||||
logger logger.Interface
|
logger logger.Interface
|
||||||
redisURL string
|
redisURL string
|
||||||
@ -103,7 +100,6 @@ func (gc *GarbageCollector) init(ctx job.Context, params job.Parameters) error {
|
|||||||
gc.artCtl = artifact.Ctl
|
gc.artCtl = artifact.Ctl
|
||||||
gc.artrashMgr = artifactrash.NewManager()
|
gc.artrashMgr = artifactrash.NewManager()
|
||||||
gc.blobMgr = blob.NewManager()
|
gc.blobMgr = blob.NewManager()
|
||||||
gc.projectCtl = project.Ctl
|
|
||||||
}
|
}
|
||||||
if err := gc.registryCtlClient.Health(); err != nil {
|
if err := gc.registryCtlClient.Health(); err != nil {
|
||||||
gc.logger.Errorf("failed to start gc as registry controller is unreachable: %v", err)
|
gc.logger.Errorf("failed to start gc as registry controller is unreachable: %v", err)
|
||||||
@ -416,50 +412,21 @@ func (gc *GarbageCollector) deletedArt(ctx job.Context) (map[string][]model.Arti
|
|||||||
|
|
||||||
// clean the untagged blobs in each project, these blobs are not referenced by any manifest and will be cleaned by GC
|
// clean the untagged blobs in each project, these blobs are not referenced by any manifest and will be cleaned by GC
|
||||||
func (gc *GarbageCollector) removeUntaggedBlobs(ctx job.Context) {
|
func (gc *GarbageCollector) removeUntaggedBlobs(ctx job.Context) {
|
||||||
// get all projects
|
for result := range project.ListAll(ctx.SystemContext(), 50, nil, project.Metadata(false)) {
|
||||||
projects := func(chunkSize int) <-chan *models.Project {
|
if result.Error != nil {
|
||||||
ch := make(chan *models.Project, chunkSize)
|
gc.logger.Errorf("remove untagged blobs for all projects got error: %v", result.Error)
|
||||||
|
continue
|
||||||
go func() {
|
}
|
||||||
defer close(ch)
|
p := result.Data
|
||||||
|
|
||||||
params := &models.ProjectQueryParam{
|
|
||||||
Pagination: &models.Pagination{Page: 1, Size: int64(chunkSize)},
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
|
||||||
results, err := gc.projectCtl.List(ctx.SystemContext(), params, project.Metadata(false))
|
|
||||||
if err != nil {
|
|
||||||
gc.logger.Errorf("list projects failed, error: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, p := range results {
|
|
||||||
ch <- p
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(results) < chunkSize {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
params.Pagination.Page++
|
|
||||||
}
|
|
||||||
|
|
||||||
}()
|
|
||||||
|
|
||||||
return ch
|
|
||||||
}(50)
|
|
||||||
|
|
||||||
for project := range projects {
|
|
||||||
all, err := gc.blobMgr.List(ctx.SystemContext(), blob.ListParams{
|
all, err := gc.blobMgr.List(ctx.SystemContext(), blob.ListParams{
|
||||||
ProjectID: project.ProjectID,
|
ProjectID: p.ProjectID,
|
||||||
UpdateTime: time.Now().Add(-time.Duration(gc.timeWindowHours) * time.Hour),
|
UpdateTime: time.Now().Add(-time.Duration(gc.timeWindowHours) * time.Hour),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
gc.logger.Errorf("failed to get blobs of project, %v", err)
|
gc.logger.Errorf("failed to get blobs of project, %v", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err := gc.blobMgr.CleanupAssociationsForProject(ctx.SystemContext(), project.ProjectID, all); err != nil {
|
if err := gc.blobMgr.CleanupAssociationsForProject(ctx.SystemContext(), p.ProjectID, all); err != nil {
|
||||||
gc.logger.Errorf("failed to clean untagged blobs of project, %v", err)
|
gc.logger.Errorf("failed to clean untagged blobs of project, %v", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,27 @@
|
|||||||
|
// Copyright Project Harbor Authors
|
||||||
|
//
|
||||||
|
// 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 gc
|
package gc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
"github.com/docker/distribution/manifest/schema2"
|
"github.com/docker/distribution/manifest/schema2"
|
||||||
"github.com/goharbor/harbor/src/common/models"
|
"github.com/goharbor/harbor/src/common/models"
|
||||||
commom_regctl "github.com/goharbor/harbor/src/common/registryctl"
|
commom_regctl "github.com/goharbor/harbor/src/common/registryctl"
|
||||||
|
"github.com/goharbor/harbor/src/controller/project"
|
||||||
"github.com/goharbor/harbor/src/jobservice/job"
|
"github.com/goharbor/harbor/src/jobservice/job"
|
||||||
"github.com/goharbor/harbor/src/pkg/artifact"
|
"github.com/goharbor/harbor/src/pkg/artifact"
|
||||||
"github.com/goharbor/harbor/src/pkg/artifactrash/model"
|
"github.com/goharbor/harbor/src/pkg/artifactrash/model"
|
||||||
@ -17,8 +35,6 @@ import (
|
|||||||
"github.com/goharbor/harbor/src/testing/pkg/blob"
|
"github.com/goharbor/harbor/src/testing/pkg/blob"
|
||||||
"github.com/goharbor/harbor/src/testing/registryctl"
|
"github.com/goharbor/harbor/src/testing/registryctl"
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type gcTestSuite struct {
|
type gcTestSuite struct {
|
||||||
@ -29,6 +45,8 @@ type gcTestSuite struct {
|
|||||||
projectCtl *projecttesting.Controller
|
projectCtl *projecttesting.Controller
|
||||||
blobMgr *blob.Manager
|
blobMgr *blob.Manager
|
||||||
|
|
||||||
|
originalProjectCtl project.Controller
|
||||||
|
|
||||||
regCtlInit func()
|
regCtlInit func()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,9 +57,16 @@ func (suite *gcTestSuite) SetupTest() {
|
|||||||
suite.blobMgr = &blob.Manager{}
|
suite.blobMgr = &blob.Manager{}
|
||||||
suite.projectCtl = &projecttesting.Controller{}
|
suite.projectCtl = &projecttesting.Controller{}
|
||||||
|
|
||||||
|
suite.originalProjectCtl = project.Ctl
|
||||||
|
project.Ctl = suite.projectCtl
|
||||||
|
|
||||||
regCtlInit = func() { commom_regctl.RegistryCtlClient = suite.registryCtlClient }
|
regCtlInit = func() { commom_regctl.RegistryCtlClient = suite.registryCtlClient }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (suite *gcTestSuite) TearDownTest() {
|
||||||
|
project.Ctl = suite.originalProjectCtl
|
||||||
|
}
|
||||||
|
|
||||||
func (suite *gcTestSuite) TestMaxFails() {
|
func (suite *gcTestSuite) TestMaxFails() {
|
||||||
gc := &GarbageCollector{}
|
gc := &GarbageCollector{}
|
||||||
suite.Equal(uint(1), gc.MaxFails())
|
suite.Equal(uint(1), gc.MaxFails())
|
||||||
@ -110,8 +135,7 @@ func (suite *gcTestSuite) TestRemoveUntaggedBlobs() {
|
|||||||
mock.OnAnything(suite.blobMgr, "CleanupAssociationsForProject").Return(nil)
|
mock.OnAnything(suite.blobMgr, "CleanupAssociationsForProject").Return(nil)
|
||||||
|
|
||||||
gc := &GarbageCollector{
|
gc := &GarbageCollector{
|
||||||
projectCtl: suite.projectCtl,
|
blobMgr: suite.blobMgr,
|
||||||
blobMgr: suite.blobMgr,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suite.NotPanics(func() {
|
suite.NotPanics(func() {
|
||||||
@ -244,7 +268,6 @@ func (suite *gcTestSuite) TestRun() {
|
|||||||
gc := &GarbageCollector{
|
gc := &GarbageCollector{
|
||||||
artCtl: suite.artifactCtl,
|
artCtl: suite.artifactCtl,
|
||||||
artrashMgr: suite.artrashMgr,
|
artrashMgr: suite.artrashMgr,
|
||||||
projectCtl: suite.projectCtl,
|
|
||||||
blobMgr: suite.blobMgr,
|
blobMgr: suite.blobMgr,
|
||||||
registryCtlClient: suite.registryCtlClient,
|
registryCtlClient: suite.registryCtlClient,
|
||||||
}
|
}
|
||||||
@ -318,7 +341,6 @@ func (suite *gcTestSuite) TestMark() {
|
|||||||
gc := &GarbageCollector{
|
gc := &GarbageCollector{
|
||||||
artCtl: suite.artifactCtl,
|
artCtl: suite.artifactCtl,
|
||||||
artrashMgr: suite.artrashMgr,
|
artrashMgr: suite.artrashMgr,
|
||||||
projectCtl: suite.projectCtl,
|
|
||||||
blobMgr: suite.blobMgr,
|
blobMgr: suite.blobMgr,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -336,7 +358,6 @@ func (suite *gcTestSuite) TestSweep() {
|
|||||||
gc := &GarbageCollector{
|
gc := &GarbageCollector{
|
||||||
artCtl: suite.artifactCtl,
|
artCtl: suite.artifactCtl,
|
||||||
artrashMgr: suite.artrashMgr,
|
artrashMgr: suite.artrashMgr,
|
||||||
projectCtl: suite.projectCtl,
|
|
||||||
blobMgr: suite.blobMgr,
|
blobMgr: suite.blobMgr,
|
||||||
registryCtlClient: suite.registryCtlClient,
|
registryCtlClient: suite.registryCtlClient,
|
||||||
deleteSet: []*pkg_blob.Blob{
|
deleteSet: []*pkg_blob.Blob{
|
||||||
|
@ -16,22 +16,20 @@ package gcreadonly
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
redislib "github.com/goharbor/harbor/src/lib/redis"
|
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/goharbor/harbor/src/common/models"
|
|
||||||
"github.com/goharbor/harbor/src/controller/artifact"
|
|
||||||
"github.com/goharbor/harbor/src/controller/project"
|
|
||||||
"github.com/goharbor/harbor/src/lib/q"
|
|
||||||
"github.com/goharbor/harbor/src/pkg/artifactrash"
|
|
||||||
"github.com/goharbor/harbor/src/pkg/blob"
|
|
||||||
|
|
||||||
"github.com/goharbor/harbor/src/common"
|
"github.com/goharbor/harbor/src/common"
|
||||||
"github.com/goharbor/harbor/src/common/config"
|
"github.com/goharbor/harbor/src/common/config"
|
||||||
"github.com/goharbor/harbor/src/common/registryctl"
|
"github.com/goharbor/harbor/src/common/registryctl"
|
||||||
|
"github.com/goharbor/harbor/src/controller/artifact"
|
||||||
|
"github.com/goharbor/harbor/src/controller/project"
|
||||||
"github.com/goharbor/harbor/src/jobservice/job"
|
"github.com/goharbor/harbor/src/jobservice/job"
|
||||||
"github.com/goharbor/harbor/src/jobservice/logger"
|
"github.com/goharbor/harbor/src/jobservice/logger"
|
||||||
|
"github.com/goharbor/harbor/src/lib/q"
|
||||||
|
redislib "github.com/goharbor/harbor/src/lib/redis"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/artifactrash"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/blob"
|
||||||
"github.com/goharbor/harbor/src/registryctl/client"
|
"github.com/goharbor/harbor/src/registryctl/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -160,7 +158,6 @@ func (gc *GarbageCollector) init(ctx job.Context, params job.Parameters) error {
|
|||||||
gc.artCtl = artifact.Ctl
|
gc.artCtl = artifact.Ctl
|
||||||
gc.artrashMgr = artifactrash.NewManager()
|
gc.artrashMgr = artifactrash.NewManager()
|
||||||
gc.blobMgr = blob.NewManager()
|
gc.blobMgr = blob.NewManager()
|
||||||
gc.projectCtl = project.Ctl
|
|
||||||
}
|
}
|
||||||
if err := gc.registryCtlClient.Health(); err != nil {
|
if err := gc.registryCtlClient.Health(); err != nil {
|
||||||
gc.logger.Errorf("failed to start gc as registry controller is unreachable: %v", err)
|
gc.logger.Errorf("failed to start gc as registry controller is unreachable: %v", err)
|
||||||
@ -285,49 +282,20 @@ func (gc *GarbageCollector) deleteCandidates(ctx job.Context) error {
|
|||||||
|
|
||||||
// clean the untagged blobs in each project, these blobs are not referenced by any manifest and will be cleaned by GC
|
// clean the untagged blobs in each project, these blobs are not referenced by any manifest and will be cleaned by GC
|
||||||
func (gc *GarbageCollector) removeUntaggedBlobs(ctx job.Context) {
|
func (gc *GarbageCollector) removeUntaggedBlobs(ctx job.Context) {
|
||||||
// get all projects
|
for result := range project.ListAll(ctx.SystemContext(), 50, nil, project.Metadata(false)) {
|
||||||
projects := func(chunkSize int) <-chan *models.Project {
|
if result.Error != nil {
|
||||||
ch := make(chan *models.Project, chunkSize)
|
gc.logger.Errorf("remove untagged blobs for all projects got error: %v", result.Error)
|
||||||
|
continue
|
||||||
go func() {
|
}
|
||||||
defer close(ch)
|
p := result.Data
|
||||||
|
|
||||||
params := &models.ProjectQueryParam{
|
|
||||||
Pagination: &models.Pagination{Page: 1, Size: int64(chunkSize)},
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
|
||||||
results, err := gc.projectCtl.List(ctx.SystemContext(), params, project.Metadata(false))
|
|
||||||
if err != nil {
|
|
||||||
gc.logger.Errorf("list projects failed, error: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, p := range results {
|
|
||||||
ch <- p
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(results) < chunkSize {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
params.Pagination.Page++
|
|
||||||
}
|
|
||||||
|
|
||||||
}()
|
|
||||||
|
|
||||||
return ch
|
|
||||||
}(50)
|
|
||||||
|
|
||||||
for project := range projects {
|
|
||||||
all, err := gc.blobMgr.List(ctx.SystemContext(), blob.ListParams{
|
all, err := gc.blobMgr.List(ctx.SystemContext(), blob.ListParams{
|
||||||
ProjectID: project.ProjectID,
|
ProjectID: p.ProjectID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
gc.logger.Errorf("failed to get blobs of project, %v", err)
|
gc.logger.Errorf("failed to get blobs of project, %v", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err := gc.blobMgr.CleanupAssociationsForProject(ctx.SystemContext(), project.ProjectID, all); err != nil {
|
if err := gc.blobMgr.CleanupAssociationsForProject(ctx.SystemContext(), p.ProjectID, all); err != nil {
|
||||||
gc.logger.Errorf("failed to clean untagged blobs of project, %v", err)
|
gc.logger.Errorf("failed to clean untagged blobs of project, %v", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,27 @@
|
|||||||
|
// Copyright Project Harbor Authors
|
||||||
|
//
|
||||||
|
// 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 gcreadonly
|
package gcreadonly
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
"github.com/goharbor/harbor/src/common/config"
|
"github.com/goharbor/harbor/src/common/config"
|
||||||
"github.com/goharbor/harbor/src/common/models"
|
"github.com/goharbor/harbor/src/common/models"
|
||||||
commom_regctl "github.com/goharbor/harbor/src/common/registryctl"
|
commom_regctl "github.com/goharbor/harbor/src/common/registryctl"
|
||||||
|
"github.com/goharbor/harbor/src/controller/project"
|
||||||
"github.com/goharbor/harbor/src/jobservice/job"
|
"github.com/goharbor/harbor/src/jobservice/job"
|
||||||
"github.com/goharbor/harbor/src/pkg/artifact"
|
"github.com/goharbor/harbor/src/pkg/artifact"
|
||||||
"github.com/goharbor/harbor/src/pkg/artifactrash/model"
|
"github.com/goharbor/harbor/src/pkg/artifactrash/model"
|
||||||
@ -16,8 +34,6 @@ import (
|
|||||||
"github.com/goharbor/harbor/src/testing/pkg/blob"
|
"github.com/goharbor/harbor/src/testing/pkg/blob"
|
||||||
"github.com/goharbor/harbor/src/testing/registryctl"
|
"github.com/goharbor/harbor/src/testing/registryctl"
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type gcTestSuite struct {
|
type gcTestSuite struct {
|
||||||
@ -28,6 +44,8 @@ type gcTestSuite struct {
|
|||||||
projectCtl *projecttesting.Controller
|
projectCtl *projecttesting.Controller
|
||||||
blobMgr *blob.Manager
|
blobMgr *blob.Manager
|
||||||
|
|
||||||
|
originalProjectCtl project.Controller
|
||||||
|
|
||||||
regCtlInit func()
|
regCtlInit func()
|
||||||
setReadOnly func(cfgMgr *config.CfgManager, switcher bool) error
|
setReadOnly func(cfgMgr *config.CfgManager, switcher bool) error
|
||||||
getReadOnly func(cfgMgr *config.CfgManager) (bool, error)
|
getReadOnly func(cfgMgr *config.CfgManager) (bool, error)
|
||||||
@ -40,11 +58,18 @@ func (suite *gcTestSuite) SetupTest() {
|
|||||||
suite.blobMgr = &blob.Manager{}
|
suite.blobMgr = &blob.Manager{}
|
||||||
suite.projectCtl = &projecttesting.Controller{}
|
suite.projectCtl = &projecttesting.Controller{}
|
||||||
|
|
||||||
|
suite.originalProjectCtl = project.Ctl
|
||||||
|
project.Ctl = suite.projectCtl
|
||||||
|
|
||||||
regCtlInit = func() { commom_regctl.RegistryCtlClient = suite.registryCtlClient }
|
regCtlInit = func() { commom_regctl.RegistryCtlClient = suite.registryCtlClient }
|
||||||
setReadOnly = func(cfgMgr *config.CfgManager, switcher bool) error { return nil }
|
setReadOnly = func(cfgMgr *config.CfgManager, switcher bool) error { return nil }
|
||||||
getReadOnly = func(cfgMgr *config.CfgManager) (bool, error) { return true, nil }
|
getReadOnly = func(cfgMgr *config.CfgManager) (bool, error) { return true, nil }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (suite *gcTestSuite) TearDownTest() {
|
||||||
|
project.Ctl = suite.originalProjectCtl
|
||||||
|
}
|
||||||
|
|
||||||
func (suite *gcTestSuite) TestMaxFails() {
|
func (suite *gcTestSuite) TestMaxFails() {
|
||||||
gc := &GarbageCollector{}
|
gc := &GarbageCollector{}
|
||||||
suite.Equal(uint(1), gc.MaxFails())
|
suite.Equal(uint(1), gc.MaxFails())
|
||||||
@ -105,8 +130,7 @@ func (suite *gcTestSuite) TestRemoveUntaggedBlobs() {
|
|||||||
mock.OnAnything(suite.blobMgr, "CleanupAssociationsForProject").Return(nil)
|
mock.OnAnything(suite.blobMgr, "CleanupAssociationsForProject").Return(nil)
|
||||||
|
|
||||||
gc := &GarbageCollector{
|
gc := &GarbageCollector{
|
||||||
projectCtl: suite.projectCtl,
|
blobMgr: suite.blobMgr,
|
||||||
blobMgr: suite.blobMgr,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suite.NotPanics(func() {
|
suite.NotPanics(func() {
|
||||||
@ -218,7 +242,6 @@ func (suite *gcTestSuite) TestRun() {
|
|||||||
artCtl: suite.artifactCtl,
|
artCtl: suite.artifactCtl,
|
||||||
artrashMgr: suite.artrashMgr,
|
artrashMgr: suite.artrashMgr,
|
||||||
cfgMgr: config.NewInMemoryManager(),
|
cfgMgr: config.NewInMemoryManager(),
|
||||||
projectCtl: suite.projectCtl,
|
|
||||||
blobMgr: suite.blobMgr,
|
blobMgr: suite.blobMgr,
|
||||||
registryCtlClient: suite.registryCtlClient,
|
registryCtlClient: suite.registryCtlClient,
|
||||||
}
|
}
|
||||||
|
62
src/lib/convert_types.go
Normal file
62
src/lib/convert_types.go
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
// Copyright Project Harbor Authors
|
||||||
|
//
|
||||||
|
// 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 lib
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BoolValue returns the value of the bool pointer or false if the pointer is nil
|
||||||
|
func BoolValue(v *bool) bool {
|
||||||
|
if v != nil {
|
||||||
|
return *v
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Int64Value returns the value of the int64 pointer or 0 if the pointer is nil
|
||||||
|
func Int64Value(v *int64) int64 {
|
||||||
|
if v != nil {
|
||||||
|
return *v
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringValue returns the value of the string pointer or "" if the pointer is nil
|
||||||
|
func StringValue(v *string) string {
|
||||||
|
if v != nil {
|
||||||
|
return *v
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToBool convert interface to bool
|
||||||
|
func ToBool(v interface{}) bool {
|
||||||
|
switch b := v.(type) {
|
||||||
|
case bool:
|
||||||
|
return b
|
||||||
|
case nil:
|
||||||
|
return false
|
||||||
|
case int:
|
||||||
|
return v.(int) != 0
|
||||||
|
case int64:
|
||||||
|
return v.(int64) != 0
|
||||||
|
case string:
|
||||||
|
r, _ := strconv.ParseBool(v.(string))
|
||||||
|
return r
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
51
src/lib/convert_types_test.go
Normal file
51
src/lib/convert_types_test.go
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
// Copyright Project Harbor Authors
|
||||||
|
//
|
||||||
|
// 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 lib
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestToBool(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
v interface{}
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{"nil", args{nil}, false},
|
||||||
|
{"bool true", args{true}, true},
|
||||||
|
{"bool false", args{false}, false},
|
||||||
|
{"string true", args{"true"}, true},
|
||||||
|
{"string True", args{"True"}, true},
|
||||||
|
{"string 1", args{"1"}, true},
|
||||||
|
{"string false", args{"false"}, false},
|
||||||
|
{"string False", args{"False"}, false},
|
||||||
|
{"string 0", args{"0"}, false},
|
||||||
|
{"int 1", args{1}, true},
|
||||||
|
{"int 0", args{0}, false},
|
||||||
|
{"int64 1", args{int64(1)}, true},
|
||||||
|
{"int64 0", args{int64(0)}, false},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := ToBool(tt.args.v); got != tt.want {
|
||||||
|
t.Errorf("ToBool() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
31
src/lib/json_copy.go
Normal file
31
src/lib/json_copy.go
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
// Copyright Project Harbor Authors
|
||||||
|
//
|
||||||
|
// 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 lib
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
// JSONCopy copy from src to dst with json marshal and unmarshal.
|
||||||
|
// NOTE: copy from one struct to another struct may miss some values depend on
|
||||||
|
// the json tag in the fields, you should know what will happened when call this function.
|
||||||
|
func JSONCopy(dst, src interface{}) error {
|
||||||
|
data, err := json.Marshal(src)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Unmarshal(data, dst)
|
||||||
|
}
|
68
src/lib/json_copy_test.go
Normal file
68
src/lib/json_copy_test.go
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
// Copyright Project Harbor Authors
|
||||||
|
//
|
||||||
|
// 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 lib
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
type jsonCopyFoo struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Age int `json:"age"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJSONCopy(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
{
|
||||||
|
var m map[string]interface{}
|
||||||
|
foo := &jsonCopyFoo{
|
||||||
|
Name: "foo",
|
||||||
|
Age: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Nil(m)
|
||||||
|
assert.Nil(JSONCopy(&m, foo))
|
||||||
|
assert.NotNil(m)
|
||||||
|
assert.Len(m, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
var m map[string]interface{}
|
||||||
|
var foo *jsonCopyFoo
|
||||||
|
|
||||||
|
assert.Nil(m)
|
||||||
|
assert.Nil(JSONCopy(&m, foo))
|
||||||
|
assert.Nil(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
m := map[string]interface{}{
|
||||||
|
"name": "foo",
|
||||||
|
"age": 1,
|
||||||
|
}
|
||||||
|
var foo *jsonCopyFoo
|
||||||
|
assert.Nil(JSONCopy(&foo, &m))
|
||||||
|
assert.NotNil(foo)
|
||||||
|
assert.Equal("foo", foo.Name)
|
||||||
|
assert.Equal(1, foo.Age)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
assert.Error(JSONCopy(nil, JSONCopy))
|
||||||
|
}
|
||||||
|
}
|
@ -51,6 +51,11 @@ func Context() context.Context {
|
|||||||
return NewContext(context.Background(), orm.NewOrm())
|
return NewContext(context.Background(), orm.NewOrm())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clone returns new context with orm for ctx
|
||||||
|
func Clone(ctx context.Context) context.Context {
|
||||||
|
return NewContext(ctx, orm.NewOrm())
|
||||||
|
}
|
||||||
|
|
||||||
// WithTransaction a decorator which make f run in transaction
|
// WithTransaction a decorator which make f run in transaction
|
||||||
func WithTransaction(f func(ctx context.Context) error) func(ctx context.Context) error {
|
func WithTransaction(f func(ctx context.Context) error) func(ctx context.Context) error {
|
||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
|
@ -27,7 +27,7 @@ import (
|
|||||||
|
|
||||||
func abstractArtData(ctx context.Context) error {
|
func abstractArtData(ctx context.Context) error {
|
||||||
abstractor := art.NewAbstractor()
|
abstractor := art.NewAbstractor()
|
||||||
pros, err := project.Mgr.List(ctx)
|
pros, err := project.Mgr.List(ctx, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -94,6 +94,7 @@ func (d *dao) Create(ctx context.Context, project *models.Project) (int64, error
|
|||||||
func (d *dao) Count(ctx context.Context, query *q.Query) (total int64, err error) {
|
func (d *dao) Count(ctx context.Context, query *q.Query) (total int64, err error) {
|
||||||
query = q.MustClone(query)
|
query = q.MustClone(query)
|
||||||
query.Keywords["deleted"] = false
|
query.Keywords["deleted"] = false
|
||||||
|
query.Sorting = ""
|
||||||
query.PageNumber = 0
|
query.PageNumber = 0
|
||||||
query.PageSize = 0
|
query.PageSize = 0
|
||||||
|
|
||||||
@ -101,6 +102,7 @@ func (d *dao) Count(ctx context.Context, query *q.Query) (total int64, err error
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return qs.Count()
|
return qs.Count()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -160,6 +162,10 @@ func (d *dao) List(ctx context.Context, query *q.Query) ([]*models.Project, erro
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if query.Sorting != "" {
|
||||||
|
qs = qs.OrderBy(query.Sorting)
|
||||||
|
}
|
||||||
|
|
||||||
projects := []*models.Project{}
|
projects := []*models.Project{}
|
||||||
if _, err := qs.All(&projects); err != nil {
|
if _, err := qs.All(&projects); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -196,9 +196,11 @@ func (suite *DaoTestSuite) TestList() {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
projects, err := suite.dao.List(orm.Context(), q.New(q.KeyWords{"project_id__in": projectIDs}))
|
{
|
||||||
suite.Nil(err)
|
projects, err := suite.dao.List(orm.Context(), q.New(q.KeyWords{"project_id__in": projectIDs}))
|
||||||
suite.Len(projects, len(projectNames))
|
suite.Nil(err)
|
||||||
|
suite.Len(projects, len(projectNames))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *DaoTestSuite) TestListByPublic() {
|
func (suite *DaoTestSuite) TestListByPublic() {
|
||||||
@ -259,6 +261,13 @@ func (suite *DaoTestSuite) TestListByMember() {
|
|||||||
suite.Len(projects, 0)
|
suite.Len(projects, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// guest with public projects
|
||||||
|
projects, err := suite.dao.List(orm.Context(), q.New(q.KeyWords{"member": &models.MemberQuery{Name: "admin", Role: common.RoleGuest, WithPublic: true}}))
|
||||||
|
suite.Nil(err)
|
||||||
|
suite.Len(projects, 1)
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
suite.WithUser(func(userID int64, username string) {
|
suite.WithUser(func(userID int64, username string) {
|
||||||
project := &models.Project{
|
project := &models.Project{
|
||||||
|
@ -16,7 +16,9 @@ package project
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
"github.com/goharbor/harbor/src/common/utils"
|
||||||
"github.com/goharbor/harbor/src/lib/errors"
|
"github.com/goharbor/harbor/src/lib/errors"
|
||||||
"github.com/goharbor/harbor/src/lib/q"
|
"github.com/goharbor/harbor/src/lib/q"
|
||||||
"github.com/goharbor/harbor/src/pkg/project/dao"
|
"github.com/goharbor/harbor/src/pkg/project/dao"
|
||||||
@ -43,7 +45,7 @@ type Manager interface {
|
|||||||
Get(ctx context.Context, idOrName interface{}) (*models.Project, error)
|
Get(ctx context.Context, idOrName interface{}) (*models.Project, error)
|
||||||
|
|
||||||
// List projects according to the query
|
// List projects according to the query
|
||||||
List(ctx context.Context, query ...*models.ProjectQueryParam) ([]*models.Project, error)
|
List(ctx context.Context, query *q.Query) ([]*models.Project, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// New returns a default implementation of Manager
|
// New returns a default implementation of Manager
|
||||||
@ -51,6 +53,14 @@ func New() Manager {
|
|||||||
return &manager{dao: dao.New()}
|
return &manager{dao: dao.New()}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const projectNameMaxLen int = 255
|
||||||
|
const projectNameMinLen int = 1
|
||||||
|
const restrictedNameChars = `[a-z0-9]+(?:[._-][a-z0-9]+)*`
|
||||||
|
|
||||||
|
var (
|
||||||
|
validProjectName = regexp.MustCompile(`^` + restrictedNameChars + `$`)
|
||||||
|
)
|
||||||
|
|
||||||
type manager struct {
|
type manager struct {
|
||||||
dao dao.DAO
|
dao dao.DAO
|
||||||
}
|
}
|
||||||
@ -60,6 +70,17 @@ func (m *manager) Create(ctx context.Context, project *models.Project) (int64, e
|
|||||||
if project.OwnerID <= 0 {
|
if project.OwnerID <= 0 {
|
||||||
return 0, errors.BadRequestError(nil).WithMessage("Owner is missing when creating project %s", project.Name)
|
return 0, errors.BadRequestError(nil).WithMessage("Owner is missing when creating project %s", project.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if utils.IsIllegalLength(project.Name, projectNameMinLen, projectNameMaxLen) {
|
||||||
|
format := "Project name %s is illegal in length. (greater than %d or less than %d)"
|
||||||
|
return 0, errors.BadRequestError(nil).WithMessage(format, project.Name, projectNameMaxLen, projectNameMinLen)
|
||||||
|
}
|
||||||
|
|
||||||
|
legal := validProjectName.MatchString(project.Name)
|
||||||
|
if !legal {
|
||||||
|
return 0, errors.BadRequestError(nil).WithMessage("project name is not in lower case or contains illegal characters")
|
||||||
|
}
|
||||||
|
|
||||||
return m.dao.Create(ctx, project)
|
return m.dao.Create(ctx, project)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,14 +108,6 @@ func (m *manager) Get(ctx context.Context, idOrName interface{}) (*models.Projec
|
|||||||
}
|
}
|
||||||
|
|
||||||
// List projects according to the query
|
// List projects according to the query
|
||||||
func (m *manager) List(ctx context.Context, query ...*models.ProjectQueryParam) ([]*models.Project, error) {
|
func (m *manager) List(ctx context.Context, query *q.Query) ([]*models.Project, error) {
|
||||||
var param *models.ProjectQueryParam
|
return m.dao.List(ctx, query)
|
||||||
if len(query) > 0 {
|
|
||||||
param = query[0]
|
|
||||||
}
|
|
||||||
if param == nil {
|
|
||||||
return m.dao.List(ctx, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
return m.dao.List(ctx, param.ToQuery())
|
|
||||||
}
|
}
|
||||||
|
@ -36,8 +36,5 @@ func (projects Projects) OwnerIDs() []int {
|
|||||||
// Member ...
|
// Member ...
|
||||||
type Member = models.Member
|
type Member = models.Member
|
||||||
|
|
||||||
// ProjectQueryParam ...
|
|
||||||
type ProjectQueryParam = models.ProjectQueryParam
|
|
||||||
|
|
||||||
// MemberQuery ...
|
// MemberQuery ...
|
||||||
type MemberQuery = models.MemberQuery
|
type MemberQuery = models.MemberQuery
|
||||||
|
@ -326,7 +326,7 @@ func launcherError(err error) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getProjects(projectMgr project.Manager) ([]*selector.Candidate, error) {
|
func getProjects(projectMgr project.Manager) ([]*selector.Candidate, error) {
|
||||||
projects, err := projectMgr.List(orm.Context())
|
projects, err := projectMgr.List(orm.Context(), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,6 @@ package dao
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/goharbor/harbor/src/lib/orm"
|
"github.com/goharbor/harbor/src/lib/orm"
|
||||||
"github.com/goharbor/harbor/src/lib/q"
|
"github.com/goharbor/harbor/src/lib/q"
|
||||||
@ -39,34 +38,19 @@ type dao struct{}
|
|||||||
// List list users
|
// List list users
|
||||||
func (d *dao) List(ctx context.Context, query *q.Query) ([]*models.User, error) {
|
func (d *dao) List(ctx context.Context, query *q.Query) ([]*models.User, error) {
|
||||||
query = q.MustClone(query)
|
query = q.MustClone(query)
|
||||||
if query.Sorting == "" {
|
query.Keywords["deleted"] = false
|
||||||
query.Sorting = "username"
|
|
||||||
}
|
|
||||||
|
|
||||||
excludeAdmin := true
|
|
||||||
for key := range query.Keywords {
|
|
||||||
str := strings.ToLower(key)
|
|
||||||
if str == "user_id__in" {
|
|
||||||
excludeAdmin = false
|
|
||||||
break
|
|
||||||
} else if str == "user_id" {
|
|
||||||
excludeAdmin = false
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if excludeAdmin {
|
|
||||||
// Exclude admin account when not filter by UserIDs, see https://github.com/goharbor/harbor/issues/2527
|
|
||||||
query.Keywords["user_id__gt"] = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
qs, err := orm.QuerySetter(ctx, &models.User{}, query)
|
qs, err := orm.QuerySetter(ctx, &models.User{}, query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
users := []*models.User{}
|
if query.Sorting != "" {
|
||||||
if _, err := qs.OrderBy(query.Sorting).All(&users); err != nil {
|
qs = qs.OrderBy(query.Sorting)
|
||||||
|
}
|
||||||
|
|
||||||
|
var users []*models.User
|
||||||
|
if _, err := qs.All(&users); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,6 +39,12 @@ func (suite *DaoTestSuite) TestList() {
|
|||||||
suite.Nil(err)
|
suite.Nil(err)
|
||||||
suite.Len(users, 1)
|
suite.Len(users, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
users, err := suite.dao.List(orm.Context(), q.New(q.KeyWords{"username": "admin"}))
|
||||||
|
suite.Nil(err)
|
||||||
|
suite.Len(users, 1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDaoTestSuite(t *testing.T) {
|
func TestDaoTestSuite(t *testing.T) {
|
||||||
|
@ -16,7 +16,9 @@ package user
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/goharbor/harbor/src/lib/errors"
|
||||||
"github.com/goharbor/harbor/src/lib/q"
|
"github.com/goharbor/harbor/src/lib/q"
|
||||||
"github.com/goharbor/harbor/src/pkg/user/dao"
|
"github.com/goharbor/harbor/src/pkg/user/dao"
|
||||||
"github.com/goharbor/harbor/src/pkg/user/models"
|
"github.com/goharbor/harbor/src/pkg/user/models"
|
||||||
@ -29,6 +31,10 @@ var (
|
|||||||
|
|
||||||
// Manager is used for user management
|
// Manager is used for user management
|
||||||
type Manager interface {
|
type Manager interface {
|
||||||
|
// Get get user by user id
|
||||||
|
Get(ctx context.Context, id int) (*models.User, error)
|
||||||
|
// Get get user by username
|
||||||
|
GetByName(ctx context.Context, username string) (*models.User, error)
|
||||||
// List users according to the query
|
// List users according to the query
|
||||||
List(ctx context.Context, query *q.Query) (models.Users, error)
|
List(ctx context.Context, query *q.Query) (models.Users, error)
|
||||||
}
|
}
|
||||||
@ -42,7 +48,57 @@ type manager struct {
|
|||||||
dao dao.DAO
|
dao dao.DAO
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get get user by user id
|
||||||
|
func (m *manager) Get(ctx context.Context, id int) (*models.User, error) {
|
||||||
|
users, err := m.dao.List(ctx, q.New(q.KeyWords{"user_id": id}))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(users) == 0 {
|
||||||
|
return nil, errors.NotFoundError(nil).WithMessage("user %d not found", id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return users[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get get user by username
|
||||||
|
func (m *manager) GetByName(ctx context.Context, username string) (*models.User, error) {
|
||||||
|
users, err := m.dao.List(ctx, q.New(q.KeyWords{"username": username}))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(users) == 0 {
|
||||||
|
return nil, errors.NotFoundError(nil).WithMessage("user %s not found", username)
|
||||||
|
}
|
||||||
|
|
||||||
|
return users[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
// List users according to the query
|
// List users according to the query
|
||||||
func (m *manager) List(ctx context.Context, query *q.Query) (models.Users, error) {
|
func (m *manager) List(ctx context.Context, query *q.Query) (models.Users, error) {
|
||||||
|
query = q.MustClone(query)
|
||||||
|
if query.Sorting == "" {
|
||||||
|
query.Sorting = "username"
|
||||||
|
}
|
||||||
|
|
||||||
|
excludeAdmin := true
|
||||||
|
for key := range query.Keywords {
|
||||||
|
str := strings.ToLower(key)
|
||||||
|
if str == "user_id__in" {
|
||||||
|
excludeAdmin = false
|
||||||
|
break
|
||||||
|
} else if str == "user_id" {
|
||||||
|
excludeAdmin = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if excludeAdmin {
|
||||||
|
// Exclude admin account when not filter by UserIDs, see https://github.com/goharbor/harbor/issues/2527
|
||||||
|
query.Keywords["user_id__gt"] = 1
|
||||||
|
}
|
||||||
|
|
||||||
return m.dao.List(ctx, query)
|
return m.dao.List(ctx, query)
|
||||||
}
|
}
|
||||||
|
@ -59,7 +59,7 @@ func Middleware() func(http.Handler) http.Handler {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
proj, err := projectController.Get(ctx, art.ProjectID, project.CVEAllowlist(true))
|
proj, err := projectController.Get(ctx, art.ProjectID, project.WithEffectCVEAllowlist())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("get the project %d failed, error: %v", art.ProjectID, err)
|
logger.Errorf("get the project %d failed, error: %v", art.ProjectID, err)
|
||||||
return err
|
return err
|
||||||
|
@ -18,14 +18,15 @@ package handler
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"github.com/go-openapi/runtime"
|
"github.com/go-openapi/runtime"
|
||||||
"github.com/goharbor/harbor/src/lib"
|
"github.com/goharbor/harbor/src/lib"
|
||||||
"github.com/goharbor/harbor/src/lib/errors"
|
"github.com/goharbor/harbor/src/lib/errors"
|
||||||
lib_http "github.com/goharbor/harbor/src/lib/http"
|
lib_http "github.com/goharbor/harbor/src/lib/http"
|
||||||
"github.com/goharbor/harbor/src/lib/q"
|
"github.com/goharbor/harbor/src/lib/q"
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/go-openapi/runtime/middleware"
|
"github.com/go-openapi/runtime/middleware"
|
||||||
"github.com/goharbor/harbor/src/common/rbac"
|
"github.com/goharbor/harbor/src/common/rbac"
|
||||||
@ -115,6 +116,18 @@ func (b *BaseAPI) RequireSysAdmin(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RequireAuthenticated checks it's authenticated according to the security context
|
||||||
|
func (b *BaseAPI) RequireAuthenticated(ctx context.Context) error {
|
||||||
|
secCtx, ok := security.FromContext(ctx)
|
||||||
|
if !ok {
|
||||||
|
return errors.UnauthorizedError(errors.New("security context not found"))
|
||||||
|
}
|
||||||
|
if !secCtx.IsAuthenticated() {
|
||||||
|
return errors.UnauthorizedError(nil)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// BuildQuery builds the query model according to the query string
|
// BuildQuery builds the query model according to the query string
|
||||||
func (b *BaseAPI) BuildQuery(ctx context.Context, query *string, pageNumber, pageSize *int64) (*q.Query, error) {
|
func (b *BaseAPI) BuildQuery(ctx context.Context, query *string, pageNumber, pageSize *int64) (*q.Query, error) {
|
||||||
var (
|
var (
|
||||||
|
79
src/server/v2.0/handler/model/project.go
Normal file
79
src/server/v2.0/handler/model/project.go
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
// Copyright Project Harbor Authors
|
||||||
|
//
|
||||||
|
// 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 model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/go-openapi/strfmt"
|
||||||
|
"github.com/goharbor/harbor/src/controller/project"
|
||||||
|
"github.com/goharbor/harbor/src/lib"
|
||||||
|
"github.com/goharbor/harbor/src/lib/log"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/scan/vuln"
|
||||||
|
"github.com/goharbor/harbor/src/server/v2.0/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Project model
|
||||||
|
type Project struct {
|
||||||
|
*project.Project
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToSwagger converts the project to the swagger model
|
||||||
|
func (p *Project) ToSwagger() *models.Project {
|
||||||
|
var currentUserRoleIds []int32
|
||||||
|
for _, role := range p.RoleList {
|
||||||
|
currentUserRoleIds = append(currentUserRoleIds, int32(role))
|
||||||
|
}
|
||||||
|
|
||||||
|
var md *models.ProjectMetadata
|
||||||
|
if p.Metadata != nil {
|
||||||
|
var m models.ProjectMetadata
|
||||||
|
lib.JSONCopy(&m, p.Metadata)
|
||||||
|
|
||||||
|
// Transform the severity to severity of CVSS v3.0 Ratings
|
||||||
|
if m.Severity != nil {
|
||||||
|
severity := strings.ToLower(vuln.ParseSeverityVersion3(*m.Severity).String())
|
||||||
|
m.Severity = &severity
|
||||||
|
}
|
||||||
|
|
||||||
|
md = &m
|
||||||
|
}
|
||||||
|
|
||||||
|
var allowlist models.CVEAllowlist
|
||||||
|
if err := lib.JSONCopy(&allowlist, p.CVEAllowlist); err != nil {
|
||||||
|
log.Warningf("failed to copy CVEAllowlist form %T", p.CVEAllowlist)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &models.Project{
|
||||||
|
ChartCount: int64(p.ChartCount),
|
||||||
|
CreationTime: strfmt.DateTime(p.CreationTime),
|
||||||
|
CurrentUserRoleID: int64(p.Role),
|
||||||
|
CurrentUserRoleIds: currentUserRoleIds,
|
||||||
|
CVEAllowlist: &allowlist,
|
||||||
|
Metadata: md,
|
||||||
|
Name: p.Name,
|
||||||
|
OwnerID: int32(p.OwnerID),
|
||||||
|
OwnerName: p.OwnerName,
|
||||||
|
ProjectID: int32(p.ProjectID),
|
||||||
|
RegistryID: p.RegistryID,
|
||||||
|
RepoCount: p.RepoCount,
|
||||||
|
UpdateTime: strfmt.DateTime(p.UpdateTime),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewProject ...
|
||||||
|
func NewProject(p *project.Project) *Project {
|
||||||
|
return &Project{p}
|
||||||
|
}
|
@ -2,33 +2,216 @@ package handler
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/go-openapi/runtime/middleware"
|
"github.com/go-openapi/runtime/middleware"
|
||||||
"github.com/go-openapi/strfmt"
|
"github.com/go-openapi/strfmt"
|
||||||
|
"github.com/goharbor/harbor/src/common"
|
||||||
|
pro "github.com/goharbor/harbor/src/common/dao/project"
|
||||||
"github.com/goharbor/harbor/src/common/rbac"
|
"github.com/goharbor/harbor/src/common/rbac"
|
||||||
|
"github.com/goharbor/harbor/src/common/security"
|
||||||
|
"github.com/goharbor/harbor/src/common/security/local"
|
||||||
"github.com/goharbor/harbor/src/controller/project"
|
"github.com/goharbor/harbor/src/controller/project"
|
||||||
|
"github.com/goharbor/harbor/src/controller/quota"
|
||||||
|
"github.com/goharbor/harbor/src/controller/repository"
|
||||||
|
"github.com/goharbor/harbor/src/core/api"
|
||||||
|
"github.com/goharbor/harbor/src/core/config"
|
||||||
|
"github.com/goharbor/harbor/src/lib"
|
||||||
|
"github.com/goharbor/harbor/src/lib/errors"
|
||||||
|
"github.com/goharbor/harbor/src/lib/log"
|
||||||
|
"github.com/goharbor/harbor/src/lib/orm"
|
||||||
|
"github.com/goharbor/harbor/src/lib/q"
|
||||||
"github.com/goharbor/harbor/src/pkg/audit"
|
"github.com/goharbor/harbor/src/pkg/audit"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/project/metadata"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/quota/types"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/retention/policy"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/user"
|
||||||
|
"github.com/goharbor/harbor/src/replication"
|
||||||
|
"github.com/goharbor/harbor/src/server/v2.0/handler/model"
|
||||||
"github.com/goharbor/harbor/src/server/v2.0/models"
|
"github.com/goharbor/harbor/src/server/v2.0/models"
|
||||||
operation "github.com/goharbor/harbor/src/server/v2.0/restapi/operations/project"
|
operation "github.com/goharbor/harbor/src/server/v2.0/restapi/operations/project"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// for the proxy cache type project, we will create a 7 days retention policy for it by default
|
||||||
|
const defaultDaysToRetentionForProxyCacheProject = 7
|
||||||
|
|
||||||
func newProjectAPI() *projectAPI {
|
func newProjectAPI() *projectAPI {
|
||||||
return &projectAPI{
|
return &projectAPI{
|
||||||
auditMgr: audit.Mgr,
|
auditMgr: audit.Mgr,
|
||||||
proCtl: project.Ctl,
|
metadataMgr: metadata.Mgr,
|
||||||
|
userMgr: user.Mgr,
|
||||||
|
repositoryCtl: repository.Ctl,
|
||||||
|
projectCtl: project.Ctl,
|
||||||
|
quotaCtl: quota.Ctl,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type projectAPI struct {
|
type projectAPI struct {
|
||||||
BaseAPI
|
BaseAPI
|
||||||
auditMgr audit.Manager
|
auditMgr audit.Manager
|
||||||
proCtl project.Controller
|
metadataMgr metadata.Manager
|
||||||
|
userMgr user.Manager
|
||||||
|
repositoryCtl repository.Controller
|
||||||
|
projectCtl project.Controller
|
||||||
|
quotaCtl quota.Controller
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *projectAPI) CreateProject(ctx context.Context, params operation.CreateProjectParams) middleware.Responder {
|
||||||
|
if err := a.RequireAuthenticated(ctx); err != nil {
|
||||||
|
return a.SendError(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
onlyAdmin, err := config.OnlyAdminCreateProject()
|
||||||
|
if err != nil {
|
||||||
|
return a.SendError(ctx, fmt.Errorf("failed to determine whether only admin can create projects: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
secCtx, _ := security.FromContext(ctx)
|
||||||
|
if onlyAdmin && !(secCtx.IsSysAdmin() || secCtx.IsSolutionUser()) {
|
||||||
|
log.Errorf("Only sys admin can create project")
|
||||||
|
return a.SendError(ctx, errors.ForbiddenError(nil).WithMessage("Only system admin can create project"))
|
||||||
|
}
|
||||||
|
|
||||||
|
req := params.Project
|
||||||
|
|
||||||
|
if req.RegistryID != nil && !secCtx.IsSysAdmin() {
|
||||||
|
// only system admin can create the proxy cache project
|
||||||
|
return a.SendError(ctx, errors.ForbiddenError(nil).WithMessage("Only system admin can create proxy cache project"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// populate storage limit
|
||||||
|
if config.QuotaPerProjectEnable() {
|
||||||
|
// the security context is not sys admin, set the StorageLimit the global StoragePerProject
|
||||||
|
if req.StorageLimit == nil || *req.StorageLimit == 0 || !secCtx.IsSysAdmin() {
|
||||||
|
setting, err := config.QuotaSetting()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to get quota setting: %v", err)
|
||||||
|
return a.SendError(ctx, fmt.Errorf("failed to get quota setting: %v", err))
|
||||||
|
}
|
||||||
|
defaultStorageLimit := setting.StoragePerProject
|
||||||
|
req.StorageLimit = &defaultStorageLimit
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// ignore storage limit when quota per project disabled
|
||||||
|
req.StorageLimit = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Metadata == nil {
|
||||||
|
req.Metadata = &models.ProjectMetadata{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// accept the "public" property to make replication work well with old versions(<=1.2.0)
|
||||||
|
if req.Public != nil && req.Metadata.Public == "" {
|
||||||
|
req.Metadata.Public = strconv.FormatBool(*req.Public)
|
||||||
|
}
|
||||||
|
|
||||||
|
// populate public metadata as false if it isn't set
|
||||||
|
if req.Metadata.Public == "" {
|
||||||
|
req.Metadata.Public = strconv.FormatBool(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate the RegistryID and StorageLimit in the body of the request
|
||||||
|
if err := a.validateProjectReq(ctx, req); err != nil {
|
||||||
|
return a.SendError(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var ownerID int
|
||||||
|
// set the owner as the system admin when the API being called by replication
|
||||||
|
// it's a solution to workaround the restriction of project creation API:
|
||||||
|
// only normal users can create projects
|
||||||
|
if secCtx.IsSolutionUser() {
|
||||||
|
ownerID = 1
|
||||||
|
} else {
|
||||||
|
ownerName := secCtx.GetUsername()
|
||||||
|
user, err := a.userMgr.GetByName(ctx, ownerName)
|
||||||
|
if err != nil {
|
||||||
|
return a.SendError(ctx, err)
|
||||||
|
}
|
||||||
|
ownerID = user.UserID
|
||||||
|
}
|
||||||
|
|
||||||
|
p := &project.Project{
|
||||||
|
Name: req.ProjectName,
|
||||||
|
OwnerID: ownerID,
|
||||||
|
RegistryID: lib.Int64Value(req.RegistryID),
|
||||||
|
}
|
||||||
|
lib.JSONCopy(&p.Metadata, req.Metadata)
|
||||||
|
|
||||||
|
projectID, err := a.projectCtl.Create(ctx, p)
|
||||||
|
if err != nil {
|
||||||
|
return a.SendError(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StorageLimit is provided in the request body and it's valid,
|
||||||
|
// create the quota for the project
|
||||||
|
if req.StorageLimit != nil {
|
||||||
|
referenceID := quota.ReferenceID(projectID)
|
||||||
|
hardLimits := types.ResourceList{types.ResourceStorage: *req.StorageLimit}
|
||||||
|
if _, err := a.quotaCtl.Create(ctx, quota.ProjectReference, referenceID, hardLimits); err != nil {
|
||||||
|
return a.SendError(ctx, fmt.Errorf("failed to create quota for project: %v", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegistryID is provided in the request body and it's valid,
|
||||||
|
// create a default retention policy for proxy project
|
||||||
|
if req.RegistryID != nil {
|
||||||
|
plc := policy.WithNDaysSinceLastPull(projectID, defaultDaysToRetentionForProxyCacheProject)
|
||||||
|
// TODO: move the retention controller to `src/controller/retention` and
|
||||||
|
// change to use the default retention controller in `src/controller/retention`
|
||||||
|
retentionID, err := api.GetRetentionController().CreateRetention(plc)
|
||||||
|
if err != nil {
|
||||||
|
return a.SendError(ctx, err)
|
||||||
|
}
|
||||||
|
md := map[string]string{"retention_id": strconv.FormatInt(retentionID, 10)}
|
||||||
|
if err := a.metadataMgr.Add(ctx, projectID, md); err != nil {
|
||||||
|
return a.SendError(ctx, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
location := fmt.Sprintf("%s/%d", strings.TrimSuffix(params.HTTPRequest.URL.Path, "/"), projectID)
|
||||||
|
return operation.NewCreateProjectCreated().WithLocation(location)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *projectAPI) DeleteProject(ctx context.Context, params operation.DeleteProjectParams) middleware.Responder {
|
||||||
|
if err := a.RequireProjectAccess(ctx, params.ProjectID, rbac.ActionDelete); err != nil {
|
||||||
|
return a.SendError(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := a.deletable(ctx, params.ProjectID)
|
||||||
|
if err != nil {
|
||||||
|
return a.SendError(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !result.Deletable {
|
||||||
|
return a.SendError(ctx, errors.PreconditionFailedError(errors.New(result.Message)))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := a.projectCtl.Delete(ctx, params.ProjectID); err != nil {
|
||||||
|
return a.SendError(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
referenceID := quota.ReferenceID(params.ProjectID)
|
||||||
|
q, err := a.quotaCtl.GetByRef(ctx, quota.ProjectReference, referenceID)
|
||||||
|
if err != nil {
|
||||||
|
log.Warningf("failed to get quota for project %d, error: %v", params.ProjectID, err)
|
||||||
|
} else {
|
||||||
|
if err := a.quotaCtl.Delete(ctx, q.ID); err != nil {
|
||||||
|
return a.SendError(ctx, fmt.Errorf("failed to delete quota for project: %v", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return operation.NewDeleteProjectOK()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *projectAPI) GetLogs(ctx context.Context, params operation.GetLogsParams) middleware.Responder {
|
func (a *projectAPI) GetLogs(ctx context.Context, params operation.GetLogsParams) middleware.Responder {
|
||||||
if err := a.RequireProjectAccess(ctx, params.ProjectName, rbac.ActionList, rbac.ResourceLog); err != nil {
|
if err := a.RequireProjectAccess(ctx, params.ProjectName, rbac.ActionList, rbac.ResourceLog); err != nil {
|
||||||
return a.SendError(ctx, err)
|
return a.SendError(ctx, err)
|
||||||
}
|
}
|
||||||
pro, err := a.proCtl.GetByName(ctx, params.ProjectName)
|
pro, err := a.projectCtl.GetByName(ctx, params.ProjectName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return a.SendError(ctx, err)
|
return a.SendError(ctx, err)
|
||||||
}
|
}
|
||||||
@ -63,3 +246,393 @@ func (a *projectAPI) GetLogs(ctx context.Context, params operation.GetLogsParams
|
|||||||
WithLink(a.Links(ctx, params.HTTPRequest.URL, total, query.PageNumber, query.PageSize).String()).
|
WithLink(a.Links(ctx, params.HTTPRequest.URL, total, query.PageNumber, query.PageSize).String()).
|
||||||
WithPayload(auditLogs)
|
WithPayload(auditLogs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *projectAPI) GetProject(ctx context.Context, params operation.GetProjectParams) middleware.Responder {
|
||||||
|
if err := a.RequireProjectAccess(ctx, params.ProjectID, rbac.ActionRead); err != nil {
|
||||||
|
return a.SendError(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
p, err := a.getProject(ctx, params.ProjectID, project.WithCVEAllowlist(), project.WithOwner())
|
||||||
|
if err != nil {
|
||||||
|
return a.SendError(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return operation.NewGetProjectOK().WithPayload(model.NewProject(p).ToSwagger())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *projectAPI) GetProjectDeletable(ctx context.Context, params operation.GetProjectDeletableParams) middleware.Responder {
|
||||||
|
if err := a.RequireProjectAccess(ctx, params.ProjectID, rbac.ActionDelete); err != nil {
|
||||||
|
return a.SendError(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := a.deletable(ctx, params.ProjectID)
|
||||||
|
if err != nil {
|
||||||
|
return a.SendError(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return operation.NewGetProjectDeletableOK().WithPayload(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *projectAPI) GetProjectSummary(ctx context.Context, params operation.GetProjectSummaryParams) middleware.Responder {
|
||||||
|
if err := a.RequireProjectAccess(ctx, params.ProjectID, rbac.ActionRead); err != nil {
|
||||||
|
return a.SendError(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
p, err := a.getProject(ctx, params.ProjectID)
|
||||||
|
if err != nil {
|
||||||
|
return a.SendError(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
summary := &models.ProjectSummary{
|
||||||
|
ChartCount: int64(p.ChartCount),
|
||||||
|
RepoCount: p.RepoCount,
|
||||||
|
}
|
||||||
|
|
||||||
|
var fetchSummaries []func(context.Context, *project.Project, *models.ProjectSummary)
|
||||||
|
|
||||||
|
if hasPerm := a.HasProjectPermission(ctx, p.ProjectID, rbac.ActionRead, rbac.ResourceQuota); hasPerm {
|
||||||
|
fetchSummaries = append(fetchSummaries, getProjectQuotaSummary)
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasPerm := a.HasProjectPermission(ctx, p.ProjectID, rbac.ActionList, rbac.ResourceMember); hasPerm {
|
||||||
|
fetchSummaries = append(fetchSummaries, getProjectMemberSummary)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.RegistryID > 0 {
|
||||||
|
fetchSummaries = append(fetchSummaries, getProjectRegistrySummary)
|
||||||
|
}
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
for _, fn := range fetchSummaries {
|
||||||
|
fn := fn
|
||||||
|
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
fn(ctx, p, summary)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
return operation.NewGetProjectSummaryOK().WithPayload(summary)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *projectAPI) HeadProject(ctx context.Context, params operation.HeadProjectParams) middleware.Responder {
|
||||||
|
if err := a.RequireAuthenticated(ctx); err != nil {
|
||||||
|
return a.SendError(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := a.projectCtl.GetByName(ctx, params.ProjectName); err != nil {
|
||||||
|
return a.SendError(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return operation.NewHeadProjectOK()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *projectAPI) ListProjects(ctx context.Context, params operation.ListProjectsParams) middleware.Responder {
|
||||||
|
query := q.New(q.KeyWords{})
|
||||||
|
query.Sorting = "name"
|
||||||
|
query.PageNumber = *params.Page
|
||||||
|
query.PageSize = *params.PageSize
|
||||||
|
|
||||||
|
if name := lib.StringValue(params.Name); name != "" {
|
||||||
|
query.Keywords["name"] = &q.FuzzyMatchValue{Value: name}
|
||||||
|
}
|
||||||
|
if owner := lib.StringValue(params.Owner); owner != "" {
|
||||||
|
query.Keywords["owner"] = owner
|
||||||
|
}
|
||||||
|
if params.Public != nil {
|
||||||
|
query.Keywords["public"] = lib.BoolValue(params.Public)
|
||||||
|
}
|
||||||
|
|
||||||
|
secCtx, ok := security.FromContext(ctx)
|
||||||
|
if ok && secCtx.IsAuthenticated() {
|
||||||
|
if !secCtx.IsSysAdmin() && !secCtx.IsSolutionUser() {
|
||||||
|
// authenticated but not system admin or solution user,
|
||||||
|
// return public projects and projects that the user is member of
|
||||||
|
if l, ok := secCtx.(*local.SecurityContext); ok {
|
||||||
|
currentUser := l.User()
|
||||||
|
member := &project.MemberQuery{
|
||||||
|
Name: currentUser.Username,
|
||||||
|
GroupIDs: currentUser.GroupIDs,
|
||||||
|
}
|
||||||
|
|
||||||
|
// not filter by public or filter by the public with true,
|
||||||
|
// so also return public projects for the member
|
||||||
|
if public, ok := query.Keywords["public"]; !ok || lib.ToBool(public) {
|
||||||
|
member.WithPublic = true
|
||||||
|
}
|
||||||
|
|
||||||
|
query.Keywords["member"] = member
|
||||||
|
} else {
|
||||||
|
// can't get the user info, force to return public projects
|
||||||
|
query.Keywords["public"] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if params.Public != nil && !*params.Public {
|
||||||
|
// anonymous want to query private projects return empty projects directly
|
||||||
|
return operation.NewListProjectsOK().WithXTotalCount(0).WithPayload([]*models.Project{})
|
||||||
|
}
|
||||||
|
// force to return public projects for anonymous
|
||||||
|
query.Keywords["public"] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
total, err := a.projectCtl.Count(ctx, query)
|
||||||
|
if err != nil {
|
||||||
|
return a.SendError(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if total == 0 {
|
||||||
|
// no projects found for the query return directly
|
||||||
|
return operation.NewListProjectsOK().WithXTotalCount(0).WithPayload([]*models.Project{})
|
||||||
|
}
|
||||||
|
|
||||||
|
projects, err := a.projectCtl.List(ctx, query, project.WithCVEAllowlist(), project.WithOwner())
|
||||||
|
if err != nil {
|
||||||
|
return a.SendError(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
for _, p := range projects {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(p *project.Project) {
|
||||||
|
defer wg.Done()
|
||||||
|
// due to the issue https://github.com/lib/pq/issues/81 of lib/pg or postgres,
|
||||||
|
// simultaneous queries in transaction may failed, so clone a ctx with new ormer here
|
||||||
|
if err := a.populateProperties(orm.Clone(ctx), p); err != nil {
|
||||||
|
log.G(ctx).Errorf("failed to populate propertites for project %s, error: %v", p.Name, err)
|
||||||
|
}
|
||||||
|
}(p)
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
var payload []*models.Project
|
||||||
|
for _, p := range projects {
|
||||||
|
payload = append(payload, model.NewProject(p).ToSwagger())
|
||||||
|
}
|
||||||
|
|
||||||
|
return operation.NewListProjectsOK().
|
||||||
|
WithXTotalCount(total).
|
||||||
|
WithLink(a.Links(ctx, params.HTTPRequest.URL, total, query.PageNumber, query.PageSize).String()).
|
||||||
|
WithPayload(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *projectAPI) UpdateProject(ctx context.Context, params operation.UpdateProjectParams) middleware.Responder {
|
||||||
|
if err := a.RequireProjectAccess(ctx, params.ProjectID, rbac.ActionUpdate); err != nil {
|
||||||
|
return a.SendError(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
p, err := a.projectCtl.Get(ctx, params.ProjectID, project.Metadata(false))
|
||||||
|
if err != nil {
|
||||||
|
return a.SendError(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if params.Project.CVEAllowlist != nil {
|
||||||
|
if params.Project.CVEAllowlist.ProjectID == 0 {
|
||||||
|
// project_id in cve_allowlist not provided or provided as 0, let it to be the id of the project which will be updating
|
||||||
|
params.Project.CVEAllowlist.ProjectID = params.ProjectID
|
||||||
|
} else if params.Project.CVEAllowlist.ProjectID != params.ProjectID {
|
||||||
|
return a.SendError(ctx, errors.BadRequestError(nil).
|
||||||
|
WithMessage("project_id in cve_allowlist must be %d but it's %d", params.ProjectID, params.Project.CVEAllowlist.ProjectID))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := lib.JSONCopy(&p.CVEAllowlist, params.Project.CVEAllowlist); err != nil {
|
||||||
|
return a.SendError(ctx, errors.UnknownError(nil).WithMessage("failed to process cve_allowlist, error: %v", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lib.JSONCopy(&p.Metadata, params.Project.Metadata)
|
||||||
|
|
||||||
|
if err := a.projectCtl.Update(ctx, p); err != nil {
|
||||||
|
return a.SendError(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return operation.NewUpdateProjectOK()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *projectAPI) deletable(ctx context.Context, projectID int64) (*models.ProjectDeletable, error) {
|
||||||
|
proj, err := a.getProject(ctx, projectID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result := &models.ProjectDeletable{Deletable: true}
|
||||||
|
if proj.RepoCount > 0 {
|
||||||
|
result.Deletable = false
|
||||||
|
result.Message = "the project contains repositories, can not be deleted"
|
||||||
|
} else if proj.ChartCount > 0 {
|
||||||
|
result.Deletable = false
|
||||||
|
result.Message = "the project contains helm charts, can not be deleted"
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *projectAPI) getProject(ctx context.Context, projectID int64, options ...project.Option) (*project.Project, error) {
|
||||||
|
p, err := a.projectCtl.Get(ctx, projectID, options...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := a.populateProperties(ctx, p); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *projectAPI) validateProjectReq(ctx context.Context, req *models.ProjectReq) error {
|
||||||
|
if req.RegistryID != nil {
|
||||||
|
if *req.RegistryID <= 0 {
|
||||||
|
return errors.BadRequestError(fmt.Errorf("%d is invalid value of registry_id, it should be geater than 0", *req.RegistryID))
|
||||||
|
}
|
||||||
|
|
||||||
|
registry, err := replication.RegistryMgr.Get(*req.RegistryID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get the registry %d: %v", *req.RegistryID, err)
|
||||||
|
}
|
||||||
|
if registry == nil {
|
||||||
|
return errors.NotFoundError(fmt.Errorf("registry %d not found", *req.RegistryID))
|
||||||
|
}
|
||||||
|
permitted := false
|
||||||
|
for _, t := range config.GetPermittedRegistryTypesForProxyCache() {
|
||||||
|
if string(registry.Type) == t {
|
||||||
|
permitted = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !permitted {
|
||||||
|
return errors.BadRequestError(fmt.Errorf("unsupported registry type %s", string(registry.Type)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.StorageLimit != nil {
|
||||||
|
hardLimits := types.ResourceList{types.ResourceStorage: *req.StorageLimit}
|
||||||
|
if err := quota.Validate(ctx, quota.ProjectReference, hardLimits); err != nil {
|
||||||
|
return errors.BadRequestError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *projectAPI) populateProperties(ctx context.Context, p *project.Project) error {
|
||||||
|
if secCtx, ok := security.FromContext(ctx); ok {
|
||||||
|
if sc, ok := secCtx.(*local.SecurityContext); ok {
|
||||||
|
roles, err := pro.ListRoles(sc.User(), p.ProjectID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p.RoleList = roles
|
||||||
|
p.Role = highestRole(roles)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
total, err := a.repositoryCtl.Count(ctx, q.New(q.KeyWords{"project_id": p.ProjectID}))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p.RepoCount = total
|
||||||
|
|
||||||
|
// Populate chart count property
|
||||||
|
if config.WithChartMuseum() {
|
||||||
|
count, err := api.GetChartController().GetCountOfCharts([]string{p.Name})
|
||||||
|
if err != nil {
|
||||||
|
err = errors.Wrap(err, fmt.Sprintf("get chart count of project %d failed", p.ProjectID))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
p.ChartCount = count
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getProjectQuotaSummary(ctx context.Context, p *project.Project, summary *models.ProjectSummary) {
|
||||||
|
if !config.QuotaPerProjectEnable() {
|
||||||
|
log.Debug("Quota per project disabled")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
q, err := quota.Ctl.GetByRef(ctx, quota.ProjectReference, quota.ReferenceID(p.ProjectID))
|
||||||
|
if err != nil {
|
||||||
|
log.Warningf("failed to get quota for project: %d", p.ProjectID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
summary.Quota = &models.ProjectSummaryQuota{}
|
||||||
|
if hard, err := q.GetHard(); err == nil {
|
||||||
|
lib.JSONCopy(&summary.Quota.Hard, hard)
|
||||||
|
}
|
||||||
|
if used, err := q.GetUsed(); err == nil {
|
||||||
|
lib.JSONCopy(&summary.Quota.Used, used)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getProjectMemberSummary(ctx context.Context, p *project.Project, summary *models.ProjectSummary) {
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
for _, e := range []struct {
|
||||||
|
role int
|
||||||
|
count *int64
|
||||||
|
}{
|
||||||
|
{common.RoleProjectAdmin, &summary.ProjectAdminCount},
|
||||||
|
{common.RoleMaintainer, &summary.MaintainerCount},
|
||||||
|
{common.RoleDeveloper, &summary.DeveloperCount},
|
||||||
|
{common.RoleGuest, &summary.GuestCount},
|
||||||
|
{common.RoleLimitedGuest, &summary.LimitedGuestCount},
|
||||||
|
} {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(role int, count *int64) {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
total, err := pro.GetTotalOfProjectMembers(p.ProjectID, role)
|
||||||
|
if err != nil {
|
||||||
|
log.Warningf("failed to get total of project members of role %d", role)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
*count = total
|
||||||
|
}(e.role, e.count)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func getProjectRegistrySummary(ctx context.Context, p *project.Project, summary *models.ProjectSummary) {
|
||||||
|
if p.RegistryID <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
registry, err := replication.RegistryMgr.Get(p.RegistryID)
|
||||||
|
if err != nil {
|
||||||
|
log.Warningf("failed to get registry %d: %v", p.RegistryID, err)
|
||||||
|
} else if registry != nil {
|
||||||
|
registry.Credential = nil
|
||||||
|
lib.JSONCopy(&summary.Registry, registry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the highest role in the role list.
|
||||||
|
// This func should be removed once we deprecate the "current_user_role_id" in project API
|
||||||
|
// A user can have multiple roles and they may not have a strict ranking relationship
|
||||||
|
func highestRole(roles []int) int {
|
||||||
|
if roles == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
rolePower := map[int]int{
|
||||||
|
common.RoleProjectAdmin: 50,
|
||||||
|
common.RoleMaintainer: 40,
|
||||||
|
common.RoleDeveloper: 30,
|
||||||
|
common.RoleGuest: 20,
|
||||||
|
common.RoleLimitedGuest: 10,
|
||||||
|
}
|
||||||
|
var highest, highestPower int
|
||||||
|
for _, role := range roles {
|
||||||
|
if p, ok := rolePower[role]; ok && p > highestPower {
|
||||||
|
highest = role
|
||||||
|
highestPower = p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return highest
|
||||||
|
}
|
||||||
|
@ -24,8 +24,6 @@ import (
|
|||||||
func registerLegacyRoutes() {
|
func registerLegacyRoutes() {
|
||||||
version := APIVersion
|
version := APIVersion
|
||||||
beego.Router("/api/"+version+"/projects/:pid([0-9]+)/members/?:pmid([0-9]+)", &api.ProjectMemberAPI{})
|
beego.Router("/api/"+version+"/projects/:pid([0-9]+)/members/?:pmid([0-9]+)", &api.ProjectMemberAPI{})
|
||||||
beego.Router("/api/"+version+"/projects/", &api.ProjectAPI{}, "head:Head")
|
|
||||||
beego.Router("/api/"+version+"/projects/:id([0-9]+)", &api.ProjectAPI{})
|
|
||||||
beego.Router("/api/"+version+"/users/:id", &api.UserAPI{}, "get:Get;delete:Delete;put:Put")
|
beego.Router("/api/"+version+"/users/:id", &api.UserAPI{}, "get:Get;delete:Delete;put:Put")
|
||||||
beego.Router("/api/"+version+"/users", &api.UserAPI{}, "get:List;post:Post")
|
beego.Router("/api/"+version+"/users", &api.UserAPI{}, "get:List;post:Post")
|
||||||
beego.Router("/api/"+version+"/users/search", &api.UserAPI{}, "get:Search")
|
beego.Router("/api/"+version+"/users/search", &api.UserAPI{}, "get:Search")
|
||||||
@ -42,9 +40,6 @@ func registerLegacyRoutes() {
|
|||||||
beego.Router("/api/"+version+"/health", &api.HealthAPI{}, "get:CheckHealth")
|
beego.Router("/api/"+version+"/health", &api.HealthAPI{}, "get:CheckHealth")
|
||||||
beego.Router("/api/"+version+"/ping", &api.SystemInfoAPI{}, "get:Ping")
|
beego.Router("/api/"+version+"/ping", &api.SystemInfoAPI{}, "get:Ping")
|
||||||
beego.Router("/api/"+version+"/search", &api.SearchAPI{})
|
beego.Router("/api/"+version+"/search", &api.SearchAPI{})
|
||||||
beego.Router("/api/"+version+"/projects/", &api.ProjectAPI{}, "get:List;post:Post")
|
|
||||||
beego.Router("/api/"+version+"/projects/:id([0-9]+)/summary", &api.ProjectAPI{}, "get:Summary")
|
|
||||||
beego.Router("/api/"+version+"/projects/:id([0-9]+)/_deletable", &api.ProjectAPI{}, "get:Deletable")
|
|
||||||
beego.Router("/api/"+version+"/projects/:id([0-9]+)/metadatas/?:name", &api.MetadataAPI{}, "get:Get")
|
beego.Router("/api/"+version+"/projects/:id([0-9]+)/metadatas/?:name", &api.MetadataAPI{}, "get:Get")
|
||||||
beego.Router("/api/"+version+"/projects/:id([0-9]+)/metadatas/", &api.MetadataAPI{}, "post:Post")
|
beego.Router("/api/"+version+"/projects/:id([0-9]+)/metadatas/", &api.MetadataAPI{}, "post:Post")
|
||||||
beego.Router("/api/"+version+"/projects/:pid([0-9]+)/robots", &api.RobotAPI{}, "post:Post;get:List")
|
beego.Router("/api/"+version+"/projects/:pid([0-9]+)/robots", &api.RobotAPI{}, "post:Post;get:List")
|
||||||
|
@ -135,7 +135,7 @@ func (_m *Controller) GetByName(ctx context.Context, projectName string, options
|
|||||||
}
|
}
|
||||||
|
|
||||||
// List provides a mock function with given fields: ctx, query, options
|
// List provides a mock function with given fields: ctx, query, options
|
||||||
func (_m *Controller) List(ctx context.Context, query *models.ProjectQueryParam, options ...project.Option) ([]*models.Project, error) {
|
func (_m *Controller) List(ctx context.Context, query *q.Query, options ...project.Option) ([]*models.Project, error) {
|
||||||
_va := make([]interface{}, len(options))
|
_va := make([]interface{}, len(options))
|
||||||
for _i := range options {
|
for _i := range options {
|
||||||
_va[_i] = options[_i]
|
_va[_i] = options[_i]
|
||||||
@ -146,7 +146,7 @@ func (_m *Controller) List(ctx context.Context, query *models.ProjectQueryParam,
|
|||||||
ret := _m.Called(_ca...)
|
ret := _m.Called(_ca...)
|
||||||
|
|
||||||
var r0 []*models.Project
|
var r0 []*models.Project
|
||||||
if rf, ok := ret.Get(0).(func(context.Context, *models.ProjectQueryParam, ...project.Option) []*models.Project); ok {
|
if rf, ok := ret.Get(0).(func(context.Context, *q.Query, ...project.Option) []*models.Project); ok {
|
||||||
r0 = rf(ctx, query, options...)
|
r0 = rf(ctx, query, options...)
|
||||||
} else {
|
} else {
|
||||||
if ret.Get(0) != nil {
|
if ret.Get(0) != nil {
|
||||||
@ -155,7 +155,7 @@ func (_m *Controller) List(ctx context.Context, query *models.ProjectQueryParam,
|
|||||||
}
|
}
|
||||||
|
|
||||||
var r1 error
|
var r1 error
|
||||||
if rf, ok := ret.Get(1).(func(context.Context, *models.ProjectQueryParam, ...project.Option) error); ok {
|
if rf, ok := ret.Get(1).(func(context.Context, *q.Query, ...project.Option) error); ok {
|
||||||
r1 = rf(ctx, query, options...)
|
r1 = rf(ctx, query, options...)
|
||||||
} else {
|
} else {
|
||||||
r1 = ret.Error(1)
|
r1 = ret.Error(1)
|
||||||
@ -163,3 +163,17 @@ func (_m *Controller) List(ctx context.Context, query *models.ProjectQueryParam,
|
|||||||
|
|
||||||
return r0, r1
|
return r0, r1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update provides a mock function with given fields: ctx, _a1
|
||||||
|
func (_m *Controller) Update(ctx context.Context, _a1 *models.Project) error {
|
||||||
|
ret := _m.Called(ctx, _a1)
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, *models.Project) error); ok {
|
||||||
|
r0 = rf(ctx, _a1)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
@ -96,19 +96,12 @@ func (_m *Manager) Get(ctx context.Context, idOrName interface{}) (*models.Proje
|
|||||||
}
|
}
|
||||||
|
|
||||||
// List provides a mock function with given fields: ctx, query
|
// List provides a mock function with given fields: ctx, query
|
||||||
func (_m *Manager) List(ctx context.Context, query ...*models.ProjectQueryParam) ([]*models.Project, error) {
|
func (_m *Manager) List(ctx context.Context, query *q.Query) ([]*models.Project, error) {
|
||||||
_va := make([]interface{}, len(query))
|
ret := _m.Called(ctx, query)
|
||||||
for _i := range query {
|
|
||||||
_va[_i] = query[_i]
|
|
||||||
}
|
|
||||||
var _ca []interface{}
|
|
||||||
_ca = append(_ca, ctx)
|
|
||||||
_ca = append(_ca, _va...)
|
|
||||||
ret := _m.Called(_ca...)
|
|
||||||
|
|
||||||
var r0 []*models.Project
|
var r0 []*models.Project
|
||||||
if rf, ok := ret.Get(0).(func(context.Context, ...*models.ProjectQueryParam) []*models.Project); ok {
|
if rf, ok := ret.Get(0).(func(context.Context, *q.Query) []*models.Project); ok {
|
||||||
r0 = rf(ctx, query...)
|
r0 = rf(ctx, query)
|
||||||
} else {
|
} else {
|
||||||
if ret.Get(0) != nil {
|
if ret.Get(0) != nil {
|
||||||
r0 = ret.Get(0).([]*models.Project)
|
r0 = ret.Get(0).([]*models.Project)
|
||||||
@ -116,8 +109,8 @@ func (_m *Manager) List(ctx context.Context, query ...*models.ProjectQueryParam)
|
|||||||
}
|
}
|
||||||
|
|
||||||
var r1 error
|
var r1 error
|
||||||
if rf, ok := ret.Get(1).(func(context.Context, ...*models.ProjectQueryParam) error); ok {
|
if rf, ok := ret.Get(1).(func(context.Context, *q.Query) error); ok {
|
||||||
r1 = rf(ctx, query...)
|
r1 = rf(ctx, query)
|
||||||
} else {
|
} else {
|
||||||
r1 = ret.Error(1)
|
r1 = ret.Error(1)
|
||||||
}
|
}
|
||||||
|
@ -5,10 +5,12 @@ package user
|
|||||||
import (
|
import (
|
||||||
context "context"
|
context "context"
|
||||||
|
|
||||||
models "github.com/goharbor/harbor/src/pkg/user/models"
|
models "github.com/goharbor/harbor/src/common/models"
|
||||||
mock "github.com/stretchr/testify/mock"
|
mock "github.com/stretchr/testify/mock"
|
||||||
|
|
||||||
q "github.com/goharbor/harbor/src/lib/q"
|
q "github.com/goharbor/harbor/src/lib/q"
|
||||||
|
|
||||||
|
usermodels "github.com/goharbor/harbor/src/pkg/user/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Manager is an autogenerated mock type for the Manager type
|
// Manager is an autogenerated mock type for the Manager type
|
||||||
@ -16,16 +18,62 @@ type Manager struct {
|
|||||||
mock.Mock
|
mock.Mock
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get provides a mock function with given fields: ctx, id
|
||||||
|
func (_m *Manager) Get(ctx context.Context, id int) (*models.User, error) {
|
||||||
|
ret := _m.Called(ctx, id)
|
||||||
|
|
||||||
|
var r0 *models.User
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, int) *models.User); ok {
|
||||||
|
r0 = rf(ctx, id)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(*models.User)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(context.Context, int) error); ok {
|
||||||
|
r1 = rf(ctx, id)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByName provides a mock function with given fields: ctx, username
|
||||||
|
func (_m *Manager) GetByName(ctx context.Context, username string) (*models.User, error) {
|
||||||
|
ret := _m.Called(ctx, username)
|
||||||
|
|
||||||
|
var r0 *models.User
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, string) *models.User); ok {
|
||||||
|
r0 = rf(ctx, username)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(*models.User)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
|
||||||
|
r1 = rf(ctx, username)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
// List provides a mock function with given fields: ctx, query
|
// List provides a mock function with given fields: ctx, query
|
||||||
func (_m *Manager) List(ctx context.Context, query *q.Query) (models.Users, error) {
|
func (_m *Manager) List(ctx context.Context, query *q.Query) (usermodels.Users, error) {
|
||||||
ret := _m.Called(ctx, query)
|
ret := _m.Called(ctx, query)
|
||||||
|
|
||||||
var r0 models.Users
|
var r0 usermodels.Users
|
||||||
if rf, ok := ret.Get(0).(func(context.Context, *q.Query) models.Users); ok {
|
if rf, ok := ret.Get(0).(func(context.Context, *q.Query) usermodels.Users); ok {
|
||||||
r0 = rf(ctx, query)
|
r0 = rf(ctx, query)
|
||||||
} else {
|
} else {
|
||||||
if ret.Get(0) != nil {
|
if ret.Get(0) != nil {
|
||||||
r0 = ret.Get(0).(models.Users)
|
r0 = ret.Get(0).(usermodels.Users)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
import os
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
import subprocess
|
import subprocess
|
||||||
@ -22,6 +22,10 @@ class Credential:
|
|||||||
self.username = username
|
self.username = username
|
||||||
self.password = password
|
self.password = password
|
||||||
|
|
||||||
|
def get_endpoint():
|
||||||
|
harbor_server = os.environ.get("HARBOR_HOST", "localhost:8080")
|
||||||
|
return os.environ.get("HARBOR_HOST_SCHEMA", "https")+ "://"+harbor_server+"/api/v2.0"
|
||||||
|
|
||||||
def _create_client(server, credential, debug, api_type="products"):
|
def _create_client(server, credential, debug, api_type="products"):
|
||||||
cfg = None
|
cfg = None
|
||||||
if api_type in ('projectv2', 'artifact', 'repository', 'scan'):
|
if api_type in ('projectv2', 'artifact', 'repository', 'scan'):
|
||||||
@ -85,13 +89,16 @@ def run_command(command):
|
|||||||
raise Exception('Error: Exited with error code: %s. Output:%s'% (e.returncode, e.output))
|
raise Exception('Error: Exited with error code: %s. Output:%s'% (e.returncode, e.output))
|
||||||
return output
|
return output
|
||||||
|
|
||||||
class Base:
|
class Base(object):
|
||||||
def __init__(self,
|
def __init__(self, server=None, credential=None, debug=True, api_type="products"):
|
||||||
server = Server(endpoint="http://localhost:8080/api", verify_ssl=False),
|
if server is None:
|
||||||
credential = Credential(type="basic_auth", username="admin", password="Harbor12345"),
|
server = Server(endpoint=get_endpoint(), verify_ssl=False)
|
||||||
debug = True, api_type = "products"):
|
|
||||||
if not isinstance(server.verify_ssl, bool):
|
if not isinstance(server.verify_ssl, bool):
|
||||||
server.verify_ssl = server.verify_ssl == "True"
|
server.verify_ssl = server.verify_ssl == "True"
|
||||||
|
|
||||||
|
if credential is None:
|
||||||
|
credential = Credential(type="basic_auth", username="admin", password="Harbor12345")
|
||||||
|
|
||||||
self.server = server
|
self.server = server
|
||||||
self.credential = credential
|
self.credential = credential
|
||||||
self.debug = debug
|
self.debug = debug
|
||||||
@ -102,8 +109,6 @@ class Base:
|
|||||||
if len(kwargs) == 0:
|
if len(kwargs) == 0:
|
||||||
return self.client
|
return self.client
|
||||||
server = self.server
|
server = self.server
|
||||||
if "api_type" in kwargs:
|
|
||||||
server.api_type = kwargs.get("api_type")
|
|
||||||
if "endpoint" in kwargs:
|
if "endpoint" in kwargs:
|
||||||
server.endpoint = kwargs.get("endpoint")
|
server.endpoint = kwargs.get("endpoint")
|
||||||
if "verify_ssl" in kwargs:
|
if "verify_ssl" in kwargs:
|
||||||
@ -118,4 +123,4 @@ class Base:
|
|||||||
credential.username = kwargs.get("username")
|
credential.username = kwargs.get("username")
|
||||||
if "password" in kwargs:
|
if "password" in kwargs:
|
||||||
credential.password = kwargs.get("password")
|
credential.password = kwargs.get("password")
|
||||||
return _create_client(server, credential, self.debug, self.api_type)
|
return _create_client(server, credential, self.debug, kwargs.get('api_type', self.api_type))
|
||||||
|
@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
import base
|
import base
|
||||||
import swagger_client
|
import swagger_client
|
||||||
from swagger_client.rest import ApiException
|
import v2_swagger_client
|
||||||
|
from v2_swagger_client.rest import ApiException
|
||||||
|
|
||||||
def is_member_exist_in_project(members, member_user_name, expected_member_role_id = None):
|
def is_member_exist_in_project(members, member_user_name, expected_member_role_id = None):
|
||||||
result = False
|
result = False
|
||||||
@ -22,6 +23,12 @@ def get_member_id_by_name(members, member_user_name):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
class Project(base.Base):
|
class Project(base.Base):
|
||||||
|
def __init__(self, username=None, password=None):
|
||||||
|
kwargs = dict(api_type="projectv2")
|
||||||
|
if username and password:
|
||||||
|
kwargs["credential"] = base.Credential('basic_auth', username, password)
|
||||||
|
super(Project, self).__init__(**kwargs)
|
||||||
|
|
||||||
def create_project(self, name=None, metadata=None, expect_status_code = 201, expect_response_body = None, **kwargs):
|
def create_project(self, name=None, metadata=None, expect_status_code = 201, expect_response_body = None, **kwargs):
|
||||||
if name is None:
|
if name is None:
|
||||||
name = base._random_name("project")
|
name = base._random_name("project")
|
||||||
@ -30,7 +37,7 @@ class Project(base.Base):
|
|||||||
client = self._get_client(**kwargs)
|
client = self._get_client(**kwargs)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
_, status_code, header = client.projects_post_with_http_info(swagger_client.ProjectReq(name, metadata))
|
_, status_code, header = client.create_project_with_http_info(v2_swagger_client.ProjectReq(project_name=name, metadata=metadata))
|
||||||
except ApiException as e:
|
except ApiException as e:
|
||||||
base._assert_status_code(expect_status_code, e.status)
|
base._assert_status_code(expect_status_code, e.status)
|
||||||
if expect_response_body is not None:
|
if expect_response_body is not None:
|
||||||
@ -43,7 +50,7 @@ class Project(base.Base):
|
|||||||
def get_projects(self, params, **kwargs):
|
def get_projects(self, params, **kwargs):
|
||||||
client = self._get_client(**kwargs)
|
client = self._get_client(**kwargs)
|
||||||
data = []
|
data = []
|
||||||
data, status_code, _ = client.projects_get_with_http_info(**params)
|
data, status_code, _ = client.list_projects_with_http_info(**params)
|
||||||
base._assert_status_code(200, status_code)
|
base._assert_status_code(200, status_code)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@ -66,7 +73,7 @@ class Project(base.Base):
|
|||||||
def check_project_name_exist(self, name=None, **kwargs):
|
def check_project_name_exist(self, name=None, **kwargs):
|
||||||
client = self._get_client(**kwargs)
|
client = self._get_client(**kwargs)
|
||||||
try:
|
try:
|
||||||
_, status_code, _ = client.projects_head_with_http_info(name)
|
_, status_code, _ = client.head_project_with_http_info(name)
|
||||||
except ApiException as e:
|
except ApiException as e:
|
||||||
status_code = -1
|
status_code = -1
|
||||||
return {
|
return {
|
||||||
@ -77,7 +84,7 @@ class Project(base.Base):
|
|||||||
def get_project(self, project_id, expect_status_code = 200, expect_response_body = None, **kwargs):
|
def get_project(self, project_id, expect_status_code = 200, expect_response_body = None, **kwargs):
|
||||||
client = self._get_client(**kwargs)
|
client = self._get_client(**kwargs)
|
||||||
try:
|
try:
|
||||||
data, status_code, _ = client.projects_project_id_get_with_http_info(project_id)
|
data, status_code, _ = client.get_project_with_http_info(project_id)
|
||||||
except ApiException as e:
|
except ApiException as e:
|
||||||
base._assert_status_code(expect_status_code, e.status)
|
base._assert_status_code(expect_status_code, e.status)
|
||||||
if expect_response_body is not None:
|
if expect_response_body is not None:
|
||||||
@ -90,9 +97,9 @@ class Project(base.Base):
|
|||||||
|
|
||||||
def update_project(self, project_id, expect_status_code=200, metadata=None, cve_allowlist=None, **kwargs):
|
def update_project(self, project_id, expect_status_code=200, metadata=None, cve_allowlist=None, **kwargs):
|
||||||
client = self._get_client(**kwargs)
|
client = self._get_client(**kwargs)
|
||||||
project = swagger_client.ProjectReq(metadata=metadata, cve_allowlist=cve_allowlist)
|
project = v2_swagger_client.ProjectReq(metadata=metadata, cve_allowlist=cve_allowlist)
|
||||||
try:
|
try:
|
||||||
_, sc, _ = client.projects_project_id_put_with_http_info(project_id, project)
|
_, sc, _ = client.update_project_with_http_info(project_id, project)
|
||||||
except ApiException as e:
|
except ApiException as e:
|
||||||
base._assert_status_code(expect_status_code, e.status)
|
base._assert_status_code(expect_status_code, e.status)
|
||||||
else:
|
else:
|
||||||
@ -100,31 +107,34 @@ class Project(base.Base):
|
|||||||
|
|
||||||
def delete_project(self, project_id, expect_status_code = 200, **kwargs):
|
def delete_project(self, project_id, expect_status_code = 200, **kwargs):
|
||||||
client = self._get_client(**kwargs)
|
client = self._get_client(**kwargs)
|
||||||
_, status_code, _ = client.projects_project_id_delete_with_http_info(project_id)
|
_, status_code, _ = client.delete_project_with_http_info(project_id)
|
||||||
base._assert_status_code(expect_status_code, status_code)
|
base._assert_status_code(expect_status_code, status_code)
|
||||||
|
|
||||||
def get_project_log(self, project_id, expect_status_code = 200, **kwargs):
|
def get_project_log(self, project_name, expect_status_code = 200, **kwargs):
|
||||||
client = self._get_client(**kwargs)
|
client = self._get_client(**kwargs)
|
||||||
body, status_code, _ = client.projects_project_id_logs_get_with_http_info(project_id)
|
body, status_code, _ = client.get_logs_with_http_info(project_name)
|
||||||
base._assert_status_code(expect_status_code, status_code)
|
base._assert_status_code(expect_status_code, status_code)
|
||||||
return body
|
return body
|
||||||
|
|
||||||
def filter_project_logs(self, project_id, operator, repository, tag, operation_type, **kwargs):
|
def filter_project_logs(self, project_name, operator, resource, resource_type, operation, **kwargs):
|
||||||
access_logs = self.get_project_log(project_id, **kwargs)
|
access_logs = self.get_project_log(project_name, **kwargs)
|
||||||
count = 0
|
count = 0
|
||||||
for each_access_log in list(access_logs):
|
for each_access_log in list(access_logs):
|
||||||
if each_access_log.username == operator and \
|
if each_access_log.username == operator and \
|
||||||
each_access_log.repo_name.strip(r'/') == repository and \
|
each_access_log.resource_type == resource_type and \
|
||||||
each_access_log.repo_tag == tag and \
|
each_access_log.resource == resource and \
|
||||||
each_access_log.operation == operation_type:
|
each_access_log.operation == operation:
|
||||||
count = count + 1
|
count = count + 1
|
||||||
return count
|
return count
|
||||||
|
|
||||||
def get_project_members(self, project_id, **kwargs):
|
def get_project_members(self, project_id, **kwargs):
|
||||||
|
kwargs['api_type'] = 'products'
|
||||||
client = self._get_client(**kwargs)
|
client = self._get_client(**kwargs)
|
||||||
return client.projects_project_id_members_get(project_id)
|
return client.projects_project_id_members_get(project_id)
|
||||||
|
|
||||||
def get_project_member(self, project_id, member_id, expect_status_code = 200, expect_response_body = None, **kwargs):
|
def get_project_member(self, project_id, member_id, expect_status_code = 200, expect_response_body = None, **kwargs):
|
||||||
|
from swagger_client.rest import ApiException
|
||||||
|
kwargs['api_type'] = 'products'
|
||||||
client = self._get_client(**kwargs)
|
client = self._get_client(**kwargs)
|
||||||
data = []
|
data = []
|
||||||
try:
|
try:
|
||||||
@ -140,6 +150,7 @@ class Project(base.Base):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
def get_project_member_id(self, project_id, member_user_name, **kwargs):
|
def get_project_member_id(self, project_id, member_user_name, **kwargs):
|
||||||
|
kwargs['api_type'] = 'products'
|
||||||
members = self.get_project_members(project_id, **kwargs)
|
members = self.get_project_members(project_id, **kwargs)
|
||||||
result = get_member_id_by_name(list(members), member_user_name)
|
result = get_member_id_by_name(list(members), member_user_name)
|
||||||
if result == None:
|
if result == None:
|
||||||
@ -148,18 +159,21 @@ class Project(base.Base):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
def check_project_member_not_exist(self, project_id, member_user_name, **kwargs):
|
def check_project_member_not_exist(self, project_id, member_user_name, **kwargs):
|
||||||
|
kwargs['api_type'] = 'products'
|
||||||
members = self.get_project_members(project_id, **kwargs)
|
members = self.get_project_members(project_id, **kwargs)
|
||||||
result = is_member_exist_in_project(list(members), member_user_name)
|
result = is_member_exist_in_project(list(members), member_user_name)
|
||||||
if result == True:
|
if result == True:
|
||||||
raise Exception(r"User {} should not be a member of project with ID {}.".format(member_user_name, project_id))
|
raise Exception(r"User {} should not be a member of project with ID {}.".format(member_user_name, project_id))
|
||||||
|
|
||||||
def check_project_members_exist(self, project_id, member_user_name, expected_member_role_id = None, **kwargs):
|
def check_project_members_exist(self, project_id, member_user_name, expected_member_role_id = None, **kwargs):
|
||||||
|
kwargs['api_type'] = 'products'
|
||||||
members = self.get_project_members(project_id, **kwargs)
|
members = self.get_project_members(project_id, **kwargs)
|
||||||
result = is_member_exist_in_project(members, member_user_name, expected_member_role_id = expected_member_role_id)
|
result = is_member_exist_in_project(members, member_user_name, expected_member_role_id = expected_member_role_id)
|
||||||
if result == False:
|
if result == False:
|
||||||
raise Exception(r"User {} should be a member of project with ID {}.".format(member_user_name, project_id))
|
raise Exception(r"User {} should be a member of project with ID {}.".format(member_user_name, project_id))
|
||||||
|
|
||||||
def update_project_member_role(self, project_id, member_id, member_role_id, expect_status_code = 200, **kwargs):
|
def update_project_member_role(self, project_id, member_id, member_role_id, expect_status_code = 200, **kwargs):
|
||||||
|
kwargs['api_type'] = 'products'
|
||||||
client = self._get_client(**kwargs)
|
client = self._get_client(**kwargs)
|
||||||
role = swagger_client.Role(role_id = member_role_id)
|
role = swagger_client.Role(role_id = member_role_id)
|
||||||
data, status_code, _ = client.projects_project_id_members_mid_put_with_http_info(project_id, member_id, role = role)
|
data, status_code, _ = client.projects_project_id_members_mid_put_with_http_info(project_id, member_id, role = role)
|
||||||
@ -168,12 +182,14 @@ class Project(base.Base):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
def delete_project_member(self, project_id, member_id, expect_status_code = 200, **kwargs):
|
def delete_project_member(self, project_id, member_id, expect_status_code = 200, **kwargs):
|
||||||
|
kwargs['api_type'] = 'products'
|
||||||
client = self._get_client(**kwargs)
|
client = self._get_client(**kwargs)
|
||||||
_, status_code, _ = client.projects_project_id_members_mid_delete_with_http_info(project_id, member_id)
|
_, status_code, _ = client.projects_project_id_members_mid_delete_with_http_info(project_id, member_id)
|
||||||
base._assert_status_code(expect_status_code, status_code)
|
base._assert_status_code(expect_status_code, status_code)
|
||||||
base._assert_status_code(200, status_code)
|
base._assert_status_code(200, status_code)
|
||||||
|
|
||||||
def add_project_members(self, project_id, user_id, member_role_id = None, expect_status_code = 201, **kwargs):
|
def add_project_members(self, project_id, user_id, member_role_id = None, expect_status_code = 201, **kwargs):
|
||||||
|
kwargs['api_type'] = 'products'
|
||||||
if member_role_id is None:
|
if member_role_id is None:
|
||||||
member_role_id = 1
|
member_role_id = 1
|
||||||
_member_user = {"user_id": int(user_id)}
|
_member_user = {"user_id": int(user_id)}
|
||||||
@ -185,6 +201,7 @@ class Project(base.Base):
|
|||||||
return base._get_id_from_header(header)
|
return base._get_id_from_header(header)
|
||||||
|
|
||||||
def add_project_robot_account(self, project_id, project_name, expires_at, robot_name = None, robot_desc = None, has_pull_right = True, has_push_right = True, has_chart_read_right = True, has_chart_create_right = True, expect_status_code = 201, **kwargs):
|
def add_project_robot_account(self, project_id, project_name, expires_at, robot_name = None, robot_desc = None, has_pull_right = True, has_push_right = True, has_chart_read_right = True, has_chart_create_right = True, expect_status_code = 201, **kwargs):
|
||||||
|
kwargs['api_type'] = 'products'
|
||||||
if robot_name is None:
|
if robot_name is None:
|
||||||
robot_name = base._random_name("robot")
|
robot_name = base._random_name("robot")
|
||||||
if robot_desc is None:
|
if robot_desc is None:
|
||||||
@ -221,11 +238,13 @@ class Project(base.Base):
|
|||||||
return base._get_id_from_header(header), data
|
return base._get_id_from_header(header), data
|
||||||
|
|
||||||
def get_project_robot_account_by_id(self, project_id, robot_id, **kwargs):
|
def get_project_robot_account_by_id(self, project_id, robot_id, **kwargs):
|
||||||
|
kwargs['api_type'] = 'products'
|
||||||
client = self._get_client(**kwargs)
|
client = self._get_client(**kwargs)
|
||||||
data, status_code, _ = client.projects_project_id_robots_robot_id_get_with_http_info(project_id, robot_id)
|
data, status_code, _ = client.projects_project_id_robots_robot_id_get_with_http_info(project_id, robot_id)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def disable_project_robot_account(self, project_id, robot_id, disable, expect_status_code = 200, **kwargs):
|
def disable_project_robot_account(self, project_id, robot_id, disable, expect_status_code = 200, **kwargs):
|
||||||
|
kwargs['api_type'] = 'products'
|
||||||
client = self._get_client(**kwargs)
|
client = self._get_client(**kwargs)
|
||||||
robotAccountUpdate = swagger_client.RobotAccountUpdate(disable)
|
robotAccountUpdate = swagger_client.RobotAccountUpdate(disable)
|
||||||
_, status_code, _ = client.projects_project_id_robots_robot_id_put_with_http_info(project_id, robot_id, robotAccountUpdate)
|
_, status_code, _ = client.projects_project_id_robots_robot_id_put_with_http_info(project_id, robot_id, robotAccountUpdate)
|
||||||
@ -233,6 +252,7 @@ class Project(base.Base):
|
|||||||
base._assert_status_code(200, status_code)
|
base._assert_status_code(200, status_code)
|
||||||
|
|
||||||
def delete_project_robot_account(self, project_id, robot_id, expect_status_code = 200, **kwargs):
|
def delete_project_robot_account(self, project_id, robot_id, expect_status_code = 200, **kwargs):
|
||||||
|
kwargs['api_type'] = 'products'
|
||||||
client = self._get_client(**kwargs)
|
client = self._get_client(**kwargs)
|
||||||
_, status_code, _ = client.projects_project_id_robots_robot_id_delete_with_http_info(project_id, robot_id)
|
_, status_code, _ = client.projects_project_id_robots_robot_id_delete_with_http_info(project_id, robot_id)
|
||||||
base._assert_status_code(expect_status_code, status_code)
|
base._assert_status_code(expect_status_code, status_code)
|
||||||
|
@ -14,7 +14,7 @@ def pull_harbor_image(registry, username, password, image, tag, expected_login_e
|
|||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
ret = _docker_api.docker_image_pull(r'{}/{}'.format(registry, image), tag = tag, expected_error_message = expected_error_message)
|
ret = _docker_api.docker_image_pull(r'{}/{}'.format(registry, image), tag = tag, expected_error_message = expected_error_message)
|
||||||
|
|
||||||
def push_image_to_project(project_name, registry, username, password, image, tag, expected_login_error_message = None, expected_error_message = None, profix_for_image = None):
|
def push_image_to_project(project_name, registry, username, password, image, tag, expected_login_error_message = None, expected_error_message = None, profix_for_image = None, new_image=None):
|
||||||
_docker_api = DockerAPI()
|
_docker_api = DockerAPI()
|
||||||
_docker_api.docker_login(registry, username, password, expected_error_message = expected_login_error_message)
|
_docker_api.docker_login(registry, username, password, expected_error_message = expected_login_error_message)
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
@ -23,6 +23,8 @@ def push_image_to_project(project_name, registry, username, password, image, tag
|
|||||||
_docker_api.docker_image_pull(image, tag = tag)
|
_docker_api.docker_image_pull(image, tag = tag)
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
|
|
||||||
|
image = new_image or image
|
||||||
|
|
||||||
if profix_for_image == None:
|
if profix_for_image == None:
|
||||||
new_harbor_registry, new_tag = _docker_api.docker_image_tag(r'{}:{}'.format(image, tag), r'{}/{}/{}'.format(registry, project_name, image))
|
new_harbor_registry, new_tag = _docker_api.docker_image_tag(r'{}:{}'.format(image, tag), r'{}/{}/{}'.format(registry, project_name, image))
|
||||||
else:
|
else:
|
||||||
|
@ -44,7 +44,7 @@ class TestProjects(unittest.TestCase):
|
|||||||
project_001_data = self.project.get_projects(dict(public=False), **USER_001_CLIENT)
|
project_001_data = self.project.get_projects(dict(public=False), **USER_001_CLIENT)
|
||||||
|
|
||||||
#3.2 Check user-001 has no any private project
|
#3.2 Check user-001 has no any private project
|
||||||
self.assertEqual(project_001_data, None, msg="user-001 should has no any private project, but we got {}".format(project_001_data))
|
self.assertEqual(len(project_001_data), 0, msg="user-001 should has no any private project, but we got {}".format(project_001_data))
|
||||||
|
|
||||||
#4. Add user-001 as a member of project-001
|
#4. Add user-001 as a member of project-001
|
||||||
result = self.project.add_project_members(project_001_id, user_001_id, **ADMIN_CLIENT)
|
result = self.project.add_project_members(project_001_id, user_001_id, **ADMIN_CLIENT)
|
||||||
|
@ -20,20 +20,13 @@ import unittest
|
|||||||
import testutils
|
import testutils
|
||||||
import docker
|
import docker
|
||||||
|
|
||||||
import swagger_client
|
|
||||||
|
|
||||||
from testutils import ADMIN_CLIENT
|
from testutils import ADMIN_CLIENT
|
||||||
from swagger_client.models.project import Project
|
|
||||||
from swagger_client.models.project_req import ProjectReq
|
|
||||||
from swagger_client.models.project_metadata import ProjectMetadata
|
|
||||||
from swagger_client.models.project_member import ProjectMember
|
from swagger_client.models.project_member import ProjectMember
|
||||||
from swagger_client.models.user_group import UserGroup
|
from swagger_client.models.user_group import UserGroup
|
||||||
from swagger_client.models.configurations import Configurations
|
from swagger_client.models.configurations import Configurations
|
||||||
from library.projectV2 import ProjectV2
|
from library.project import Project
|
||||||
from library.base import _assert_status_code
|
from library.base import _assert_status_code
|
||||||
from library.base import _random_name
|
from library.base import _random_name
|
||||||
|
|
||||||
|
|
||||||
from v2_swagger_client.rest import ApiException
|
from v2_swagger_client.rest import ApiException
|
||||||
from pprint import pprint
|
from pprint import pprint
|
||||||
|
|
||||||
@ -46,23 +39,20 @@ class TestAssignRoleToLdapGroup(unittest.TestCase):
|
|||||||
repository_api = testutils.GetRepositoryApi("admin", "Harbor12345")
|
repository_api = testutils.GetRepositoryApi("admin", "Harbor12345")
|
||||||
project_id = 0
|
project_id = 0
|
||||||
docker_client = docker.from_env()
|
docker_client = docker.from_env()
|
||||||
_project_name = _random_name("test_private")
|
_project_name = _random_name("test-ldap-group")
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.projectv2= ProjectV2()
|
self.project = Project()
|
||||||
|
|
||||||
#login with admin, create a project and assign role to ldap group
|
#login with admin, create a project and assign role to ldap group
|
||||||
result = self.product_api.configurations_put(configurations=Configurations(ldap_filter="", ldap_group_attribute_name="cn", ldap_group_base_dn="ou=groups,dc=example,dc=com", ldap_group_search_filter="objectclass=groupOfNames", ldap_group_search_scope=2))
|
result = self.product_api.configurations_put(configurations=Configurations(ldap_filter="", ldap_group_attribute_name="cn", ldap_group_base_dn="ou=groups,dc=example,dc=com", ldap_group_search_filter="objectclass=groupOfNames", ldap_group_search_scope=2))
|
||||||
pprint(result)
|
pprint(result)
|
||||||
cfgs = self.product_api.configurations_get()
|
cfgs = self.product_api.configurations_get()
|
||||||
pprint(cfgs)
|
pprint(cfgs)
|
||||||
req = ProjectReq()
|
result = self.project.create_project(self._project_name, dict(public="false"))
|
||||||
req.project_name = self._project_name
|
|
||||||
req.metadata = ProjectMetadata(public="false")
|
|
||||||
result = self.product_api.projects_post(req)
|
|
||||||
pprint(result)
|
pprint(result)
|
||||||
|
|
||||||
projs = self.product_api.projects_get(name = self._project_name)
|
projs = self.project.get_projects(dict(name = self._project_name))
|
||||||
if len(projs)>0 :
|
if len(projs)>0 :
|
||||||
project = projs[0]
|
project = projs[0]
|
||||||
self.project_id = project.project_id
|
self.project_id = project.project_id
|
||||||
@ -91,33 +81,30 @@ class TestAssignRoleToLdapGroup(unittest.TestCase):
|
|||||||
|
|
||||||
result = self.product_api.projects_project_id_members_post( project_id=self.project_id, project_member=projectmember )
|
result = self.product_api.projects_project_id_members_post( project_id=self.project_id, project_member=projectmember )
|
||||||
pprint(result)
|
pprint(result)
|
||||||
pass
|
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
#delete images in project
|
|
||||||
result = self.repository_api.delete_repository(self._project_name, "busybox")
|
|
||||||
pprint(result)
|
|
||||||
result = self.repository_api.delete_repository(self._project_name, "busyboxdev")
|
|
||||||
pprint(result)
|
|
||||||
if self.project_id > 0 :
|
if self.project_id > 0 :
|
||||||
self.product_api.projects_project_id_delete(self.project_id)
|
# delete images in project
|
||||||
pass
|
result = self.repository_api.delete_repository(self._project_name, "busybox")
|
||||||
|
pprint(result)
|
||||||
|
result = self.repository_api.delete_repository(self._project_name, "busyboxdev")
|
||||||
|
pprint(result)
|
||||||
|
self.project.delete_project(self.project_id)
|
||||||
|
|
||||||
def testAssignRoleToLdapGroup(self):
|
def testAssignRoleToLdapGroup(self):
|
||||||
"""Test AssignRoleToLdapGroup"""
|
"""Test AssignRoleToLdapGroup"""
|
||||||
admin_product_api = testutils.GetProductApi(username="admin_user", password="zhu88jie")
|
admin_product_api = Project("admin_user", "zhu88jie")
|
||||||
projects = admin_product_api.projects_get(name=self._project_name)
|
projects = admin_product_api.get_projects(dict(name=self._project_name))
|
||||||
self.assertTrue(len(projects) == 1)
|
self.assertTrue(len(projects) == 1)
|
||||||
self.assertEqual(1, projects[0].current_user_role_id)
|
self.assertEqual(1, projects[0].current_user_role_id)
|
||||||
|
|
||||||
|
dev_product_api = Project("dev_user", "zhu88jie")
|
||||||
dev_product_api = testutils.GetProductApi("dev_user", "zhu88jie")
|
projects = dev_product_api.get_projects(dict(name=self._project_name))
|
||||||
projects = dev_product_api.projects_get(name=self._project_name)
|
|
||||||
self.assertTrue(len(projects) == 1)
|
self.assertTrue(len(projects) == 1)
|
||||||
self.assertEqual(2, projects[0].current_user_role_id)
|
self.assertEqual(2, projects[0].current_user_role_id)
|
||||||
|
|
||||||
guest_product_api = testutils.GetProductApi("guest_user", "zhu88jie")
|
guest_product_api = Project("guest_user", "zhu88jie")
|
||||||
projects = guest_product_api.projects_get(name=self._project_name)
|
projects = guest_product_api.get_projects(dict(name=self._project_name))
|
||||||
self.assertTrue(len(projects) == 1)
|
self.assertTrue(len(projects) == 1)
|
||||||
self.assertEqual(3, projects[0].current_user_role_id)
|
self.assertEqual(3, projects[0].current_user_role_id)
|
||||||
|
|
||||||
@ -130,8 +117,6 @@ class TestAssignRoleToLdapGroup(unittest.TestCase):
|
|||||||
self.assertTrue(self.queryUserLogs(username="guest_user", password="zhu88jie")>0, "guest user can see logs")
|
self.assertTrue(self.queryUserLogs(username="guest_user", password="zhu88jie")>0, "guest user can see logs")
|
||||||
self.assertTrue(self.queryUserLogs(username="test", password="123456", status_code=403)==0, "test user can not see any logs")
|
self.assertTrue(self.queryUserLogs(username="test", password="123456", status_code=403)==0, "test user can not see any logs")
|
||||||
|
|
||||||
pass
|
|
||||||
|
|
||||||
# admin user can push, pull images
|
# admin user can push, pull images
|
||||||
def dockerCmdLoginAdmin(self, username, password):
|
def dockerCmdLoginAdmin(self, username, password):
|
||||||
pprint(self.docker_client.info())
|
pprint(self.docker_client.info())
|
||||||
@ -143,7 +128,7 @@ class TestAssignRoleToLdapGroup(unittest.TestCase):
|
|||||||
if output.find("error")>0 :
|
if output.find("error")>0 :
|
||||||
self.fail("Should not fail to push image for admin_user")
|
self.fail("Should not fail to push image for admin_user")
|
||||||
self.docker_client.images.pull(repository=self.harbor_host+"/"+self._project_name+"/busybox", tag="latest")
|
self.docker_client.images.pull(repository=self.harbor_host+"/"+self._project_name+"/busybox", tag="latest")
|
||||||
pass
|
|
||||||
# dev user can push, pull images
|
# dev user can push, pull images
|
||||||
def dockerCmdLoginDev(self, username, password, harbor_server=harbor_host):
|
def dockerCmdLoginDev(self, username, password, harbor_server=harbor_host):
|
||||||
self.docker_client.login(username=username, password=password, registry=self.harbor_host)
|
self.docker_client.login(username=username, password=password, registry=self.harbor_host)
|
||||||
@ -153,7 +138,7 @@ class TestAssignRoleToLdapGroup(unittest.TestCase):
|
|||||||
output = self.docker_client.images.push(repository=self.harbor_host+"/"+self._project_name+"/busyboxdev", tag="latest")
|
output = self.docker_client.images.push(repository=self.harbor_host+"/"+self._project_name+"/busyboxdev", tag="latest")
|
||||||
if output.find("error") >0 :
|
if output.find("error") >0 :
|
||||||
self.fail("Should not fail to push images for dev_user")
|
self.fail("Should not fail to push images for dev_user")
|
||||||
pass
|
|
||||||
# guest user can pull images
|
# guest user can pull images
|
||||||
def dockerCmdLoginGuest(self, username, password, harbor_server=harbor_host):
|
def dockerCmdLoginGuest(self, username, password, harbor_server=harbor_host):
|
||||||
self.docker_client.login(username=username, password=password, registry=self.harbor_host)
|
self.docker_client.login(username=username, password=password, registry=self.harbor_host)
|
||||||
@ -164,12 +149,12 @@ class TestAssignRoleToLdapGroup(unittest.TestCase):
|
|||||||
if output.find("error")<0 :
|
if output.find("error")<0 :
|
||||||
self.fail("Should failed to push image for guest user")
|
self.fail("Should failed to push image for guest user")
|
||||||
self.docker_client.images.pull(repository=self.harbor_host+"/"+self._project_name+"/busybox", tag="latest")
|
self.docker_client.images.pull(repository=self.harbor_host+"/"+self._project_name+"/busybox", tag="latest")
|
||||||
pass
|
|
||||||
# check can see his log in current project
|
# check can see his log in current project
|
||||||
def queryUserLogs(self, username, password, status_code=200):
|
def queryUserLogs(self, username, password, status_code=200):
|
||||||
client=dict(endpoint = ADMIN_CLIENT["endpoint"], username = username, password = password)
|
client=dict(endpoint = ADMIN_CLIENT["endpoint"], username = username, password = password)
|
||||||
try:
|
try:
|
||||||
logs = self.projectv2.get_project_log(self._project_name, status_code, **client)
|
logs = self.project.get_project_log(self._project_name, status_code, **client)
|
||||||
count = 0
|
count = 0
|
||||||
for log in list(logs):
|
for log in list(logs):
|
||||||
count = count + 1
|
count = count + 1
|
||||||
|
@ -20,10 +20,10 @@ sys.path.append(os.environ["SWAGGER_CLIENT_PATH"])
|
|||||||
import unittest
|
import unittest
|
||||||
import testutils
|
import testutils
|
||||||
import swagger_client
|
import swagger_client
|
||||||
|
from testutils import TEARDOWN
|
||||||
from library.base import _random_name
|
from library.base import _random_name
|
||||||
from swagger_client.models.project_req import ProjectReq
|
from library.project import Project
|
||||||
from swagger_client.models.configurations import Configurations
|
from swagger_client.models.configurations import Configurations
|
||||||
from swagger_client.rest import ApiException
|
|
||||||
from pprint import pprint
|
from pprint import pprint
|
||||||
|
|
||||||
|
|
||||||
@ -32,26 +32,30 @@ from pprint import pprint
|
|||||||
class TestLdapAdminRole(unittest.TestCase):
|
class TestLdapAdminRole(unittest.TestCase):
|
||||||
"""AccessLog unit test stubs"""
|
"""AccessLog unit test stubs"""
|
||||||
product_api = testutils.GetProductApi("admin", "Harbor12345")
|
product_api = testutils.GetProductApi("admin", "Harbor12345")
|
||||||
mike_product_api = testutils.GetProductApi("mike", "zhu88jie")
|
|
||||||
project_id = 0
|
project_id = 0
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
pass
|
self.project= Project()
|
||||||
|
self.mike_product_api = Project("mike", "zhu88jie")
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
|
print("Case completed")
|
||||||
|
|
||||||
|
@unittest.skipIf(TEARDOWN == False, "Test data won't be erased.")
|
||||||
|
def test_ClearData(self):
|
||||||
if self.project_id > 0 :
|
if self.project_id > 0 :
|
||||||
self.mike_product_api.projects_project_id_delete(project_id=self.project_id)
|
self.mike_product_api.delete_project(self.project_id)
|
||||||
pass
|
|
||||||
|
|
||||||
def testLdapAdminRole(self):
|
def testLdapAdminRole(self):
|
||||||
"""Test LdapAdminRole"""
|
"""Test LdapAdminRole"""
|
||||||
_project_name = _random_name("test_private")
|
_project_name = _random_name("test-ldap-admin-role")
|
||||||
result = self.product_api.configurations_put(configurations=Configurations(ldap_group_admin_dn="cn=harbor_users,ou=groups,dc=example,dc=com"))
|
result = self.product_api.configurations_put(configurations=Configurations(ldap_group_admin_dn="cn=harbor_users,ou=groups,dc=example,dc=com"))
|
||||||
|
|
||||||
# Create a private project
|
# Create a private project
|
||||||
result = self.product_api.projects_post(project=ProjectReq(project_name= _project_name))
|
result = self.project.create_project(_project_name)
|
||||||
|
|
||||||
# query project with ldap user mike
|
# query project with ldap user mike
|
||||||
projects = self.mike_product_api.projects_get(name=_project_name)
|
projects = self.mike_product_api.get_projects(dict(name=_project_name))
|
||||||
|
|
||||||
print("=================", projects)
|
print("=================", projects)
|
||||||
self.assertTrue(len(projects) == 1)
|
self.assertTrue(len(projects) == 1)
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
import swagger_client
|
import v2_swagger_client
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from testutils import ADMIN_CLIENT
|
from testutils import ADMIN_CLIENT, TEARDOWN
|
||||||
from library.project import Project
|
from library.project import Project
|
||||||
from library.user import User
|
from library.user import User
|
||||||
|
|
||||||
@ -50,6 +50,10 @@ class TestProjectCVEAllowlist(unittest.TestCase):
|
|||||||
self.member_id = int(m_id)
|
self.member_id = int(m_id)
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
|
print("Case completed")
|
||||||
|
|
||||||
|
@unittest.skipIf(TEARDOWN == False, "Test data won't be erased.")
|
||||||
|
def test_ClearData(self):
|
||||||
print("Tearing down...")
|
print("Tearing down...")
|
||||||
self.project.delete_project_member(self.project_pa_id, self.member_id, **ADMIN_CLIENT)
|
self.project.delete_project_member(self.project_pa_id, self.member_id, **ADMIN_CLIENT)
|
||||||
self.project.delete_project(self.project_pa_id,**ADMIN_CLIENT)
|
self.project.delete_project(self.project_pa_id,**ADMIN_CLIENT)
|
||||||
@ -63,9 +67,9 @@ class TestProjectCVEAllowlist(unittest.TestCase):
|
|||||||
self.assertEqual(0, len(p.cve_allowlist.items))
|
self.assertEqual(0, len(p.cve_allowlist.items))
|
||||||
|
|
||||||
# User(RA) updates the project CVE allowlist, verify it fails with Forbidden error.
|
# User(RA) updates the project CVE allowlist, verify it fails with Forbidden error.
|
||||||
item_list = [swagger_client.CVEAllowlistItem(cve_id="CVE-2019-12310")]
|
item_list = [v2_swagger_client.CVEAllowlistItem(cve_id="CVE-2019-12310")]
|
||||||
exp = int(time.time()) + 1000
|
exp = int(time.time()) + 1000
|
||||||
wl = swagger_client.CVEAllowlist(expires_at=exp, items=item_list)
|
wl = v2_swagger_client.CVEAllowlist(expires_at=exp, items=item_list)
|
||||||
self.project.update_project(self.project_pa_id, cve_allowlist=wl, expect_status_code=403, **self.USER_RA_CLIENT)
|
self.project.update_project(self.project_pa_id, cve_allowlist=wl, expect_status_code=403, **self.USER_RA_CLIENT)
|
||||||
|
|
||||||
# Admin user updates User(RA) as project admin.
|
# Admin user updates User(RA) as project admin.
|
||||||
@ -78,14 +82,14 @@ class TestProjectCVEAllowlist(unittest.TestCase):
|
|||||||
self.assertEqual(exp, p.cve_allowlist.expires_at)
|
self.assertEqual(exp, p.cve_allowlist.expires_at)
|
||||||
|
|
||||||
# User(RA) updates the project CVE allowlist with empty items list
|
# User(RA) updates the project CVE allowlist with empty items list
|
||||||
wl2 = swagger_client.CVEAllowlist(items=[])
|
wl2 = v2_swagger_client.CVEAllowlist(items=[])
|
||||||
self.project.update_project(self.project_pa_id, cve_allowlist=wl2, **self.USER_RA_CLIENT)
|
self.project.update_project(self.project_pa_id, cve_allowlist=wl2, **self.USER_RA_CLIENT)
|
||||||
p = self.project.get_project(self.project_pa_id, **self.USER_RA_CLIENT)
|
p = self.project.get_project(self.project_pa_id, **self.USER_RA_CLIENT)
|
||||||
self.assertEqual(0, len(p.cve_allowlist.items))
|
self.assertEqual(0, len(p.cve_allowlist.items))
|
||||||
self.assertIsNone(p.cve_allowlist.expires_at)
|
self.assertIsNone(p.cve_allowlist.expires_at)
|
||||||
|
|
||||||
# User(RA) updates the project metadata to set "reuse_sys_cve_allowlist" to true.
|
# User(RA) updates the project metadata to set "reuse_sys_cve_allowlist" to true.
|
||||||
meta = swagger_client.ProjectMetadata(reuse_sys_cve_allowlist="true")
|
meta = v2_swagger_client.ProjectMetadata(reuse_sys_cve_allowlist="true")
|
||||||
self.project.update_project(self.project_pa_id, metadata=meta, **self.USER_RA_CLIENT)
|
self.project.update_project(self.project_pa_id, metadata=meta, **self.USER_RA_CLIENT)
|
||||||
p = self.project.get_project(self.project_pa_id, **self.USER_RA_CLIENT)
|
p = self.project.get_project(self.project_pa_id, **self.USER_RA_CLIENT)
|
||||||
self.assertEqual("true", p.metadata.reuse_sys_cve_allowlist)
|
self.assertEqual("true", p.metadata.reuse_sys_cve_allowlist)
|
||||||
|
Loading…
Reference in New Issue
Block a user