Repository: python-gitlab/python-gitlab

Issue:
'project-runner get' command should not exist
## Description of the problem, including code/CLI snippet
The library allows to query `/projects/:projectId/runners/:id` URL through `gitlab.v4.objects.ProjectRunnerManager` class, but the official Gitlab API does not describe this endpoint, which does not exists.

Project's runners list :
```bash
$ gitlab -d -o json project-runner list --all --project-id 76644
DEBUG:urllib3.connectionpool:https://gitlab.company.com:443 "GET /api/v4/projects/76644/runners?project_id=76644 HTTP/1.1" 200 None
send: b'GET /api/v4/projects/76644/runners?project_id=76644 HTTP/1.1\r\nHost: gitlab.company.com\r\nUser-Agent: python-gitlab/2.10.1\r\nAccept-Encoding: gzip, deflate\r\nAccept: */*\r\nConnection: keep-alive\r\nPRIVATE-TOKEN: [hidden content]\r\nContent-type: application/json\r\n\r\n'
reply: 'HTTP/1.1 200 OK\r\n'
header: Server: nginx
header: Date: Wed, 22 Sep 2021 18:08:59 GMT
header: Content-Type: application/json
header: Transfer-Encoding: chunked
header: Connection: keep-alive
header: Vary: Accept-Encoding
header: Cache-Control: max-age=0, private, must-revalidate
header: Etag: W/"1ae5eff0bd18cd69fc1aa3325e259422"
header: Link: [hidden content]
header: Vary: Origin
header: X-Content-Type-Options: nosniff
header: X-Frame-Options: SAMEORIGIN
header: X-Gitlab-Feature-Category: continuous_integration
header: X-Next-Page:
header: X-Page: 1
header: X-Per-Page: 20
header: X-Prev-Page:
header: X-Request-Id: 01FG7B4MD17BPMDZXG2XHFFK4Q
header: X-Runtime: 0.072099
header: X-Total: 2
header: X-Total-Pages: 1
header: Strict-Transport-Security: max-age=31536000
header: Referrer-Policy: strict-origin-when-cross-origin
header: Content-Encoding: gzip
[{"id": 7723, "description": "<description>", "ip_address": "<ip address>", "active": true, "is_shared": false, "name": "gitlab-runner", "online": false, "status": "offline"}, {"id": 11737, "description": "shared-gitlab-runner-77d49f579-zpr98", "ip_address": "<ip address>", "active": true, "is_shared": true, "name": "gitlab-runner", "online": true, "status": "online"}]
```

Invalid query :
```bash
$ gitlab -d -o json project-runner get --project-id 76644 --id 7723
DEBUG:urllib3.connectionpool:https://gitlab.company.com:443 "GET /api/v4/projects/76644/runners/7723?project_id=76644 HTTP/1.1" 404 25
Impossible to get object (404: 404 Not Found)
send: b'GET /api/v4/projects/76644/runners/7723?project_id=76644 HTTP/1.1\r\nHost: gitlab.company.com\r\nUser-Agent: python-gitlab/2.10.1\r\nAccept-Encoding: gzip, deflate\r\nAccept: */*\r\nConnection: keep-alive\r\nPRIVATE-TOKEN: [hidden content]\r\nContent-type: application/json\r\n\r\n'
reply: 'HTTP/1.1 404 Not Found\r\n'
header: Server: nginx
header: Date: Wed, 22 Sep 2021 18:11:44 GMT
header: Content-Type: application/json
header: Content-Length: 25
header: Connection: keep-alive
header: Cache-Control: no-cache
header: Vary: Origin
header: X-Content-Type-Options: nosniff
header: X-Frame-Options: SAMEORIGIN
header: X-Gitlab-Feature-Category: not_owned
header: X-Request-Id: 01FG7B9MZ0K19YPZ4MQJ390PK1
header: X-Runtime: 0.019269
```

Valid query (which already exists) : GET /runner/7723
```bash
$ gitlab -d  -o json runner get --id 7723
DEBUG:urllib3.connectionpool:https://gitlab.company.com:443 "GET /api/v4/runners/7723 HTTP/1.1" 200 None
send: b'GET /api/v4/runners/7723 HTTP/1.1\r\nHost: gitlab.company.com\r\nUser-Agent: python-gitlab/2.10.1\r\nAccept-Encoding: gzip, deflate\r\nAccept: */*\r\nConnection: keep-alive\r\nPRIVATE-TOKEN: [hidden content]\r\nContent-type: application/json\r\n\r\n'
reply: 'HTTP/1.1 200 OK\r\n'
header: Server: nginx
header: Date: Wed, 22 Sep 2021 18:11:34 GMT
header: Content-Type: application/json
header: Transfer-Encoding: chunked
header: Connection: keep-alive
header: Vary: Accept-Encoding
header: Cache-Control: max-age=0, private, must-revalidate
header: Etag: W/"7506c48834a201e1d86531647d32b044"
header: Vary: Origin
header: X-Content-Type-Options: nosniff
header: X-Frame-Options: SAMEORIGIN
header: X-Gitlab-Feature-Category: continuous_integration
header: X-Request-Id: 01FG7B9B43ME9BMFFZTFM8A20H
header: X-Runtime: 0.124597
header: Strict-Transport-Security: max-age=31536000
header: Referrer-Policy: strict-origin-when-cross-origin
header: Content-Encoding: gzip
{"id": 7723, "description": "<description>", "ip_address": "<id address>", "active": true, "is_shared": false, "name": "gitlab-runner", "online": false, "status": "offline", "tag_list": [], "run_untagged": true, "locked": false, "maximum_timeout": null, "access_level": "not_protected", "version": "13.11.0", "revision": "7f7a4bb0", "platform": "linux", "architecture": "amd64", "contacted_at": "2021-04-23T13:03:09.789Z", "projects": [projects list], "groups": []}
```

If I correctly understand the library, I think that `ProjectRunnerManager` class should not inherits `GetMixin` class (which comes through `NoUpdateMixin`), but only `ListMixin`, `CreateMixin` and `DeleteMixin` classes (and `RESTManager` class, of course).

The unit tests do not cover the `project-runner get` command (this could have revelead the bug earlier).

Should I open a PR to fix this issue ?

## Expected Behavior
The `gitlab projet-runner get` command should not exist.

## Actual Behavior
The `gitlab projet-runner get` command exists.

## Specifications

  - python-gitlab version: 2.10.1
  - API version you are using (v3/v4): v4
  - Gitlab server version (or gitlab.com): 13.12.1-ee



Discussion:
Sorry for the delay @lgatellier. Yes, this was an oversight when adding that manager. What you describe should fix it as the command and method won't be available. Do you still want to open a PR?
Hi @nejch , thanks for your answer.

I'll open the related PR.

I think GroupRunnerManager is affected too. It should inherits only ListMixin, as there's no Create/Read/Delete on this resource.
Runners API : https://docs.gitlab.com/ee/api/runners.html#list-groups-runners

Resolving patch:
diff --git a/gitlab/v4/objects/runners.py b/gitlab/v4/objects/runners.py
--- a/gitlab/v4/objects/runners.py
+++ b/gitlab/v4/objects/runners.py
@@ -3,9 +3,10 @@
 from gitlab import types
 from gitlab.base import RequiredOptional, RESTManager, RESTObject
 from gitlab.mixins import (
+    CreateMixin,
     CRUDMixin,
+    DeleteMixin,
     ListMixin,
-    NoUpdateMixin,
     ObjectDeleteMixin,
     SaveMixin,
 )
@@ -114,11 +115,11 @@ def verify(self, token, **kwargs):
         self.gitlab.http_post(path, post_data=post_data, **kwargs)
 
 
-class GroupRunner(ObjectDeleteMixin, RESTObject):
+class GroupRunner(RESTObject):
     pass
 
 
-class GroupRunnerManager(NoUpdateMixin, RESTManager):
+class GroupRunnerManager(ListMixin, RESTManager):
     _path = "/groups/%(group_id)s/runners"
     _obj_cls = GroupRunner
     _from_parent_attrs = {"group_id": "id"}
@@ -131,7 +132,7 @@ class ProjectRunner(ObjectDeleteMixin, RESTObject):
     pass
 
 
-class ProjectRunnerManager(NoUpdateMixin, RESTManager):
+class ProjectRunnerManager(CreateMixin, DeleteMixin, ListMixin, RESTManager):
     _path = "/projects/%(project_id)s/runners"
     _obj_cls = ProjectRunner
     _from_parent_attrs = {"project_id": "id"}


Test patch:
diff --git a/tests/unit/objects/test_runners.py b/tests/unit/objects/test_runners.py
--- a/tests/unit/objects/test_runners.py
+++ b/tests/unit/objects/test_runners.py
@@ -143,7 +143,7 @@ def resp_runner_register():
 @pytest.fixture
 def resp_runner_enable():
     with responses.RequestsMock() as rsps:
-        pattern = re.compile(r".*?(projects|groups)/1/runners")
+        pattern = re.compile(r".*?projects/1/runners")
         rsps.add(
             method=responses.POST,
             url=pattern,
@@ -176,7 +176,7 @@ def resp_runner_delete():
 @pytest.fixture
 def resp_runner_disable():
     with responses.RequestsMock() as rsps:
-        pattern = re.compile(r".*?/(groups|projects)/1/runners/6")
+        pattern = re.compile(r".*?/projects/1/runners/6")
         rsps.add(
             method=responses.DELETE,
             url=pattern,
@@ -252,10 +252,6 @@ def test_disable_project_runner(gl: gitlab.Gitlab, resp_runner_disable):
     gl.projects.get(1, lazy=True).runners.delete(6)
 
 
-def test_disable_group_runner(gl: gitlab.Gitlab, resp_runner_disable):
-    gl.groups.get(1, lazy=True).runners.delete(6)
-
-
 def test_enable_project_runner(gl: gitlab.Gitlab, resp_runner_enable):
     runner = gl.projects.get(1, lazy=True).runners.create({"runner_id": 6})
     assert runner.active is True
@@ -263,13 +259,6 @@ def test_enable_project_runner(gl: gitlab.Gitlab, resp_runner_enable):
     assert runner.name == "test-name"
 
 
-def test_enable_group_runner(gl: gitlab.Gitlab, resp_runner_enable):
-    runner = gl.groups.get(1, lazy=True).runners.create({"runner_id": 6})
-    assert runner.active is True
-    assert runner.id == 6
-    assert runner.name == "test-name"
-
-
 def test_verify_runner(gl: gitlab.Gitlab, resp_runner_verify):
     gl.runners.verify("token")