community.general/plugins/modules/gitlab_protected_branch.py
David Phillips f772bcda88
Some checks failed
EOL CI / EOL Sanity (Ⓐ2.16) (push) Has been cancelled
EOL CI / EOL Units (Ⓐ2.16+py2.7) (push) Has been cancelled
EOL CI / EOL Units (Ⓐ2.16+py3.11) (push) Has been cancelled
EOL CI / EOL Units (Ⓐ2.16+py3.6) (push) Has been cancelled
EOL CI / EOL I (Ⓐ2.16+alpine3+py:azp/posix/1/) (push) Has been cancelled
EOL CI / EOL I (Ⓐ2.16+alpine3+py:azp/posix/2/) (push) Has been cancelled
EOL CI / EOL I (Ⓐ2.16+alpine3+py:azp/posix/3/) (push) Has been cancelled
EOL CI / EOL I (Ⓐ2.16+fedora38+py:azp/posix/1/) (push) Has been cancelled
EOL CI / EOL I (Ⓐ2.16+fedora38+py:azp/posix/2/) (push) Has been cancelled
EOL CI / EOL I (Ⓐ2.16+fedora38+py:azp/posix/3/) (push) Has been cancelled
EOL CI / EOL I (Ⓐ2.16+opensuse15+py:azp/posix/1/) (push) Has been cancelled
EOL CI / EOL I (Ⓐ2.16+opensuse15+py:azp/posix/2/) (push) Has been cancelled
EOL CI / EOL I (Ⓐ2.16+opensuse15+py:azp/posix/3/) (push) Has been cancelled
nox / Run extra sanity tests (push) Has been cancelled
gitlab_protected_branch: refactor, add allow_force_push, code_owner_approval_required (#10795)
* gitlab_protected_branch: fix typo

* gitlab_protected_branch: lump parameters into options dictionary

Hardcoding parameter lists gets repetitive. Refactor this module to use
an options dictionary like many other gitlab_* modules. This makes it
cleaner to add new options.

* gitlab_protected_branch: update when possible

Until now, the module deletes and re-creates the protected branch if any
change is detected. This makes sense for the access level parameters, as
these are not easily mutated after creation.

However, in order to add further options which _can_ easily be updated,
we should support updating by default, unless known-immutable parameters
are changing.

* gitlab_protected_branch: add `allow_force_push` option

* gitlab_protected_branch: add `code_owner_approval_required` option

* gitlab_protected_branch: add issues to changelog

* Update changelog.

---------

Co-authored-by: Felix Fontein <felix@fontein.de>
2025-09-08 19:02:40 +02:00

227 lines
8.1 KiB
Python

#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Werner Dijkerman (ikben@werner-dijkerman.nl)
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = r"""
module: gitlab_protected_branch
short_description: Manage protection of existing branches
version_added: 3.4.0
description:
- (un)Marking existing branches for protection.
author:
- "Werner Dijkerman (@dj-wasabi)"
requirements:
- python-gitlab >= 2.3.0
extends_documentation_fragment:
- community.general.auth_basic
- community.general.gitlab
- community.general.attributes
attributes:
check_mode:
support: full
diff_mode:
support: none
options:
state:
description:
- Create or delete protected branch.
default: present
type: str
choices: ["present", "absent"]
project:
description:
- The path and name of the project.
required: true
type: str
name:
description:
- The name of the branch that needs to be protected.
- Can make use a wildcard character for like V(production/*) or just have V(main) or V(develop) as value.
required: true
type: str
merge_access_levels:
description:
- Access levels allowed to merge.
default: maintainer
type: str
choices: ["maintainer", "developer", "nobody"]
push_access_level:
description:
- Access levels allowed to push.
default: maintainer
type: str
choices: ["maintainer", "developer", "nobody"]
allow_force_push:
description:
- Whether or not to allow force pushes to the protected branch.
type: bool
version_added: '11.3.0'
code_owner_approval_required:
description:
- Whether or not to require code owner approval to push.
type: bool
version_added: '11.3.0'
"""
EXAMPLES = r"""
- name: Create protected branch on main
community.general.gitlab_protected_branch:
api_url: https://gitlab.com
api_token: secret_access_token
project: "dj-wasabi/collection.general"
name: main
merge_access_levels: maintainer
push_access_level: nobody
"""
RETURN = r"""
"""
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.api import basic_auth_argument_spec
from ansible_collections.community.general.plugins.module_utils.version import LooseVersion
from ansible_collections.community.general.plugins.module_utils.gitlab import (
auth_argument_spec, gitlab_authentication, gitlab
)
class GitlabProtectedBranch(object):
def __init__(self, module, project, gitlab_instance):
self.repo = gitlab_instance
self._module = module
self.project = self.get_project(project)
self.ACCESS_LEVEL = {
'nobody': gitlab.const.NO_ACCESS,
'developer': gitlab.const.DEVELOPER_ACCESS,
'maintainer': gitlab.const.MAINTAINER_ACCESS
}
def get_project(self, project_name):
return self.repo.projects.get(project_name)
def protected_branch_exist(self, name):
try:
return self.project.protectedbranches.get(name)
except Exception as e:
return False
def create_or_update_protected_branch(self, name, options):
protected_branch_options = {
'name': name,
'allow_force_push': options['allow_force_push'],
'code_owner_approval_required': options['code_owner_approval_required'],
}
protected_branch = self.protected_branch_exist(name=name)
changed = False
if protected_branch and self.can_update(protected_branch, options):
for arg_key, arg_value in protected_branch_options.items():
if arg_value is not None:
if getattr(protected_branch, arg_key) != arg_value:
setattr(protected_branch, arg_key, arg_value)
changed = True
if changed and not self._module.check_mode:
protected_branch.save()
else:
# Set immutable options only on (re)creation
protected_branch_options['merge_access_level'] = options['merge_access_levels']
protected_branch_options['push_access_level'] = options['push_access_level']
if protected_branch:
# Exists, but couldn't update. So, delete first
self.delete_protected_branch(name)
if not self._module.check_mode:
self.project.protectedbranches.create(protected_branch_options)
changed = True
return changed
def can_update(self, protected_branch, options):
# these keys are not set on update the same way they are on creation
configured_merge = options['merge_access_levels']
configured_push = options['push_access_level']
current_merge = protected_branch.merge_access_levels[0]['access_level']
current_push = protected_branch.push_access_levels[0]['access_level']
return ((configured_merge is None or current_merge == configured_merge) and
(configured_push is None or current_push == configured_push))
def delete_protected_branch(self, name):
if self._module.check_mode:
return True
return self.project.protectedbranches.delete(name)
def main():
argument_spec = basic_auth_argument_spec()
argument_spec.update(auth_argument_spec())
argument_spec.update(
project=dict(type='str', required=True),
name=dict(type='str', required=True),
merge_access_levels=dict(type='str', default="maintainer", choices=["maintainer", "developer", "nobody"]),
push_access_level=dict(type='str', default="maintainer", choices=["maintainer", "developer", "nobody"]),
allow_force_push=dict(type='bool'),
code_owner_approval_required=dict(type='bool'),
state=dict(type='str', default="present", choices=["absent", "present"]),
)
module = AnsibleModule(
argument_spec=argument_spec,
mutually_exclusive=[
['api_username', 'api_token'],
['api_username', 'api_oauth_token'],
['api_username', 'api_job_token'],
['api_token', 'api_oauth_token'],
['api_token', 'api_job_token'],
],
required_together=[
['api_username', 'api_password'],
],
required_one_of=[
['api_username', 'api_token', 'api_oauth_token', 'api_job_token']
],
supports_check_mode=True
)
# check prerequisites and connect to gitlab server
gitlab_instance = gitlab_authentication(module)
project = module.params['project']
name = module.params['name']
merge_access_levels = module.params['merge_access_levels']
push_access_level = module.params['push_access_level']
state = module.params['state']
gitlab_version = gitlab.__version__
if LooseVersion(gitlab_version) < LooseVersion('2.3.0'):
module.fail_json(msg="community.general.gitlab_protected_branch requires python-gitlab Python module >= 2.3.0 (installed version: [%s])."
" Please upgrade python-gitlab to version 2.3.0 or above." % gitlab_version)
this_gitlab = GitlabProtectedBranch(module=module, project=project, gitlab_instance=gitlab_instance)
p_branch = this_gitlab.protected_branch_exist(name=name)
options = {
"merge_access_levels": this_gitlab.ACCESS_LEVEL[merge_access_levels],
"push_access_level": this_gitlab.ACCESS_LEVEL[push_access_level],
"allow_force_push": module.params["allow_force_push"],
"code_owner_approval_required": module.params["code_owner_approval_required"],
}
if state == "present":
changed = this_gitlab.create_or_update_protected_branch(name, options)
module.exit_json(changed=changed, msg="Created or updated the protected branch.")
elif p_branch and state == "absent":
this_gitlab.delete_protected_branch(name=name)
module.exit_json(changed=True, msg="Deleted the protected branch.")
module.exit_json(changed=False, msg="No changes are needed.")
if __name__ == '__main__':
main()