-
Notifications
You must be signed in to change notification settings - Fork 88
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: manage Project and Group Labels via gitlabform (#751)
feat: manage Project and Group Labels via gitlabform --------- Signed-off-by: Tim Knight <tim.knight1@engineering.digital.dwp.gov.uk>
- Loading branch information
1 parent
a362d8e
commit f1be42b
Showing
9 changed files
with
516 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
# Labels | ||
|
||
This section purpose is to manage labels both [project](https://docs.gitlab.com/ee/api/labels.html) and [group](https://docs.gitlab.com/ee/api/group_labels.html) | ||
|
||
## Project Labels | ||
|
||
The keys name are as in the endpoints described in the [GitLab Labels API docs](https://docs.gitlab.com/ee/api/labels.html), f.e. `description`, `color` etc. | ||
|
||
```yaml | ||
projects_and_groups: | ||
group_1/project_1: | ||
labels: | ||
my_label: | ||
color: red | ||
description: hello world | ||
``` | ||
|
||
`enforce` is used to determine whether labels present in GitLab but not the configuration should be deleted or not. Without enabled `enforce: true` we retain any labels not present in the configuration, to support automated tooling which may apply labels based on user's rulesets and work practices, such as for Compliance Frameworks. | ||
|
||
```yaml | ||
projects_and_groups: | ||
group_1/project_1: | ||
labels: | ||
enforce: true | ||
my_label: | ||
color: red | ||
description: hello world | ||
``` | ||
|
||
The same project labels can be applied to all projects in a group using the following syntax: | ||
|
||
```yaml | ||
projects_and_groups: | ||
group_1/*: | ||
labels: | ||
my_label: | ||
color: red | ||
description: hello world | ||
``` | ||
|
||
## Group Labels | ||
|
||
The keys name are as in the endpoints described in the [GitLab Group Labels API docs](https://docs.gitlab.com/ee/api/group_labels.html), f.e. `description`, `color` etc. | ||
|
||
We use `group_labels` as the key within the configuration to disambiguate from labels being applied to the Group and labels being applied to **all Projects** in a Group. | ||
|
||
```yaml | ||
projects_and_groups: | ||
group_1/*: | ||
group_labels: | ||
my_label: | ||
color: red | ||
description: hello world | ||
``` | ||
|
||
`enforce` is used to determine whether labels present in GitLab but not the configuration should be deleted or not. Without enabled `enforce: true` we retain any labels not present in the configuration, to support automated tooling which may apply labels based on user's rulesets and work practices, such as for Compliance Frameworks. | ||
|
||
```yaml | ||
projects_and_groups: | ||
group_1/*: | ||
group_labels: | ||
enforce: true | ||
my_label: | ||
color: red | ||
description: hello world | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
from logging import info | ||
from typing import Dict, List | ||
|
||
|
||
from gitlabform.gitlab import GitLab | ||
from gitlabform.processors.abstract_processor import AbstractProcessor | ||
from gitlabform.processors.util.labels_processor import LabelsProcessor | ||
|
||
from gitlab.v4.objects import Group | ||
|
||
|
||
class GroupLabelsProcessor(AbstractProcessor): | ||
def __init__(self, gitlab: GitLab): | ||
super().__init__("group_labels", gitlab) | ||
self._labels_processor = LabelsProcessor() | ||
|
||
def _process_configuration(self, group_path_and_name: str, configuration: Dict): | ||
configured_labels = configuration.get("group_labels", {}) | ||
|
||
enforce = configuration.get("group_labels|enforce", False) | ||
|
||
# Remove 'enforce' key from the config so that it's not treated as a "label" | ||
if enforce: | ||
configured_labels.pop("enforce") | ||
|
||
group: Group = self.gl.get_group_by_path_cached(group_path_and_name) | ||
|
||
self._labels_processor.process_labels( | ||
configured_labels, enforce, group, self._needs_update | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
from typing import Dict | ||
|
||
from gitlabform.gitlab import GitLab | ||
from gitlabform.processors.abstract_processor import AbstractProcessor | ||
|
||
from gitlab.v4.objects import Project | ||
|
||
from gitlabform.processors.util.labels_processor import LabelsProcessor | ||
|
||
|
||
class ProjectLabelsProcessor(AbstractProcessor): | ||
def __init__(self, gitlab: GitLab): | ||
super().__init__("labels", gitlab) | ||
self._labels_processor = LabelsProcessor() | ||
|
||
def _process_configuration(self, project_and_group: str, configuration: Dict): | ||
configured_labels = configuration.get("labels", {}) | ||
|
||
enforce = configuration.get("labels|enforce", False) | ||
|
||
# Remove 'enforce' key from the config so that it's not treated as a "label" | ||
if enforce: | ||
configured_labels.pop("enforce") | ||
|
||
project: Project = self.gl.get_project_by_path_cached(project_and_group) | ||
|
||
self._labels_processor.process_labels( | ||
configured_labels, enforce, project, self._needs_update | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
from logging import debug, info | ||
from typing import Dict, List, Callable, Union | ||
|
||
from gitlab.base import RESTObject | ||
from gitlab.v4.objects import Group, Project, ProjectLabel, GroupLabel | ||
|
||
|
||
class LabelsProcessor: | ||
|
||
# Groups and Projects share the same API for .labels within python-gitlab | ||
def process_labels( | ||
self, | ||
configured_labels: Dict, | ||
enforce: bool, | ||
group_or_project: Union[ | ||
Group, Project | ||
], # Group | Project -> |: operand not supported in 3.8/3.9 | ||
needs_update: Callable, # self._needs_update passed from AbstractProcessor called process_labels | ||
): | ||
# python-gitlab/python-gitlab#2843 | ||
existing_labels = group_or_project.labels.list(get_all=True) | ||
existing_label_names: List = [] | ||
|
||
if isinstance(group_or_project, Group): | ||
parent_object_type = "Group" | ||
else: | ||
parent_object_type = "Project" | ||
|
||
if existing_labels: | ||
for listed_label in existing_labels: | ||
full_label = group_or_project.labels.get(listed_label.id) | ||
label_name = full_label.name | ||
|
||
if label_name not in configured_labels.keys(): | ||
debug(f"{label_name} not in configured labels") | ||
# only delete labels when enforce is true, because user's maybe automatically applying labels based | ||
# on Repo state, for example: Compliance Framework labels based on language or CI-template status | ||
if enforce: | ||
info(f"Removing {label_name} from {parent_object_type}") | ||
full_label.delete() | ||
else: | ||
configured_label = configured_labels.get(label_name) | ||
existing_label_names.append(label_name) | ||
|
||
if needs_update(full_label.asdict(), configured_label): | ||
self.update_existing_label( | ||
configured_label, | ||
full_label, | ||
parent_object_type, | ||
) | ||
else: | ||
debug(f"No update required for label: {label_name}") | ||
|
||
# add new labels | ||
for label_name in configured_labels.keys(): | ||
if label_name not in existing_label_names: | ||
self.create_new_label( | ||
configured_labels, group_or_project, label_name, parent_object_type | ||
) | ||
|
||
@staticmethod | ||
def update_existing_label( | ||
configured_label, | ||
full_label: Union[GroupLabel, ProjectLabel], # GroupLabel | ProjectLabel | ||
parent_object_type: str, | ||
): | ||
info(f"Updating {full_label.name} on {parent_object_type}") | ||
|
||
# label APIs in python-gitlab do not supply an update() method | ||
for key in configured_label: | ||
full_label.__setattr__(key, configured_label[key]) | ||
|
||
full_label.save() | ||
|
||
@staticmethod | ||
def create_new_label( | ||
configured_labels, | ||
group_or_project: Union[Group, Project], # Group | Project | ||
label_name: str, | ||
parent_object_type: str, | ||
): | ||
label = configured_labels.get(label_name) | ||
info(f"Adding {label_name} to {parent_object_type}") | ||
group_or_project.labels.create({"name": label_name, **label}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
from tests.acceptance import run_gitlabform | ||
|
||
|
||
class TestGroupLabels: | ||
def test__can_add_a_label_to_group(self, gl, group_for_function): | ||
group = gl.groups.get(group_for_function.id) | ||
labels = group.labels.list() | ||
assert len(labels) == 0 | ||
|
||
config_for_labels = f""" | ||
projects_and_groups: | ||
{group.full_path}/*: | ||
group_labels: | ||
test_label: | ||
color: red | ||
description: this is a label | ||
""" | ||
|
||
run_gitlabform(config_for_labels, group_for_function) | ||
|
||
updated_group = gl.groups.get(group.id) | ||
updated_labels = updated_group.labels.list() | ||
assert len(updated_labels) == 1 | ||
|
||
updated_label = updated_labels[0] | ||
assert updated_label.name == "test_label" | ||
assert updated_label.description == "this is a label" | ||
|
||
# text color gets converted to HexCode by GitLab ie red -> #FF0000 | ||
assert updated_label.color == "#FF0000" | ||
|
||
def test__removes_existing_label_when_enforce_is_true(self, gl, group_for_function): | ||
group = gl.groups.get(group_for_function.id) | ||
group.labels.create({"name": "delete_this", "color": "red"}) | ||
labels = group.labels.list() | ||
assert len(labels) == 1 | ||
|
||
config_for_labels = f""" | ||
projects_and_groups: | ||
{group.full_path}/*: | ||
group_labels: | ||
enforce: true | ||
test_label: | ||
color: red | ||
description: this is a label | ||
""" | ||
|
||
run_gitlabform(config_for_labels, group_for_function) | ||
|
||
updated_group = gl.groups.get(group.id) | ||
updated_labels = updated_group.labels.list() | ||
assert len(updated_labels) == 1 | ||
|
||
updated_label = updated_labels[0] | ||
assert updated_label.name == "test_label" | ||
assert updated_label.description == "this is a label" | ||
|
||
# text color gets converted to HexCode by GitLab ie red -> #FF0000 | ||
assert updated_label.color == "#FF0000" | ||
|
||
def test__leaves_existing_label_when_enforce_is_false(self, gl, group_for_function): | ||
group = gl.groups.get(group_for_function.id) | ||
group.labels.create({"name": "delete_this", "color": "red"}) | ||
labels = group.labels.list() | ||
assert len(labels) == 1 | ||
|
||
config_for_labels = f""" | ||
projects_and_groups: | ||
{group.full_path}/*: | ||
group_labels: | ||
test_label: | ||
color: red | ||
description: this is a label | ||
""" | ||
|
||
run_gitlabform(config_for_labels, group_for_function) | ||
|
||
updated_group = gl.groups.get(group.id) | ||
updated_labels = updated_group.labels.list() | ||
assert len(updated_labels) == 2 | ||
|
||
existing_label = updated_labels[0] | ||
assert existing_label.name == "delete_this" | ||
assert existing_label.color == "#FF0000" | ||
|
||
new_label = updated_labels[1] | ||
assert new_label.name == "test_label" | ||
assert new_label.description == "this is a label" | ||
|
||
# text color gets converted to HexCode by GitLab ie red -> #FF0000 | ||
assert new_label.color == "#FF0000" | ||
|
||
def test__updates_existing_label(self, gl, group_for_function): | ||
group = gl.groups.get(group_for_function.id) | ||
created_label = group.labels.create( | ||
{"name": "update_this", "color": "red", "description": "hello world"} | ||
) | ||
|
||
labels = group.labels.list() | ||
assert len(labels) == 1 | ||
|
||
config_for_labels = f""" | ||
projects_and_groups: | ||
{group.full_path}/*: | ||
group_labels: | ||
update_this: | ||
color: blue | ||
description: this is a label | ||
""" | ||
|
||
run_gitlabform(config_for_labels, group_for_function) | ||
|
||
updated_group = gl.groups.get(group.id) | ||
updated_labels = updated_group.labels.list() | ||
assert len(updated_labels) == 1 | ||
|
||
new_label = updated_labels[0] | ||
assert new_label.name == "update_this" | ||
assert new_label.description == "this is a label" | ||
|
||
# text color gets converted to HexCode by GitLab ie blue -> #0000FF | ||
assert new_label.color == "#0000FF" | ||
|
||
# validate same id is being used | ||
assert created_label.id == new_label.id |
Oops, something went wrong.