diff --git a/docs/swagger.yaml b/docs/swagger.yaml index d2fc1211c..75e37ae8a 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -2575,7 +2575,9 @@ paths: - Products responses: '200': - description: Get job log successfully. + description: Get successfully. + schema: + type: string '400': description: Illegal format of provided ID value. '401': @@ -2776,7 +2778,11 @@ paths: description: The project name responses: '200': - $ref: '#/definitions/ChartInfoList' + description: Searched for charts of project in Harbor successfully. + schema: + type: array + items: + $ref: '#/definitions/ChartInfoEntry' '401': $ref: '#/definitions/UnauthorizedChartAPIError' '403': @@ -4279,9 +4285,15 @@ definitions: total_versions: type: integer description: Total count of chart versions + latest_version: + type: string + description: latest version of chart created: type: string description: The created time of chart + updated: + type: string + description: The created time of chart icon: type: string description: The icon path of chart @@ -4430,19 +4442,35 @@ definitions: GCResult: type: object properties: - status: + id: + type: integer + description: the id of gc job. + job_name: + type: string + description: the job name of gc job. + job_kind: + type: string + description: the job kind of gc job. + schedule: + $ref: '#/definitions/GCScheduleSchedule' + job_status: + type: string + description: the status of gc job. + deleted: type: boolean - description: the result of gc job. - msg: + description: if gc job was deleted. + creation_time: type: string - description: the details of gc job. - starttime: + description: the creation time of gc job. + update_time: type: string - description: the start time of gc job. - endtime: - type: string - description: the end time of gc job. + description: the update time of gc job. GCSchedule: + type: object + properties: + schedule: + $ref: '#/definitions/GCScheduleSchedule' + GCScheduleSchedule: type: object properties: type: diff --git a/tests/apitests/python/library/repository.py b/tests/apitests/python/library/repository.py index b936d15a1..0a3531f62 100644 --- a/tests/apitests/python/library/repository.py +++ b/tests/apitests/python/library/repository.py @@ -25,7 +25,7 @@ def push_image_to_project(project_name, registry, username, password, image, tag _docker_api.docker_image_pull(image, tag = tag) time.sleep(2) - new_harbor_registry, new_tag = _docker_api.docker_image_tag(image, 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)) time.sleep(2) _docker_api.docker_image_push(new_harbor_registry, new_tag) diff --git a/tests/apitests/python/library/system.py b/tests/apitests/python/library/system.py new file mode 100644 index 000000000..1e54faa9b --- /dev/null +++ b/tests/apitests/python/library/system.py @@ -0,0 +1,149 @@ +# -*- coding: utf-8 -*- + +import time +import re +import base +import swagger_client +from swagger_client.rest import ApiException + +class System(base.Base): + def get_gc_history(self, expect_status_code = 200, expect_response_body = None, **kwargs): + client = self._get_client(**kwargs) + + try: + data, status_code, _ = client.system_gc_get_with_http_info() + except ApiException as e: + if e.status == expect_status_code: + if expect_response_body is not None and e.body.strip() != expect_response_body.strip(): + raise Exception(r"Get configuration response body is not as expected {} actual status is {}.".format(expect_response_body.strip(), e.body.strip())) + else: + return e.reason, e.body + else: + raise Exception(r"Get configuration result is not as expected {} actual status is {}.".format(expect_status_code, e.status)) + base._assert_status_code(expect_status_code, status_code) + return data + + def get_gc_status_by_id(self, job_id, expect_status_code = 200, expect_response_body = None, **kwargs): + client = self._get_client(**kwargs) + + try: + data, status_code, _ = client.system_gc_id_get_with_http_info(job_id) + except ApiException as e: + if e.status == expect_status_code: + if expect_response_body is not None and e.body.strip() != expect_response_body.strip(): + raise Exception(r"Get configuration response body is not as expected {} actual status is {}.".format(expect_response_body.strip(), e.body.strip())) + else: + return e.reason, e.body + else: + raise Exception(r"Get configuration result is not as expected {} actual status is {}.".format(expect_status_code, e.status)) + base._assert_status_code(expect_status_code, status_code) + return data + + def get_gc_log_by_id(self, job_id, expect_status_code = 200, expect_response_body = None, **kwargs): + client = self._get_client(**kwargs) + + try: + data, status_code, _ = client.system_gc_id_log_get_with_http_info(job_id) + except ApiException as e: + if e.status == expect_status_code: + if expect_response_body is not None and e.body.strip() != expect_response_body.strip(): + raise Exception(r"Get configuration response body is not as expected {} actual status is {}.".format(expect_response_body.strip(), e.body.strip())) + else: + return e.reason, e.body + else: + raise Exception(r"Get configuration result is not as expected {} actual status is {}.".format(expect_status_code, e.status)) + base._assert_status_code(expect_status_code, status_code) + return data + + def get_gc_schedule(self, expect_status_code = 200, expect_response_body = None, **kwargs): + client = self._get_client(**kwargs) + + try: + data, status_code, _ = client.system_gc_schedule_get_with_http_info() + except ApiException as e: + if e.status == expect_status_code: + if expect_response_body is not None and e.body.strip() != expect_response_body.strip(): + raise Exception(r"Get configuration response body is not as expected {} actual status is {}.".format(expect_response_body.strip(), e.body.strip())) + else: + return e.reason, e.body + else: + raise Exception(r"Get configuration result is not as expected {} actual status is {}.".format(expect_status_code, e.status)) + base._assert_status_code(expect_status_code, status_code) + return data + + def set_gc_schedule(self, schedule_type = 'None', offtime = None, weekday = None, expect_status_code = 200, expect_response_body = None, **kwargs): + client = self._get_client(**kwargs) + gc_schedule = swagger_client.GCSchedule() + gc_schedule.type = schedule_type + if offtime is not None: + gc_schedule.offtime = offtime + if weekday is not None: + gc_schedule.weekday = weekday + try: + data, status_code, _ = client.system_gc_schedule_put_with_http_info(gc_schedule) + except ApiException as e: + if e.status == expect_status_code: + if expect_response_body is not None and e.body.strip() != expect_response_body.strip(): + raise Exception(r"Get configuration response body is not as expected {} actual status is {}.".format(expect_response_body.strip(), e.body.strip())) + else: + return e.reason, e.body + else: + raise Exception(r"Get configuration result is not as expected {} actual status is {}.".format(expect_status_code, e.status)) + base._assert_status_code(expect_status_code, status_code) + return data + + def create_gc_schedule(self, schedule_type, offtime = None, weekday = None, expect_status_code = 201, expect_response_body = None, **kwargs): + client = self._get_client(**kwargs) + gcscheduleschedule = swagger_client.GCScheduleSchedule() + gcscheduleschedule.type = schedule_type + if offtime is not None: + gcscheduleschedule.offtime = offtime + if weekday is not None: + gcscheduleschedule.weekday = weekday + + gc_schedule = swagger_client.GCSchedule(gcscheduleschedule) + try: + _, status_code, header = client.system_gc_schedule_post_with_http_info(gc_schedule) + except ApiException as e: + if e.status == expect_status_code: + if expect_response_body is not None and e.body.strip() != expect_response_body.strip(): + raise Exception(r"Create GC schedule response body is not as expected {} actual status is {}.".format(expect_response_body.strip(), e.body.strip())) + else: + return e.reason, e.body + else: + raise Exception(r"Create GC schedule result is not as expected {} actual status is {}.".format(expect_status_code, e.status)) + base._assert_status_code(expect_status_code, status_code) + return base._get_id_from_header(header) + + def gc_now(self, **kwargs): + gc_id = self.create_gc_schedule('Manual', **kwargs) + return gc_id + + def validate_gc_job_status(self, gc_id, expected_gc_status, **kwargs): + get_gc_status_finish = False + timeout_count = 20 + while not (get_gc_status_finish): + time.sleep(5) + status = self.get_gc_status_by_id(gc_id, **kwargs) + if len(status) is not 1: + raise Exception(r"Get GC status count expected 1 actual count is {}.".format(len(status))) + if status[0].job_status == expected_gc_status: + get_gc_status_finish = True + timeout_count = timeout_count - 1 + + if not (get_gc_status_finish): + raise Exception("Scan image result is not as expected {} actual scan status is {}".format(expected_scan_status, actual_scan_status)) + + def validate_deletion_success(self, gc_id, **kwargs): + log_content = self.get_gc_log_by_id(gc_id, **kwargs) + key_message = "blobs eligible for deletion" + key_message_pos = log_content.find(key_message) + full_message = log_content[key_message_pos-30 : key_message_pos + len(key_message)] + deleted_files_count_list = re.findall(r'\s+(\d+)\s+blobs eligible for deletion', full_message) + + if len(deleted_files_count_list) != 1: + raise Exception(r"Fail to get blobs eligible for deletion in log file, failure is {}.".format(len(deleted_files_count_list))) + deleted_files_count = int(deleted_files_count_list[0]) + if deleted_files_count == 0: + raise Exception(r"Get blobs eligible for deletion count is {}, while we expect more than 1.".format(deleted_files_count)) + diff --git a/tests/apitests/python/test_garbage_collection.py b/tests/apitests/python/test_garbage_collection.py new file mode 100644 index 000000000..891f632ad --- /dev/null +++ b/tests/apitests/python/test_garbage_collection.py @@ -0,0 +1,92 @@ +from __future__ import absolute_import + +import unittest + +from testutils import ADMIN_CLIENT +from testutils import TEARDOWN +from library.user import User +from library.system import System +from library.project import Project +from library.repository import Repository +from library.repository import push_image_to_project +from testutils import harbor_server +from library.base import _assert_status_code + +class TestProjects(unittest.TestCase): + @classmethod + def setUp(self): + system = System() + self.system= system + + project = Project() + self.project= project + + user = User() + self.user= user + + repo = Repository() + self.repo= repo + + @classmethod + def tearDown(self): + print "Case completed" + + @unittest.skipIf(TEARDOWN == False, "Test data won't be erased.") + def test_ClearData(self): + #2. Delete project(PA); + self.project.delete_project(TestProjects.project_gc_id, **TestProjects.USER_GC_CLIENT) + + #3. Delete user(UA); + self.user.delete_user(TestProjects.user_gc_id, **ADMIN_CLIENT) + + def testGarbageCollection(self): + """ + Test case: + Garbage Collection + Test step and expected result: + 1. Create a new user(UA); + 2. Create a new project(PA) by user(UA); + 3. Push a new image(IA) in project(PA) by admin; + 4. Delete repository(RA) by user(UA); + 5. Get repository by user(UA), it should get nothing; + 6. Tigger garbage collection operation; + 7. Check garbage collection job was finished; + 8. Get garbage collection log, check there is number of files was deleted. + Tear down: + 1. Delete project(PA); + 2. Delete user(UA). + """ + url = ADMIN_CLIENT["endpoint"] + admin_name = ADMIN_CLIENT["username"] + admin_password = ADMIN_CLIENT["password"] + user_gc_password = "Aa123456" + + #1. Create a new user(UA); + TestProjects.user_gc_id, user_gc_name = self.user.create_user_success(user_password = user_gc_password, **ADMIN_CLIENT) + + TestProjects.USER_GC_CLIENT=dict(endpoint = url, username = user_gc_name, password = user_gc_password) + + #2. Create a new project(PA) by user(UA); + TestProjects.project_gc_id, project_gc_name = self.project.create_project(metadata = {"public": "false"}, **TestProjects.USER_GC_CLIENT) + + #3. Push a new image(IA) in project(PA) by admin; + repo_name, _ = push_image_to_project(project_gc_name, harbor_server, admin_name, admin_password, "tomcat", "latest") + + #4. Delete repository(RA) by user(UA); + self.repo.delete_repoitory(repo_name, **TestProjects.USER_GC_CLIENT) + + #5. Get repository by user(UA), it should get nothing; + repo_data = self.repo.get_repository(TestProjects.project_gc_id, **TestProjects.USER_GC_CLIENT) + _assert_status_code(len(repo_data), 0) + + #6. Tigger garbage collection operation; + gc_id = self.system.gc_now(**ADMIN_CLIENT) + + #7. Check garbage collection job was finished; + self.system.validate_gc_job_status(gc_id, "finished", **ADMIN_CLIENT) + + #8. Get garbage collection log, check there is number of files was deleted. + self.system.validate_deletion_success(gc_id, **ADMIN_CLIENT) + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/tests/robot-cases/Group0-BAT/API_DB.robot b/tests/robot-cases/Group0-BAT/API_DB.robot index c45277be3..d35882086 100644 --- a/tests/robot-cases/Group0-BAT/API_DB.robot +++ b/tests/robot-cases/Group0-BAT/API_DB.robot @@ -32,4 +32,6 @@ Test Case - Scan Image Test Case - Manage Project Member Harbor API Test ./tests/apitests/python/test_manage_project_member.py Test Case - Project Level Policy Content Trust - Harbor API Test ./tests/apitests/python/test_project_level_policy_content_trust.py \ No newline at end of file + Harbor API Test ./tests/apitests/python/test_project_level_policy_content_trust.py +Test Case - Garbage Collection + Harbor API Test ./tests/apitests/python/test_garbage_collection.py \ No newline at end of file