From da8af6f6be6886dca4f96390632cf3b91891954e Mon Sep 17 00:00:00 2001 From: Nejc Habjan Date: Sat, 29 Aug 2020 16:52:56 +0200 Subject: [PATCH 1/5] refactor: turn objects module into a package --- gitlab/v4/{objects.py => objects/__init__.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename gitlab/v4/{objects.py => objects/__init__.py} (100%) diff --git a/gitlab/v4/objects.py b/gitlab/v4/objects/__init__.py similarity index 100% rename from gitlab/v4/objects.py rename to gitlab/v4/objects/__init__.py From 4492fc42c9f6e0031dd3f3c6c99e4c58d4f472ff Mon Sep 17 00:00:00 2001 From: Nejc Habjan Date: Sat, 29 Aug 2020 17:44:08 +0200 Subject: [PATCH 2/5] feat(api): add support for instance variables --- gitlab/__init__.py | 1 + gitlab/v4/objects/__init__.py | 26 ++---------------- gitlab/v4/objects/variables.py | 49 ++++++++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 24 deletions(-) create mode 100644 gitlab/v4/objects/variables.py diff --git a/gitlab/__init__.py b/gitlab/__init__.py index 1959adcee..a1327e218 100644 --- a/gitlab/__init__.py +++ b/gitlab/__init__.py @@ -139,6 +139,7 @@ def __init__( self.pagesdomains = objects.PagesDomainManager(self) self.user_activities = objects.UserActivitiesManager(self) self.applications = objects.ApplicationManager(self) + self.variables = objects.VariableManager(self) def __enter__(self): return self diff --git a/gitlab/v4/objects/__init__.py b/gitlab/v4/objects/__init__.py index eaf1cd05b..f9a2c25e6 100644 --- a/gitlab/v4/objects/__init__.py +++ b/gitlab/v4/objects/__init__.py @@ -23,6 +23,8 @@ from gitlab.mixins import * # noqa from gitlab import types from gitlab import utils +from gitlab.v4.objects.variables import * + VISIBILITY_PRIVATE = "private" VISIBILITY_INTERNAL = "internal" @@ -1366,18 +1368,6 @@ class GroupSubgroupManager(ListMixin, RESTManager): ) -class GroupVariable(SaveMixin, ObjectDeleteMixin, RESTObject): - _id_attr = "key" - - -class GroupVariableManager(CRUDMixin, RESTManager): - _path = "/groups/%(group_id)s/variables" - _obj_cls = GroupVariable - _from_parent_attrs = {"group_id": "id"} - _create_attrs = (("key", "value"), ("protected", "variable_type", "masked")) - _update_attrs = (("key", "value"), ("protected", "variable_type", "masked")) - - class Group(SaveMixin, ObjectDeleteMixin, RESTObject): _short_print_attr = "name" _managers = ( @@ -4116,18 +4106,6 @@ class ProjectUserManager(ListMixin, RESTManager): _list_filters = ("search",) -class ProjectVariable(SaveMixin, ObjectDeleteMixin, RESTObject): - _id_attr = "key" - - -class ProjectVariableManager(CRUDMixin, RESTManager): - _path = "/projects/%(project_id)s/variables" - _obj_cls = ProjectVariable - _from_parent_attrs = {"project_id": "id"} - _create_attrs = (("key", "value"), ("protected", "variable_type", "masked")) - _update_attrs = (("key", "value"), ("protected", "variable_type", "masked")) - - class ProjectService(SaveMixin, ObjectDeleteMixin, RESTObject): pass diff --git a/gitlab/v4/objects/variables.py b/gitlab/v4/objects/variables.py new file mode 100644 index 000000000..c8de80f81 --- /dev/null +++ b/gitlab/v4/objects/variables.py @@ -0,0 +1,49 @@ +""" +GitLab API: +https://docs.gitlab.com/ee/api/instance_level_ci_variables.html +https://docs.gitlab.com/ee/api/project_level_variables.html +https://docs.gitlab.com/ee/api/group_level_variables.html +""" +from gitlab.base import * # noqa +from gitlab.mixins import * # noqa + + +class Variable(SaveMixin, ObjectDeleteMixin, RESTObject): + _id_attr = "key" + + +class VariableManager(CRUDMixin, RESTManager): + _path = "/admin/ci/variables" + _obj_cls = Variable + _create_attrs = (("key", "value"), ("protected", "variable_type", "masked")) + _update_attrs = (("key", "value"), ("protected", "variable_type", "masked")) + + +class GroupVariable(SaveMixin, ObjectDeleteMixin, RESTObject): + _id_attr = "key" + + +class GroupVariableManager(CRUDMixin, RESTManager): + _path = "/groups/%(group_id)s/variables" + _obj_cls = GroupVariable + _from_parent_attrs = {"group_id": "id"} + _create_attrs = (("key", "value"), ("protected", "variable_type", "masked")) + _update_attrs = (("key", "value"), ("protected", "variable_type", "masked")) + + +class ProjectVariable(SaveMixin, ObjectDeleteMixin, RESTObject): + _id_attr = "key" + + +class ProjectVariableManager(CRUDMixin, RESTManager): + _path = "/projects/%(project_id)s/variables" + _obj_cls = ProjectVariable + _from_parent_attrs = {"project_id": "id"} + _create_attrs = ( + ("key", "value"), + ("protected", "variable_type", "masked", "environment_scope"), + ) + _update_attrs = ( + ("key", "value"), + ("protected", "variable_type", "masked", "environment_scope"), + ) From 66d108de9665055921123476426fb6716c602496 Mon Sep 17 00:00:00 2001 From: Nejc Habjan Date: Sat, 29 Aug 2020 17:48:27 +0200 Subject: [PATCH 3/5] test(api): add tests for variables API --- gitlab/tests/objects/test_variables.py | 193 +++++++++++++++++++++ tools/functional/api/test_variables.py | 48 +++++ tools/functional/cli/conftest.py | 21 +++ tools/functional/cli/test_cli_variables.py | 19 ++ tools/functional/conftest.py | 27 +-- tools/functional_tests.sh | 2 +- tools/python_test_v4.py | 19 -- 7 files changed, 288 insertions(+), 41 deletions(-) create mode 100644 gitlab/tests/objects/test_variables.py create mode 100644 tools/functional/api/test_variables.py create mode 100644 tools/functional/cli/conftest.py create mode 100644 tools/functional/cli/test_cli_variables.py diff --git a/gitlab/tests/objects/test_variables.py b/gitlab/tests/objects/test_variables.py new file mode 100644 index 000000000..d79bf96c3 --- /dev/null +++ b/gitlab/tests/objects/test_variables.py @@ -0,0 +1,193 @@ +""" +GitLab API: +https://docs.gitlab.com/ee/api/instance_level_ci_variables.html +https://docs.gitlab.com/ee/api/project_level_variables.html +https://docs.gitlab.com/ee/api/group_level_variables.html +""" + +import re + +import pytest +import responses + +from gitlab.v4.objects import GroupVariable, ProjectVariable, Variable + + +key = "TEST_VARIABLE_1" +value = "TEST_1" +new_value = "TEST_2" + +variable_content = { + "key": key, + "variable_type": "env_var", + "value": value, + "protected": False, + "masked": True, +} +variables_url = re.compile( + r"http://localhost/api/v4/(((groups|projects)/1)|(admin/ci))/variables" +) +variables_key_url = re.compile( + rf"http://localhost/api/v4/(((groups|projects)/1)|(admin/ci))/variables/{key}" +) + + +@pytest.fixture +def resp_list_variables(): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.GET, + url=variables_url, + json=[variable_content], + content_type="application/json", + status=200, + ) + yield rsps + + +@pytest.fixture +def resp_get_variable(): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.GET, + url=variables_key_url, + json=variable_content, + content_type="application/json", + status=200, + ) + yield rsps + + +@pytest.fixture +def resp_create_variable(): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.POST, + url=variables_url, + json=variable_content, + content_type="application/json", + status=200, + ) + yield rsps + + +@pytest.fixture +def resp_update_variable(): + updated_content = dict(variable_content) + updated_content["value"] = new_value + + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.PUT, + url=variables_key_url, + json=updated_content, + content_type="application/json", + status=200, + ) + yield rsps + + +@pytest.fixture +def resp_delete_variable(no_content): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.DELETE, + url=variables_key_url, + json=no_content, + content_type="application/json", + status=204, + ) + yield rsps + + +def test_list_instance_variables(gl, resp_list_variables): + variables = gl.variables.list() + assert isinstance(variables, list) + assert isinstance(variables[0], Variable) + assert variables[0].value == value + + +def test_get_instance_variable(gl, resp_get_variable): + variable = gl.variables.get(key) + assert isinstance(variable, Variable) + assert variable.value == value + + +def test_create_instance_variable(gl, resp_create_variable): + variable = gl.variables.create({"key": key, "value": value}) + assert isinstance(variable, Variable) + assert variable.value == value + + +def test_update_instance_variable(gl, resp_update_variable): + variable = gl.variables.get(key, lazy=True) + variable.value = new_value + variable.save() + assert variable.value == new_value + + +def test_delete_instance_variable(gl, resp_delete_variable): + variable = gl.variables.get(key, lazy=True) + variable.delete() + + +def test_list_project_variables(project, resp_list_variables): + variables = project.variables.list() + assert isinstance(variables, list) + assert isinstance(variables[0], ProjectVariable) + assert variables[0].value == value + + +def test_get_project_variable(project, resp_get_variable): + variable = project.variables.get(key) + assert isinstance(variable, ProjectVariable) + assert variable.value == value + + +def test_create_project_variable(project, resp_create_variable): + variable = project.variables.create({"key": key, "value": value}) + assert isinstance(variable, ProjectVariable) + assert variable.value == value + + +def test_update_project_variable(project, resp_update_variable): + variable = project.variables.get(key, lazy=True) + variable.value = new_value + variable.save() + assert variable.value == new_value + + +def test_delete_project_variable(project, resp_delete_variable): + variable = project.variables.get(key, lazy=True) + variable.delete() + + +def test_list_group_variables(group, resp_list_variables): + variables = group.variables.list() + assert isinstance(variables, list) + assert isinstance(variables[0], GroupVariable) + assert variables[0].value == value + + +def test_get_group_variable(group, resp_get_variable): + variable = group.variables.get(key) + assert isinstance(variable, GroupVariable) + assert variable.value == value + + +def test_create_group_variable(group, resp_create_variable): + variable = group.variables.create({"key": key, "value": value}) + assert isinstance(variable, GroupVariable) + assert variable.value == value + + +def test_update_group_variable(group, resp_update_variable): + variable = group.variables.get(key, lazy=True) + variable.value = new_value + variable.save() + assert variable.value == new_value + + +def test_delete_group_variable(group, resp_delete_variable): + variable = group.variables.get(key, lazy=True) + variable.delete() diff --git a/tools/functional/api/test_variables.py b/tools/functional/api/test_variables.py new file mode 100644 index 000000000..d20ebba27 --- /dev/null +++ b/tools/functional/api/test_variables.py @@ -0,0 +1,48 @@ +""" +GitLab API: +https://docs.gitlab.com/ee/api/instance_level_ci_variables.html +https://docs.gitlab.com/ee/api/project_level_variables.html +https://docs.gitlab.com/ee/api/group_level_variables.html +""" + + +def test_instance_variables(gl): + variable = gl.variables.create({"key": "key1", "value": "value1"}) + assert variable.value == "value1" + assert len(gl.variables.list()) == 1 + + variable.value = "new_value1" + variable.save() + variable = gl.variables.get(variable.key) + assert variable.value == "new_value1" + + variable.delete() + assert len(gl.variables.list()) == 0 + + +def test_group_variables(group): + variable = group.variables.create({"key": "key1", "value": "value1"}) + assert variable.value == "value1" + assert len(group.variables.list()) == 1 + + variable.value = "new_value1" + variable.save() + variable = group.variables.get(variable.key) + assert variable.value == "new_value1" + + variable.delete() + assert len(group.variables.list()) == 0 + + +def test_project_variables(project): + variable = project.variables.create({"key": "key1", "value": "value1"}) + assert variable.value == "value1" + assert len(project.variables.list()) == 1 + + variable.value = "new_value1" + variable.save() + variable = project.variables.get(variable.key) + assert variable.value == "new_value1" + + variable.delete() + assert len(project.variables.list()) == 0 diff --git a/tools/functional/cli/conftest.py b/tools/functional/cli/conftest.py new file mode 100644 index 000000000..13c30962e --- /dev/null +++ b/tools/functional/cli/conftest.py @@ -0,0 +1,21 @@ +import pytest + + +@pytest.fixture +def gitlab_cli(script_runner, CONFIG): + """Wrapper fixture to help make test cases less verbose.""" + + def _gitlab_cli(subcommands): + """ + Return a script_runner.run method that takes a default gitlab + command, and subcommands passed as arguments inside test cases. + """ + command = ["gitlab", "--config-file", CONFIG] + + for subcommand in subcommands: + # ensure we get strings (e.g from IDs) + command.append(str(subcommand)) + + return script_runner.run(*command) + + return _gitlab_cli diff --git a/tools/functional/cli/test_cli_variables.py b/tools/functional/cli/test_cli_variables.py new file mode 100644 index 000000000..9b1b16d0c --- /dev/null +++ b/tools/functional/cli/test_cli_variables.py @@ -0,0 +1,19 @@ +def test_list_instance_variables(gitlab_cli, gl): + cmd = ["variable", "list"] + ret = gitlab_cli(cmd) + + assert ret.success + + +def test_list_group_variables(gitlab_cli, group): + cmd = ["group-variable", "list", "--group-id", group.id] + ret = gitlab_cli(cmd) + + assert ret.success + + +def test_list_project_variables(gitlab_cli, project): + cmd = ["project-variable", "list", "--project-id", project.id] + ret = gitlab_cli(cmd) + + assert ret.success diff --git a/tools/functional/conftest.py b/tools/functional/conftest.py index bd99fa9ab..e12471b5a 100644 --- a/tools/functional/conftest.py +++ b/tools/functional/conftest.py @@ -1,3 +1,5 @@ +import os +import tempfile from random import randint import pytest @@ -5,6 +7,9 @@ import gitlab +TEMP_DIR = tempfile.gettempdir() + + def random_id(): """ Helper to ensure new resource creation does not clash with @@ -17,27 +22,7 @@ def random_id(): @pytest.fixture(scope="session") def CONFIG(): - return "/tmp/python-gitlab.cfg" - - -@pytest.fixture -def gitlab_cli(script_runner, CONFIG): - """Wrapper fixture to help make test cases less verbose.""" - - def _gitlab_cli(subcommands): - """ - Return a script_runner.run method that takes a default gitlab - command, and subcommands passed as arguments inside test cases. - """ - command = ["gitlab", "--config-file", CONFIG] - - for subcommand in subcommands: - # ensure we get strings (e.g from IDs) - command.append(str(subcommand)) - - return script_runner.run(*command) - - return _gitlab_cli + return os.path.join(TEMP_DIR, "python-gitlab.cfg") @pytest.fixture(scope="session") diff --git a/tools/functional_tests.sh b/tools/functional_tests.sh index 87907c52d..9b91f0f72 100755 --- a/tools/functional_tests.sh +++ b/tools/functional_tests.sh @@ -18,4 +18,4 @@ setenv_script=$(dirname "$0")/build_test_env.sh || exit 1 BUILD_TEST_ENV_AUTO_CLEANUP=true . "$setenv_script" "$@" || exit 1 -pytest "$(dirname "$0")/functional/cli" +pytest --script-launch-mode=subprocess "$(dirname "$0")/functional/cli" diff --git a/tools/python_test_v4.py b/tools/python_test_v4.py index 21faf9e64..7ff97b67f 100644 --- a/tools/python_test_v4.py +++ b/tools/python_test_v4.py @@ -367,17 +367,6 @@ assert len(gm1.issues()) == 0 assert len(gm1.merge_requests()) == 0 -# group variables -group1.variables.create({"key": "foo", "value": "bar"}) -g_v = group1.variables.get("foo") -assert g_v.value == "bar" -g_v.value = "baz" -g_v.save() -g_v = group1.variables.get("foo") -assert g_v.value == "baz" -assert len(group1.variables.list()) == 1 -g_v.delete() -assert len(group1.variables.list()) == 0 # group labels # group1.labels.create({"name": "foo", "description": "bar", "color": "#112233"}) @@ -856,14 +845,6 @@ assert len(admin_project.triggers.list()) == 1 tr1.delete() -# variables -v1 = admin_project.variables.create({"key": "key1", "value": "value1"}) -assert len(admin_project.variables.list()) == 1 -v1.value = "new_value1" -v1.save() -v1 = admin_project.variables.get(v1.key) -assert v1.value == "new_value1" -v1.delete() # branches and merges to_merge = admin_project.branches.create({"branch": "branch1", "ref": "master"}) From ad4b87cb3d6802deea971e6574ae9afe4f352e31 Mon Sep 17 00:00:00 2001 From: Nejc Habjan Date: Sun, 30 Aug 2020 18:17:01 +0200 Subject: [PATCH 4/5] docs(variables): add docs for instance-level variables --- docs/api-objects.rst | 1 + docs/gl_objects/pipelines_and_jobs.rst | 52 ------------- docs/gl_objects/variables.rst | 102 +++++++++++++++++++++++++ 3 files changed, 103 insertions(+), 52 deletions(-) create mode 100644 docs/gl_objects/variables.rst diff --git a/docs/api-objects.rst b/docs/api-objects.rst index 5d5949702..8221f63b8 100644 --- a/docs/api-objects.rst +++ b/docs/api-objects.rst @@ -48,5 +48,6 @@ API examples gl_objects/templates gl_objects/todos gl_objects/users + gl_objects/variables gl_objects/sidekiq gl_objects/wikis diff --git a/docs/gl_objects/pipelines_and_jobs.rst b/docs/gl_objects/pipelines_and_jobs.rst index eb9e23a00..cc4db538f 100644 --- a/docs/gl_objects/pipelines_and_jobs.rst +++ b/docs/gl_objects/pipelines_and_jobs.rst @@ -184,58 +184,6 @@ Delete a schedule variable:: var.delete() -Projects and groups variables -============================= - -You can associate variables to projects and groups to modify the build/job -scripts behavior. - -Reference ---------- - -* v4 API - - + :class:`gitlab.v4.objects.ProjectVariable` - + :class:`gitlab.v4.objects.ProjectVariableManager` - + :attr:`gitlab.v4.objects.Project.variables` - + :class:`gitlab.v4.objects.GroupVariable` - + :class:`gitlab.v4.objects.GroupVariableManager` - + :attr:`gitlab.v4.objects.Group.variables` - -* GitLab API - - + https://docs.gitlab.com/ce/api/project_level_variables.html - + https://docs.gitlab.com/ce/api/group_level_variables.html - -Examples --------- - -List variables:: - - p_variables = project.variables.list() - g_variables = group.variables.list() - -Get a variable:: - - p_var = project.variables.get('key_name') - g_var = group.variables.get('key_name') - -Create a variable:: - - var = project.variables.create({'key': 'key1', 'value': 'value1'}) - var = group.variables.create({'key': 'key1', 'value': 'value1'}) - -Update a variable value:: - - var.value = 'new_value' - var.save() - -Remove a variable:: - - project.variables.delete('key_name') - group.variables.delete('key_name') - # or - var.delete() Jobs ==== diff --git a/docs/gl_objects/variables.rst b/docs/gl_objects/variables.rst new file mode 100644 index 000000000..e6ae4ba98 --- /dev/null +++ b/docs/gl_objects/variables.rst @@ -0,0 +1,102 @@ +############### +CI/CD Variables +############### + +You can configure variables at the instance-level (admin only), or associate +variables to projects and groups, to modify pipeline/job scripts behavior. + + +Instance-level variables +======================== + +This endpoint requires admin access. + +Reference +--------- + +* v4 API + + + :class:`gitlab.v4.objects.Variable` + + :class:`gitlab.v4.objects.VariableManager` + + :attr:`gitlab.Gitlab.variables` + +* GitLab API + + + https://docs.gitlab.com/ce/api/instance_level_ci_variables.html + +Examples +-------- + +List all instance variables:: + + variables = gl.variables.list() + +Get an instance variable by key:: + + variable = gl.variables.get('key_name') + +Create an instance variable:: + + variable = gl.variables.create({'key': 'key1', 'value': 'value1'}) + +Update a variable value:: + + variable.value = 'new_value' + variable.save() + +Remove a variable:: + + gl.variables.delete('key_name') + # or + variable.delete() + +Projects and groups variables +============================= + +Reference +--------- + +* v4 API + + + :class:`gitlab.v4.objects.ProjectVariable` + + :class:`gitlab.v4.objects.ProjectVariableManager` + + :attr:`gitlab.v4.objects.Project.variables` + + :class:`gitlab.v4.objects.GroupVariable` + + :class:`gitlab.v4.objects.GroupVariableManager` + + :attr:`gitlab.v4.objects.Group.variables` + +* GitLab API + + + https://docs.gitlab.com/ce/api/instance_level_ci_variables.html + + https://docs.gitlab.com/ce/api/project_level_variables.html + + https://docs.gitlab.com/ce/api/group_level_variables.html + +Examples +-------- + +List variables:: + + p_variables = project.variables.list() + g_variables = group.variables.list() + +Get a variable:: + + p_var = project.variables.get('key_name') + g_var = group.variables.get('key_name') + +Create a variable:: + + var = project.variables.create({'key': 'key1', 'value': 'value1'}) + var = group.variables.create({'key': 'key1', 'value': 'value1'}) + +Update a variable value:: + + var.value = 'new_value' + var.save() + +Remove a variable:: + + project.variables.delete('key_name') + group.variables.delete('key_name') + # or + var.delete() From 5a56b6b55f761940f80491eddcdcf17d37215cfd Mon Sep 17 00:00:00 2001 From: Nejc Habjan Date: Mon, 31 Aug 2020 22:59:10 +0200 Subject: [PATCH 5/5] chore(test): use pathlib for paths --- tools/functional/conftest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/functional/conftest.py b/tools/functional/conftest.py index e12471b5a..e60fa3915 100644 --- a/tools/functional/conftest.py +++ b/tools/functional/conftest.py @@ -1,5 +1,5 @@ -import os import tempfile +from pathlib import Path from random import randint import pytest @@ -22,7 +22,7 @@ def random_id(): @pytest.fixture(scope="session") def CONFIG(): - return os.path.join(TEMP_DIR, "python-gitlab.cfg") + return Path(TEMP_DIR) / "python-gitlab.cfg" @pytest.fixture(scope="session")