From 5a97e57a191d102213d1f6657cc8682c13c1ac35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Herv=C3=A9=20Quatremain?= Date: Sat, 23 Nov 2024 12:12:02 +0100 Subject: [PATCH] New auto-pruning modules (#16) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deprecate the auto_prune_method and auto_prune_value parameters in favor of the new quay_organization_prune and quay_repository_prune modules. --------- Co-authored-by: Hervé Quatremain --- CHANGELOG.rst | 19 ++ README.md | 2 + changelogs/changelog.yaml | 19 ++ galaxy.yml | 2 +- meta/runtime.yml | 2 + plugins/doc_fragments/autoprune.py | 47 +-- plugins/doc_fragments/autoprune_deprecated.py | 56 ++++ plugins/module_utils/api_module.py | 65 ++++ plugins/modules/quay_default_perm.py | 4 +- plugins/modules/quay_organization.py | 28 +- plugins/modules/quay_organization_prune.py | 267 ++++++++++++++++ plugins/modules/quay_proxy_cache.py | 2 +- plugins/modules/quay_quota.py | 2 +- plugins/modules/quay_repository.py | 25 +- plugins/modules/quay_repository_prune.py | 297 ++++++++++++++++++ plugins/modules/quay_team.py | 2 +- plugins/modules/quay_team_ldap.py | 2 +- plugins/modules/quay_team_oidc.py | 2 +- roles/quay_org/README.md | 21 +- roles/quay_org/defaults/main.yml | 8 +- roles/quay_org/meta/argument_specs.yml | 100 ++++++ roles/quay_org/tasks/main.yml | 3 + roles/quay_org/tasks/organization_prune.yml | 15 + roles/quay_org/tasks/repositories.yml | 16 + roles/quay_org/tests/test.yml | 16 +- tests/docker-compose.yml | 2 +- .../quay_organization_prune/meta/main.yml | 4 + .../quay_organization_prune/tasks/main.yml | 217 +++++++++++++ .../quay_repository_prune/meta/main.yml | 4 + .../quay_repository_prune/tasks/main.yml | 235 ++++++++++++++ .../targets/role_quay_org/tasks/main.yml | 17 +- 31 files changed, 1430 insertions(+), 71 deletions(-) create mode 100644 plugins/doc_fragments/autoprune_deprecated.py create mode 100644 plugins/modules/quay_organization_prune.py create mode 100644 plugins/modules/quay_repository_prune.py create mode 100644 roles/quay_org/tasks/organization_prune.yml create mode 100644 tests/integration/targets/quay_organization_prune/meta/main.yml create mode 100644 tests/integration/targets/quay_organization_prune/tasks/main.yml create mode 100644 tests/integration/targets/quay_repository_prune/meta/main.yml create mode 100644 tests/integration/targets/quay_repository_prune/tasks/main.yml diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 5dce2b7..608b1a7 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,6 +4,25 @@ Quay Container Registry Collection Release Notes .. contents:: Topics +v2.4.0 +====== + +Release Summary +--------------- + +New ``infra.quay_configuration.quay_organization_prune`` and ``infra.quay_configuration.quay_repository_prune`` modules. + +Deprecated Features +------------------- + +- The ``auto_prune_method`` and ``auto_prune_value`` parameters of the ``infra.quay_configuration.quay_organization`` and ``infra.quay_configuration.quay_repository`` modules are deprecated in favor of the new``infra.quay_configuration.quay_organization_prune`` and ``infra.quay_configuration.quay_repository_prune`` modules. + +New Modules +----------- + +- infra.quay_configuration.quay_organization_prune - Manage auto-pruning policies for organizations and user namespaces +- infra.quay_configuration.quay_repository_prune - Manage auto-pruning policies for repositories + v2.2.0 ====== diff --git a/README.md b/README.md index 97e1c2c..b022094 100644 --- a/README.md +++ b/README.md @@ -30,10 +30,12 @@ Name | Description `quay_message` | Manage Quay Container Registry global messages `quay_notification` | Manage Quay Container Registry repository notifications `quay_organization` | Manage Quay Container Registry organizations +`quay_organization_prune` | Manage auto-pruning policies for organizations and user namespaces `quay_proxy_cache` | Manage Quay Container Registry proxy cache configurations `quay_quota` | Manage Quay Container Registry organizations quota `quay_repository` | Manage Quay Container Registry repositories `quay_repository_mirror` | Manage Quay Container Registry repository mirror configurations +`quay_repository_prune` | Manage auto-pruning policies for repositories `quay_robot` | Manage Quay Container Registry robot accounts `quay_tag` | Manage Quay Container Registry image tags `quay_tag_info` | Gather information about tags in a Quay Container Registry repository diff --git a/changelogs/changelog.yaml b/changelogs/changelog.yaml index 216ed26..adeb8d1 100644 --- a/changelogs/changelog.yaml +++ b/changelogs/changelog.yaml @@ -254,3 +254,22 @@ releases: fragments: - PR5-v2.2.0-summary.yml release_date: '2024-08-05' + 2.4.0: + changes: + deprecated_features: + - The ``auto_prune_method`` and ``auto_prune_value`` parameters of the ``infra.quay_configuration.quay_organization`` + and ``infra.quay_configuration.quay_repository`` modules are deprecated in + favor of the new``infra.quay_configuration.quay_organization_prune`` and ``infra.quay_configuration.quay_repository_prune`` + modules. + release_summary: New ``infra.quay_configuration.quay_organization_prune`` and + ``infra.quay_configuration.quay_repository_prune`` modules. + fragments: + - PR16-v2.4.0-summary.yml + modules: + - description: Manage auto-pruning policies for organizations and user namespaces + name: quay_organization_prune + namespace: '' + - description: Manage auto-pruning policies for repositories + name: quay_repository_prune + namespace: '' + release_date: '2024-11-23' diff --git a/galaxy.yml b/galaxy.yml index ffa7a19..4923d25 100644 --- a/galaxy.yml +++ b/galaxy.yml @@ -1,7 +1,7 @@ --- namespace: infra name: quay_configuration -version: 2.2.0 +version: 2.4.0 readme: README.md authors: - Hervé Quatremain diff --git a/meta/runtime.yml b/meta/runtime.yml index f3c1b32..b63fabc 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -11,10 +11,12 @@ action_groups: - quay_manifest_label - quay_message - quay_notification + - quay_organization_prune - quay_organization - quay_proxy_cache - quay_quota - quay_repository_mirror + - quay_repository_prune - quay_repository - quay_robot - quay_tag_info diff --git a/plugins/doc_fragments/autoprune.py b/plugins/doc_fragments/autoprune.py index e86b003..8d42ea6 100644 --- a/plugins/doc_fragments/autoprune.py +++ b/plugins/doc_fragments/autoprune.py @@ -12,32 +12,41 @@ class ModuleDocFragment(object): # Ansible Galaxy documentation fragment DOCUMENTATION = r""" options: - auto_prune_method: + method: description: - Method to use for the auto-pruning tags policy. - - If V(none), then the module ensures that no policy is in place. The - tags are not pruned. - If V(tags), then the policy keeps only the number of tags that you - specify in O(auto_prune_value). + specify in O(value). - If V(date), then the policy deletes the tags older than the time period - that you specify in O(auto_prune_value). - - O(auto_prune_value) is required when O(auto_prune_method) is V(tags) or - V(date). + that you specify in O(value). + required: true type: str - choices: [none, tags, date] - auto_prune_value: + choices: [tags, date] + value: description: - - Number of tags to keep when O(auto_prune_method) is V(tags). + - Number of tags to keep when O(method) is V(tags). The value must be 1 or more. - - Period of time when O(auto_prune_method) is V(date). The value must be 1 - or more, and must be followed by a suffix; s (for second), m (for - minute), h (for hour), d (for day), or w (for week). - - O(auto_prune_method) is required when O(auto_prune_value) is set. + - Period of time when O(method) is V(date). The value must be 1 or more, + and must be followed by a suffix; s (for second), m (for minute), h + (for hour), d (for day), or w (for week). + required: true type: str + tag_pattern: + description: + - Regular expression to select the tags to process. + - If you do not set the parameter, then Quay processes all the tags. + type: str + tag_pattern_matches: + description: + - If V(true), then Quay processes the tags matching the O(tag_pattern) + parameter. + - If V(false), then Quay excludes the tags matching the O(tag_pattern) + parameter. + - V(true) by default. + type: bool + default: true notes: - - Your Quay administrator must enable the auto-prune capability of your Quay - installation (C(FEATURE_AUTO_PRUNE) in C(config.yaml)) to use the - O(auto_prune_method) and O(auto_prune_value) parameters. - - Using O(auto_prune_method) and O(auto_prune_value) requires Quay version - 3.11 or later. + - Your Quay administrator must enable the auto-pruning capability of your + Quay installation (C(FEATURE_AUTO_PRUNE) in C(config.yaml)). + - Auto-pruning requires Quay version 3.13 or later. """ diff --git a/plugins/doc_fragments/autoprune_deprecated.py b/plugins/doc_fragments/autoprune_deprecated.py new file mode 100644 index 0000000..7a7d502 --- /dev/null +++ b/plugins/doc_fragments/autoprune_deprecated.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- + +# Copyright: (c) 2024 Hervé Quatremain +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + + +class ModuleDocFragment(object): + # Ansible Galaxy documentation fragment + DOCUMENTATION = r""" +options: + auto_prune_method: + description: + - The O(auto_prune_method) parameter is deprecated and will be removed in + future versions of the collection. + Use the M(infra.quay_configuration.quay_organization_prune) and the + M(infra.quay_configuration.quay_repository_prune) modules instead. + - Method to use for the auto-pruning tags policy. + - If V(none), then the module ensures that no policy is in place. The + tags are not pruned. + If several policies are available, then the module removes them all. + - If V(tags), then the policy keeps only the number of tags that you + specify in O(auto_prune_value). + - If V(date), then the policy deletes the tags older than the time period + that you specify in O(auto_prune_value). + - O(auto_prune_value) is required when O(auto_prune_method) is V(tags) or + V(date). + type: str + choices: [none, tags, date] + auto_prune_value: + description: + - The O(auto_prune_value) parameter is deprecated and will be removed in + future versions of the collection. + Use the M(infra.quay_configuration.quay_organization_prune) and the + M(infra.quay_configuration.quay_repository_prune) modules instead. + - Number of tags to keep when O(auto_prune_method) is V(tags). + The value must be 1 or more. + - Period of time when O(auto_prune_method) is V(date). The value must be 1 + or more, and must be followed by a suffix; s (for second), m (for + minute), h (for hour), d (for day), or w (for week). + - O(auto_prune_method) is required when O(auto_prune_value) is set. + type: str +notes: + - The O(auto_prune_method) and O(auto_prune_value) parameters are deprecated + and will be removed in future versions of the collection. + Use the M(infra.quay_configuration.quay_organization_prune) and the + M(infra.quay_configuration.quay_repository_prune) modules instead. + - Your Quay administrator must enable the auto-prune capability of your Quay + installation (C(FEATURE_AUTO_PRUNE) in C(config.yaml)) to use the + O(auto_prune_method) and O(auto_prune_value) parameters. + - Using O(auto_prune_method) and O(auto_prune_value) requires Quay version + 3.11 or later. +""" diff --git a/plugins/module_utils/api_module.py b/plugins/module_utils/api_module.py index 723dce4..b275d25 100644 --- a/plugins/module_utils/api_module.py +++ b/plugins/module_utils/api_module.py @@ -1354,6 +1354,71 @@ def get_tags(self, namespace, repository, tag=None, digest=None, only_active_tag break return tag_list + def process_prune_parameters( + self, method, value, tag_pattern=None, tag_pattern_matches=True + ): + """Return the prune parameters in a dictionary ready for the API. + + :param method: The prune method: "tags", or "date". + :type method: str + :param value: The pruning criteria, which depends on the method. It can + be a number of tags, or a period of time. + :type value: str + :param tag_pattern: A regular expression that is used to select the tags + to purge. + :type tag_pattern: str + :param tag_pattern_matches: If ``True``, then the tags matching + :py:attribute:``tag_pattern`` are processed. + If ``False``, then the tags matching + :py:attribute:``tag_pattern`` are excluded. + :type tag_pattern_matches: bool + + :return: The prune parameters ready to be used for a call to the API. + For example:: + + { + "method": "creation_date", + "value": "7d", + "tagPattern": "dev.*", + "tagPatternMatches": True + } + """ + if method == "tags": + try: + auto_prune_value = int(value) + except ValueError: + self.fail_json( + msg=( + "Wrong format for the `value' parameter:" + " {auto_prune_value} is not a positive integer." + ).format(auto_prune_value=value) + ) + if auto_prune_value <= 0: + self.fail_json( + msg=( + "Wrong format for the `value' parameter:" + " {auto_prune_value} is not a positive integer." + ).format(auto_prune_value=value) + ) + data = {"method": "number_of_tags", "value": auto_prune_value} + else: # method == "date": + auto_prune_value = "".join(value.split()) + if not re.match(r"[1-9]\d*[smhdw]$", auto_prune_value): + self.fail_json( + msg=( + "Wrong format for the `value' parameter:" + " {auto_prune_value} is not a positive integer followed by" + " the s, m, h, d, or w suffix." + ).format(auto_prune_value=value) + ) + data = {"method": "creation_date", "value": auto_prune_value} + if tag_pattern: + data["tagPattern"] = tag_pattern + data["tagPatternMatches"] = ( + tag_pattern_matches if tag_pattern_matches is not None else True + ) + return data + class APIModuleNoAuth(APIModule): AUTH_ARGSPEC = dict( diff --git a/plugins/modules/quay_default_perm.py b/plugins/modules/quay_default_perm.py index c336346..b9ea601 100644 --- a/plugins/modules/quay_default_perm.py +++ b/plugins/modules/quay_default_perm.py @@ -35,8 +35,8 @@ options: organization: description: - - Name of the organization for the default permission. - That organization must exist. + - Name of the organization for the default permission. This organization + must exist. required: true type: str name: diff --git a/plugins/modules/quay_organization.py b/plugins/modules/quay_organization.py index 76f3ff4..f08add7 100644 --- a/plugins/modules/quay_organization.py +++ b/plugins/modules/quay_organization.py @@ -72,11 +72,6 @@ default: present choices: [absent, present] notes: - - Your Quay administrator must enable the auto-prune capability of your Quay - installation (C(FEATURE_AUTO_PRUNE) in C(config.yaml)) to use the - O(auto_prune_method) and O(auto_prune_value) parameters. - - Using O(auto_prune_method) and O(auto_prune_value) requires Quay version - 3.11 or later. - The token that you provide in O(quay_token) must have the "Administer Organization" and "Administer User" permissions. - To rename organizations, the token must also have the "Super User Access" @@ -93,7 +88,7 @@ - ansible.builtin.action_common_attributes - infra.quay_configuration.auth - infra.quay_configuration.auth.login - - infra.quay_configuration.autoprune + - infra.quay_configuration.autoprune_deprecated """ EXAMPLES = r""" @@ -102,8 +97,6 @@ name: production email: prodlist@example.com time_machine_expiration: "7d" - auto_prune_method: tags - auto_prune_value: 20 state: present quay_host: https://quay.example.com quay_token: vgfH9zH5q6eV16Con7SvDQYSr0KPYQimMHVehZv7 @@ -144,8 +137,14 @@ def main(): new_name=dict(), email=dict(), time_machine_expiration=dict(choices=list(tm_allowed_values.keys())), - auto_prune_method=dict(choices=["none", "tags", "date"]), - auto_prune_value=dict(), + auto_prune_method=dict( + choices=["none", "tags", "date"], + removed_at_date="2025-12-01", + removed_from_collection="infra.quay_configuration", + ), + auto_prune_value=dict( + removed_at_date="2025-12-01", removed_from_collection="infra.quay_configuration" + ), state=dict(choices=["present", "absent"], default="present"), ) @@ -330,15 +329,14 @@ def main(): except (TypeError, IndexError): policies = [] - # Removing the auto-prune policies (the UI only manages one policy, but - # the backend seems to allow several policies) + # Removing all the auto-pruning policies if auto_prune_method == "none": deleted = False for policy in policies: uuid = policy.get("uuid") if module.delete( uuid, - "organization auto-prune policy", + "organization auto-pruning policy", name, "organization/{orgname}/autoprunepolicy/{uuid}", auto_exit=False, @@ -366,7 +364,7 @@ def main(): # then create the policy if len(policies) == 0 or policies[0].get("uuid") is None: module.create( - "organization auto-prune policy", + "organization auto-pruning policy", name, "organization/{orgname}/autoprunepolicy/", new_policy, @@ -379,7 +377,7 @@ def main(): uuid = policies[0]["uuid"] new_policy["uuid"] = uuid module.unconditional_update( - "organization auto-prune policy", + "organization auto-pruning policy", name, "organization/{orgname}/autoprunepolicy/{uuid}", new_policy, diff --git a/plugins/modules/quay_organization_prune.py b/plugins/modules/quay_organization_prune.py new file mode 100644 index 0000000..9a7a1b0 --- /dev/null +++ b/plugins/modules/quay_organization_prune.py @@ -0,0 +1,267 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2024 Hervé Quatremain +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +# For accessing the API documentation from a running system, use the swagger-ui +# container image: +# +# $ podman run -p 8888:8080 --name=swag -d --rm \ +# -e API_URL=http://your.quay.installation:8080/api/v1/discovery \ +# docker.io/swaggerapi/swagger-ui +# +# (replace the hostname and port in API_URL with your own installation) +# +# And then navigate to http://localhost:8888 + + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + + +DOCUMENTATION = r""" +--- +module: quay_organization_prune +short_description: Manage auto-pruning policies for organizations and user namespaces +description: + - Create or delete auto-pruning policies for organizations and personal + namespaces in Quay Container Registry. +version_added: '2.4.0' +author: Hervé Quatremain (@herve4m) +options: + namespace: + description: + - Organization or personal namespace. This namespace must exist. + required: true + type: str + append: + description: + - If V(true), then add the auto-pruning policy to the existing policies. + - If V(false), then the module deletes all the existing auto-pruning + policies before adding the specified policy. + type: bool + default: true + state: + description: + - If V(absent), then the module deletes the auto-pruning policy that + matches the provided parameters. + - The module does not fail if the policy does not exist, because the + state is already as expected. + - If V(present), then the module creates the auto-pruning policy if it + does not already exist. + type: str + default: present + choices: [absent, present] +notes: + - The token that you provide in O(quay_token) must have the "Administer + Organization" permission. +attributes: + check_mode: + support: full + diff_mode: + support: none + platform: + support: full + platforms: all +extends_documentation_fragment: + - ansible.builtin.action_common_attributes + - infra.quay_configuration.auth + - infra.quay_configuration.auth.login + - infra.quay_configuration.autoprune +""" + +EXAMPLES = r""" +- name: Ensure the organization keeps only five unstable images + infra.quay_configuration.quay_organization_prune: + namespace: production + method: tags + value: 5 + # Auto-pruning tags that contain "unstable" in their names + tag_pattern: "unstable" + tag_pattern_matches: true + state: present + quay_host: https://quay.example.com + quay_token: vgfH9zH5q6eV16Con7SvDQYSr0KPYQimMHVehZv7 + +- name: Ensure the organization also prunes all tags older that seven weeks + infra.quay_configuration.quay_organization_prune: + namespace: production + method: date + value: 7w + state: present + quay_host: https://quay.example.com + quay_token: vgfH9zH5q6eV16Con7SvDQYSr0KPYQimMHVehZv7 + +- name: Ensure the organization has only the defined auto-pruning policy + infra.quay_configuration.quay_organization_prune: + namespace: development + method: date + value: 8d + tag_pattern: "nightly" + append: false + state: present + quay_host: https://quay.example.com + quay_token: vgfH9zH5q6eV16Con7SvDQYSr0KPYQimMHVehZv7 + +- name: Ensure the auto-pruning policy is removed + infra.quay_configuration.quay_organization_prune: + namespace: development + method: date + value: 8d + tag_pattern: "nightly" + state: absent + quay_host: https://quay.example.com + quay_token: vgfH9zH5q6eV16Con7SvDQYSr0KPYQimMHVehZv7 + +- name: Ensure an auto-pruning policy exists in lvasquez's personal namespace + infra.quay_configuration.quay_organization_prune: + namespace: lvasquez + method: date + value: 8d + tag_pattern: "nightly" + state: present + quay_host: https://quay.example.com + quay_token: vgfH9zH5q6eV16Con7SvDQYSr0KPYQimMHVehZv7 +""" + +RETURN = r""" +id: + description: Internal identifier of the auto-pruning policy. + type: str + returned: always + sample: 45b4cc8b-178b-4ad4-bd33-75e3cce5e889 +""" + +from ..module_utils.api_module import APIModule + + +def main(): + argument_spec = dict( + namespace=dict(required=True), + append=dict(type="bool", default=True), + method=dict(choices=["tags", "date"], required=True), + value=dict(required=True), + tag_pattern=dict(), + tag_pattern_matches=dict(type="bool", default=True), + state=dict(choices=["present", "absent"], default="present"), + ) + + # Create a module for ourselves + module = APIModule(argument_spec=argument_spec, supports_check_mode=True) + + # Extract our parameters + namespace = module.params.get("namespace") + append = module.params.get("append") + method = module.params.get("method") + value = module.params.get("value") + tag_pattern = module.params.get("tag_pattern") + tag_pattern_matches = module.params.get("tag_pattern_matches") + state = module.params.get("state") + + # Convert the parameters to a dictionary that can be used with the API + data = module.process_prune_parameters(method, value, tag_pattern, tag_pattern_matches) + + # Check whether namespace exists (organization or user account) + if not module.get_namespace(namespace): + if state == "absent": + module.exit_json(changed=False) + module.fail_json( + msg="The {orgname} organization or personal namespace does not exist.".format( + orgname=namespace + ) + ) + + # Get the auto-pruning policies for the organization + # + # GET /api/v1/organization/{orgname}/autoprunepolicy/ + # + # { + # "policies": [ + # { + # "uuid": "dc84065e-9e9c-43e9-9224-6151c80219b9", + # "method": "creation_date", + # "value": "10w", + # "tagPattern": "dev.*", + # "tagPatternMatches": true + # }, + # { + # "uuid": "b0515264-91da-46c1-829d-11230a9721a8", + # "method": "number_of_tags", + # "value": 20, + # "tagPattern": "prod.*", + # "tagPatternMatches": false + # }, + # { + # "uuid": "71fd827c-6dec-4ecd-ac92-a200e821afa9", + # "method": "number_of_tags", + # "value": 25, + # "tagPattern": null, + # "tagPatternMatches": true + # } + # ] + # } + policies = module.get_object_path( + "organization/{orgname}/autoprunepolicy/", orgname=namespace + ) + + # Finding a matching auto-pruning policy + policy_details = None + if policies: + for policy in policies.get("policies", []): + if ( + policy.get("method") == data.get("method") + and policy.get("value") == data.get("value") + and policy.get("tagPattern") == data.get("tagPattern") + and policy.get("tagPatternMatches") == data.get("tagPatternMatches", True) + ): + policy_details = policy + break + + # Remove the auto-pruning policy + if state == "absent": + module.delete( + policy_details, + "auto-pruning policy", + method, + "organization/{orgname}/autoprunepolicy/{uuid}", + orgname=namespace, + uuid=policy_details.get("uuid", "") if policy_details else "", + ) + + if append and policy_details: + module.exit_json(changed=False, id=policy_details.get("uuid")) + + # Remove all the auto-pruning policies, except the one that the user + # specifies + if not append: + deletions = False + for policy in policies.get("policies", []): + if not policy_details or (policy_details.get("uuid") != policy.get("uuid")): + module.delete( + policy, + "auto-pruning policy", + policy.get("method"), + "organization/{orgname}/autoprunepolicy/{uuid}", + auto_exit=False, + orgname=namespace, + uuid=policy.get("uuid"), + ) + deletions = True + if policy_details: + module.exit_json(changed=deletions, id=policy_details.get("uuid")) + + resp = module.create( + "auto-pruning policy", + method, + "organization/{orgname}/autoprunepolicy/", + data, + auto_exit=False, + orgname=namespace, + ) + module.exit_json(changed=True, id=resp.get("uuid")) + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/quay_proxy_cache.py b/plugins/modules/quay_proxy_cache.py index e13ab8a..4f3abc6 100644 --- a/plugins/modules/quay_proxy_cache.py +++ b/plugins/modules/quay_proxy_cache.py @@ -33,7 +33,7 @@ organization: description: - Name of the organization in which to create the proxy cache - configuration. That organization must exist. + configuration. This organization must exist. required: true type: str registry: diff --git a/plugins/modules/quay_quota.py b/plugins/modules/quay_quota.py index ed99571..3dada3f 100644 --- a/plugins/modules/quay_quota.py +++ b/plugins/modules/quay_quota.py @@ -32,7 +32,7 @@ options: organization: description: - - Name of the organization. That organization must exist. + - Name of the organization. This organization must exist. required: true type: str quota: diff --git a/plugins/modules/quay_repository.py b/plugins/modules/quay_repository.py index 946d250..e6d2454 100644 --- a/plugins/modules/quay_repository.py +++ b/plugins/modules/quay_repository.py @@ -133,7 +133,7 @@ - ansible.builtin.action_common_attributes - infra.quay_configuration.auth - infra.quay_configuration.auth.login - - infra.quay_configuration.autoprune + - infra.quay_configuration.autoprune_deprecated """ EXAMPLES = r""" @@ -200,12 +200,10 @@ quay_host: https://quay.example.com quay_token: vgfH9zH5q6eV16Con7SvDQYSr0KPYQimMHVehZv7 -- name: Ensure the repository has a star and tags older that 4 weeks are pruned +- name: Ensure the repository has a star infra.quay_configuration.quay_repository: name: production/smallimage star: true - auto_prune_method: date - auto_prune_value: 4w state: present quay_host: https://quay.example.com quay_token: vgfH9zH5q6eV16Con7SvDQYSr0KPYQimMHVehZv7 @@ -245,8 +243,14 @@ def main(): append=dict(type="bool", default=True), star=dict(type="bool"), repo_state=dict(choices=["NORMAL", "READ_ONLY", "MIRROR"]), - auto_prune_method=dict(choices=["none", "tags", "date"]), - auto_prune_value=dict(), + auto_prune_method=dict( + choices=["none", "tags", "date"], + removed_at_date="2025-12-01", + removed_from_collection="infra.quay_configuration", + ), + auto_prune_value=dict( + removed_at_date="2025-12-01", removed_from_collection="infra.quay_configuration" + ), state=dict(choices=["present", "absent"], default="present"), ) @@ -482,15 +486,14 @@ def main(): except (TypeError, IndexError): policies = [] - # Removing the auto-prune policies (the UI only manages one policy, but - # the backend seems to allow several policies) + # Removing all the auto-pruning policies if auto_prune_method == "none": deleted = False for policy in policies: uuid = policy.get("uuid") if module.delete( uuid, - "repository auto-prune policy", + "repository auto-pruning policy", full_repo_name, "repository/{full_repo_name}/autoprunepolicy/{uuid}", auto_exit=False, @@ -518,7 +521,7 @@ def main(): # then create the policy if len(policies) == 0 or policies[0].get("uuid") is None: module.create( - "repository auto-prune policy", + "repository auto-pruning policy", full_repo_name, "repository/{full_repo_name}/autoprunepolicy/", new_policy, @@ -531,7 +534,7 @@ def main(): uuid = policies[0]["uuid"] new_policy["uuid"] = uuid module.unconditional_update( - "repository auto-prune policy", + "repository auto-pruning policy", full_repo_name, "repository/{full_repo_name}/autoprunepolicy/{uuid}", new_policy, diff --git a/plugins/modules/quay_repository_prune.py b/plugins/modules/quay_repository_prune.py new file mode 100644 index 0000000..4bacd73 --- /dev/null +++ b/plugins/modules/quay_repository_prune.py @@ -0,0 +1,297 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2024 Hervé Quatremain +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +# For accessing the API documentation from a running system, use the swagger-ui +# container image: +# +# $ podman run -p 8888:8080 --name=swag -d --rm \ +# -e API_URL=http://your.quay.installation:8080/api/v1/discovery \ +# docker.io/swaggerapi/swagger-ui +# +# (replace the hostname and port in API_URL with your own installation) +# +# And then navigate to http://localhost:8888 + + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + + +DOCUMENTATION = r""" +--- +module: quay_repository_prune +short_description: Manage auto-pruning policies for repositories +description: + - Create or delete auto-pruning policies for repositories in Quay Container + Registry. +version_added: '2.4.0' +author: Hervé Quatremain (@herve4m) +options: + repository: + description: + - Name of the existing repository to configure. The format for the name is + C(namespace)/C(shortname). The namespace can be an organization or a + personal namespace. + - If you omit the namespace part in the name, then the module looks for + the repository in your personal namespace. + required: true + type: str + append: + description: + - If V(true), then add the auto-pruning policy to the existing policies. + - If V(false), then the module deletes all the existing auto-pruning + policies before adding the specified policy. + type: bool + default: true + state: + description: + - If V(absent), then the module deletes the auto-pruning policy that + matches the provided parameters. + - The module does not fail if the policy does not exist, because the + state is already as expected. + - If V(present), then the module creates the auto-pruning policy if it + does not already exist. + type: str + default: present + choices: [absent, present] +notes: + - The token that you provide in O(quay_token) must have the "Administer + Repositories" permission. +attributes: + check_mode: + support: full + diff_mode: + support: none + platform: + support: full + platforms: all +extends_documentation_fragment: + - ansible.builtin.action_common_attributes + - infra.quay_configuration.auth + - infra.quay_configuration.auth.login + - infra.quay_configuration.autoprune +""" + +EXAMPLES = r""" +- name: Ensure the repository keeps only five unstable images + infra.quay_configuration.quay_repository_prune: + repository: production/smallimage + method: tags + value: 5 + # Auto-pruning tags that contain "unstable" in their names + tag_pattern: "unstable" + tag_pattern_matches: true + state: present + quay_host: https://quay.example.com + quay_token: vgfH9zH5q6eV16Con7SvDQYSr0KPYQimMHVehZv7 + +- name: Ensure the repository also prunes all tags older that seven weeks + infra.quay_configuration.quay_repository_prune: + repository: production/smallimage + method: date + value: 7w + state: present + quay_host: https://quay.example.com + quay_token: vgfH9zH5q6eV16Con7SvDQYSr0KPYQimMHVehZv7 + +- name: Ensure the repository has only the defined auto-pruning policy + infra.quay_configuration.quay_repository_prune: + repository: development/frontend + method: date + value: 8d + tag_pattern: "nightly" + append: false + state: present + quay_host: https://quay.example.com + quay_token: vgfH9zH5q6eV16Con7SvDQYSr0KPYQimMHVehZv7 + +- name: Ensure the auto-pruning policy is removed + infra.quay_configuration.quay_repository_prune: + repository: development/frontend + method: date + value: 8d + tag_pattern: "nightly" + state: absent + quay_host: https://quay.example.com + quay_token: vgfH9zH5q6eV16Con7SvDQYSr0KPYQimMHVehZv7 + +- name: Ensure an auto-pruning policy exists for lvasquez's test repository + infra.quay_configuration.quay_repository_prune: + repository: lvasquez/test + method: date + value: 8d + tag_pattern: "nightly" + state: present + quay_host: https://quay.example.com + quay_token: vgfH9zH5q6eV16Con7SvDQYSr0KPYQimMHVehZv7 +""" + +RETURN = r""" +id: + description: Internal identifier of the auto-pruning policy. + type: str + returned: always + sample: 45b4cc8b-178b-4ad4-bd33-75e3cce5e889 +""" + +from ..module_utils.api_module import APIModule + + +def main(): + argument_spec = dict( + repository=dict(required=True), + append=dict(type="bool", default=True), + method=dict(choices=["tags", "date"], required=True), + value=dict(required=True), + tag_pattern=dict(), + tag_pattern_matches=dict(type="bool", default=True), + state=dict(choices=["present", "absent"], default="present"), + ) + + # Create a module for ourselves + module = APIModule(argument_spec=argument_spec, supports_check_mode=True) + + # Extract our parameters + repository = module.params.get("repository") + append = module.params.get("append") + method = module.params.get("method") + value = module.params.get("value") + tag_pattern = module.params.get("tag_pattern") + tag_pattern_matches = module.params.get("tag_pattern_matches") + state = module.params.get("state") + + # Convert the parameters to a dictionary that can be used with the API + data = module.process_prune_parameters(method, value, tag_pattern, tag_pattern_matches) + + # Extract namespace and repository from the repository parameter + my_name = module.who_am_i() + try: + namespace, repo_shortname = repository.split("/", 1) + except ValueError: + # No namespace part in the repository name. Therefore, the repository + # is in the user's personal namespace + if my_name: + namespace = my_name + repo_shortname = repository + else: + module.fail_json( + msg=( + "The `repository' parameter must include the" + " organization: /{name}." + ).format(name=repository) + ) + + # Check whether namespace exists (organization or user account) + namespace_details = module.get_namespace(namespace) + if not namespace_details: + if state == "absent": + module.exit_json(changed=False) + module.fail_json( + msg="The {namespace} namespace does not exist.".format(namespace=namespace) + ) + + full_repo_name = "{namespace}/{repository}".format( + namespace=namespace, repository=repo_shortname + ) + + # Get the auto-pruning policies for the repository + # + # GET /api/v1/repository/{namespace}/{repository}/autoprunepolicy/ + # + # { + # "policies": [ + # { + # "uuid": "dc84065e-9e9c-43e9-9224-6151c80219b9", + # "method": "creation_date", + # "value": "10w", + # "tagPattern": "dev.*", + # "tagPatternMatches": true + # }, + # { + # "uuid": "b0515264-91da-46c1-829d-11230a9721a8", + # "method": "number_of_tags", + # "value": 20, + # "tagPattern": "prod.*", + # "tagPatternMatches": false + # }, + # { + # "uuid": "71fd827c-6dec-4ecd-ac92-a200e821afa9", + # "method": "number_of_tags", + # "value": 25, + # "tagPattern": null, + # "tagPatternMatches": true + # } + # ] + # } + policies = module.get_object_path( + "repository/{full_repo_name}/autoprunepolicy/", full_repo_name=full_repo_name + ) + + # Finding a matching auto-pruning policy + policy_details = None + if policies: + for policy in policies.get("policies", []): + if ( + policy.get("method") == data.get("method") + and policy.get("value") == data.get("value") + and policy.get("tagPattern") == data.get("tagPattern") + and policy.get("tagPatternMatches") == data.get("tagPatternMatches", True) + ): + policy_details = policy + break + + # Remove the auto-pruning policy + if state == "absent": + module.delete( + policy_details, + "auto-pruning policy", + method, + "repository/{full_repo_name}/autoprunepolicy/{uuid}", + full_repo_name=full_repo_name, + uuid=policy_details.get("uuid", "") if policy_details else "", + ) + + if append and policy_details: + module.exit_json(changed=False, id=policy_details.get("uuid")) + + if not policies: + module.fail_json( + msg="The {repo} repository does not exist.".format(repo=full_repo_name) + ) + + # Remove all the auto-pruning policies, except the one that the user + # specifies + if not append: + deletions = False + for policy in policies.get("policies", []): + if not policy_details or (policy_details.get("uuid") != policy.get("uuid")): + module.delete( + policy, + "auto-pruning policy", + policy.get("method"), + "repository/{full_repo_name}/autoprunepolicy/{uuid}", + auto_exit=False, + full_repo_name=full_repo_name, + uuid=policy.get("uuid"), + ) + deletions = True + if policy_details: + module.exit_json(changed=deletions, id=policy_details.get("uuid")) + + resp = module.create( + "auto-pruning policy", + method, + "repository/{full_repo_name}/autoprunepolicy/", + data, + auto_exit=False, + full_repo_name=full_repo_name, + ) + module.exit_json(changed=True, id=resp.get("uuid")) + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/quay_team.py b/plugins/modules/quay_team.py index c4ce93c..50ac395 100644 --- a/plugins/modules/quay_team.py +++ b/plugins/modules/quay_team.py @@ -39,7 +39,7 @@ type: str organization: description: - - Name of the organization for the team. That organization must exist. + - Name of the organization for the team. This organization must exist. required: true type: str role: diff --git a/plugins/modules/quay_team_ldap.py b/plugins/modules/quay_team_ldap.py index c19a053..6f18eee 100644 --- a/plugins/modules/quay_team_ldap.py +++ b/plugins/modules/quay_team_ldap.py @@ -39,7 +39,7 @@ type: str organization: description: - - Name of the organization for the team. That organization must exist. + - Name of the organization for the team. This organization must exist. required: true type: str sync: diff --git a/plugins/modules/quay_team_oidc.py b/plugins/modules/quay_team_oidc.py index 040b15c..68608cc 100644 --- a/plugins/modules/quay_team_oidc.py +++ b/plugins/modules/quay_team_oidc.py @@ -39,7 +39,7 @@ type: str organization: description: - - Name of the organization for the team. That organization must exist. + - Name of the organization for the team. This organization must exist. required: true type: str sync: diff --git a/roles/quay_org/README.md b/roles/quay_org/README.md index 7514ebb..179bdd3 100644 --- a/roles/quay_org/README.md +++ b/roles/quay_org/README.md @@ -44,8 +44,7 @@ The following list gives a short descriptions of the variables: * `quay_validate_certs`: Whether to allow insecure connections to the API. * `quay_org_name`: Name of the organization to create. * `quay_org_email`: Email address to associate with the organization. -* `quay_org_auto_prune_method`: Method to use for the auto-pruning tags policy. -* `quay_org_auto_prune_value`: Number or period of time to keep tags. +* `quay_org_prune`: List of auto-pruning tags policies for the organization. * `quay_org_users`: List of user accounts to create. * `quay_org_robots`: List of robot accounts to create in the organization. * `quay_org_teams`: List of the teams to create in the organization. @@ -87,9 +86,6 @@ Example Playbook # Organization name and email quay_org_name: production quay_org_email: production@example.com - # Organization auto-prune policy - quay_org_auto_prune_method: tags - quay_org_auto_prune_value: 15 # Proxy cache quay_org_cache_registry: quay.io/sclorg quay_org_cache_expiration: 259200 @@ -97,6 +93,13 @@ Example Playbook quay_org_quota: 1.5 TiB quay_org_warning_pct: 90 quay_org_reject_pct: 97 + # Organization auto-pruning tags policies + quay_org_prune: + - method: tags + value: 15 + tag_pattern: nightly + - method: date + value: 5w # User accounts to create quay_org_users: - username: lvasquez @@ -134,12 +137,16 @@ Example Playbook quay_org_repositories: - name: small_image visibility: private - auto_prune_method: date - auto_prune_value: 5w perms: - name: qa type: team role: read + prune: + - method: tags + value: 5 + tag_pattern: nightly + - method: date + value: 1w ... ``` diff --git a/roles/quay_org/defaults/main.yml b/roles/quay_org/defaults/main.yml index 89caafe..cacb62e 100644 --- a/roles/quay_org/defaults/main.yml +++ b/roles/quay_org/defaults/main.yml @@ -7,8 +7,6 @@ # quay_org_validate_certs: false # quay_org_name: production # quay_org_email: production@example.com -# quay_org_auto_prune_method: date -# quay_org_auto_prune_value: 4w # quay_org_cache_registry: quay.io/sclorg # quay_org_cache_insecure: false # quay_org_cache_username: "" @@ -17,6 +15,12 @@ # quay_org_quota: 1.5 TiB # quay_org_warning_pct: 90 # quay_org_reject_pct: 97 +# quay_org_prune: +# - method: tags +# value: 25 +# - method: date +# value: 1w +# tag_pattern: nightly # quay_org_users: # - username: lvasquez # email: lvasquez@example.com diff --git a/roles/quay_org/meta/argument_specs.yml b/roles/quay_org/meta/argument_specs.yml index a31ef08..0a8d684 100644 --- a/roles/quay_org/meta/argument_specs.yml +++ b/roles/quay_org/meta/argument_specs.yml @@ -72,6 +72,9 @@ argument_specs: type: str quay_org_auto_prune_method: description: + - The O(quay_org_auto_prune_method) parameter is deprecated and will + be removed in future versions of the collection. + Use the O(quay_org_prune) parameter instead. - Method to use for the auto-pruning tags policy. - If V(none), then the module ensures that no policy is in place. The tags are not pruned. @@ -85,6 +88,9 @@ argument_specs: choices: [none, tags, date] quay_org_auto_prune_value: description: + - The O(quay_org_auto_prune_value) parameter is deprecated and will + be removed in future versions of the collection. + Use the O(quay_org_prune) parameter instead. - Number of tags to keep when O(quay_org_auto_prune_value) is V(tags). The value must be 1 or more. - Period of time when O(quay_org_auto_prune_value) is V(date). The @@ -144,6 +150,47 @@ argument_specs: is reached. - Set O(quay_org_reject_pct) to V(0) to remove the reject limit. type: int + quay_org_prune: + description: + - List of auto-pruning tags policies for the organization. + type: list + elements: dict + options: + method: + description: + - Method to use for the auto-pruning tags policy. + - If V(tags), then the policy keeps only the number of tags that + you specify in O(quay_org_prune[].value). + - If V(date), then the policy deletes the tags older than the + time period that you specify in O(quay_org_prune[].value). + required: true + type: str + choices: [tags, date] + value: + description: + - Number of tags to keep when O(quay_org_prune[].method) is + V(tags). The value must be 1 or more. + - Period of time when O(quay_org_prune[].method) is V(date). The + value must be 1 or more, and must be followed by a suffix; + s (for second), m (for minute), h (for hour), d (for day), + or w (for week). + required: true + type: str + tag_pattern: + description: + - Regular expression to select the tags to process. + - If you do not set the parameter, then Quay processes all the + tags. + type: str + tag_pattern_matches: + description: + - If V(true), then Quay processes the tags matching the + O(quay_org_prune[].tag_pattern) parameter. + - If V(false), then Quay excludes the tags matching the + O(quay_org_prune[].tag_pattern) parameter. + - V(true) by default. + type: bool + default: true quay_org_users: description: - List of user account to create. @@ -322,6 +369,10 @@ argument_specs: choices: [public, private] auto_prune_method: description: + - The O(quay_org_repositories[].auto_prune_method) parameter is + deprecated and will be removed in future versions of the + collection. Use the O(quay_org_repositories[].prune) parameter + instead. - Method to use for the auto-pruning tags policy. - If V(none), then the module ensures that no policy is in place. The tags are not pruned. @@ -337,6 +388,10 @@ argument_specs: choices: [none, tags, date] auto_prune_value: description: + - The O(quay_org_repositories[].auto_prune_value) parameter is + deprecated and will be removed in future versions of the + collection. Use the O(quay_org_repositories[].prune) parameter + instead. - Number of tags to keep when O(quay_org_repositories[].auto_prune_method) is V(tags). The value must be 1 or more. @@ -391,3 +446,48 @@ argument_specs: parameter. type: str choices: [NORMAL, READ_ONLY, MIRROR] + prune: + description: + - List of auto-pruning tags policies for the repositiory. + type: list + elements: dict + options: + method: + description: + - Method to use for the auto-pruning tags policy. + - If V(tags), then the policy keeps only the number of tags + that you specify in + O(quay_org_repositories[].prune[].value). + - If V(date), then the policy deletes the tags older than the + time period that you specify in + O(quay_org_repositories[].prune[].value). + required: true + type: str + choices: [tags, date] + value: + description: + - Number of tags to keep when + O(quay_org_repositories[].prune[].method) is V(tags). The + value must be 1 or more. + - Period of time when + O(quay_org_repositories[].prune[].method) is V(date). The + value must be 1 or more, and must be followed by a suffix; + s (for second), m (for minute), h (for hour), d (for day), + or w (for week). + required: true + type: str + tag_pattern: + description: + - Regular expression to select the tags to process. + - If you do not set the parameter, then Quay processes all + the tags. + type: str + tag_pattern_matches: + description: + - If V(true), then Quay processes the tags matching the + O(quay_org_repositories[].prune[].tag_pattern) parameter. + - If V(false), then Quay excludes the tags matching the + O(quay_org_repositories[].prune[].tag_pattern) parameter. + - V(true) by default. + type: bool + default: true diff --git a/roles/quay_org/tasks/main.yml b/roles/quay_org/tasks/main.yml index bc485b0..b06d121 100644 --- a/roles/quay_org/tasks/main.yml +++ b/roles/quay_org/tasks/main.yml @@ -13,6 +13,9 @@ - name: Ensure the organization exists ansible.builtin.import_tasks: organization.yml +- name: Ensure the auto-pruning policies for the organization exist + ansible.builtin.import_tasks: organization_prune.yml + - name: Ensure the proxy cache configuration exists ansible.builtin.import_tasks: proxy_cache.yml diff --git a/roles/quay_org/tasks/organization_prune.yml b/roles/quay_org/tasks/organization_prune.yml new file mode 100644 index 0000000..8ba27dc --- /dev/null +++ b/roles/quay_org/tasks/organization_prune.yml @@ -0,0 +1,15 @@ +--- +- name: Ensure the auto-pruning policies for the organization exist + infra.quay_configuration.quay_organization_prune: + namespace: "{{ quay_org_name }}" + method: "{{ item['method'] }}" + value: "{{ item['value'] }}" + tag_pattern: "{{ item['tag_pattern'] | default(omit) }}" + tag_pattern_matches: "{{ item['tag_pattern_matches'] | default(omit) }}" + state: present + quay_token: "{{ quay_org_token | default(omit) }}" + quay_username: "{{ quay_org_username | default(omit) }}" + quay_password: "{{ quay_org_password | default(omit) }}" + quay_host: "{{ quay_org_host | default(omit) }}" + validate_certs: "{{ quay_org_validate_certs | default(omit) }}" + loop: "{{ quay_org_prune }}" diff --git a/roles/quay_org/tasks/repositories.yml b/roles/quay_org/tasks/repositories.yml index 1a57d18..a3ddcac 100644 --- a/roles/quay_org/tasks/repositories.yml +++ b/roles/quay_org/tasks/repositories.yml @@ -16,3 +16,19 @@ quay_host: "{{ quay_org_host | default(omit) }}" validate_certs: "{{ quay_org_validate_certs | default(omit) }}" loop: "{{ quay_org_repositories }}" + +- name: Ensure the auto-pruning policies for the repositories exist + infra.quay_configuration.quay_repository_prune: + repository: "{{ quay_org_name }}/{{ item[0]['name'] }}" + method: "{{ item[1]['method'] }}" + value: "{{ item[1]['value'] }}" + tag_pattern: "{{ item[1]['tag_pattern'] | default(omit) }}" + tag_pattern_matches: "{{ item[1]['tag_pattern_matches'] | default(omit) }}" + state: present + quay_token: "{{ quay_org_token | default(omit) }}" + quay_username: "{{ quay_org_username | default(omit) }}" + quay_password: "{{ quay_org_password | default(omit) }}" + quay_host: "{{ quay_org_host | default(omit) }}" + validate_certs: "{{ quay_org_validate_certs | default(omit) }}" + loop: "{{ quay_org_repositories | + ansible.builtin.subelements('prune', skip_missing=true) }}" diff --git a/roles/quay_org/tests/test.yml b/roles/quay_org/tests/test.yml index 4978bf2..64440d1 100644 --- a/roles/quay_org/tests/test.yml +++ b/roles/quay_org/tests/test.yml @@ -13,13 +13,17 @@ quay_org_token: vgfH9zH5q6eV16Con7SvDQYSr0KPYQimMHVehZv7 quay_org_name: production quay_org_email: production@example.com - quay_org_auto_prune_method: tags - quay_org_auto_prune_value: 15 quay_org_cache_registry: public.ecr.aws/nginx quay_org_cache_expiration: 345600 quay_org_quota: 500 GiB quay_org_warning_pct: 80 quay_org_reject_pct: 90 + quay_org_prune: + - method: tags + value: 25 + - method: date + value: 10w + tag_pattern: nightly quay_org_users: - username: lvasquez email: lvasquez@example.com @@ -51,9 +55,13 @@ quay_org_repositories: - name: small_image visibility: public - auto_prune_method: date - auto_prune_value: 10w perms: - name: qa type: team role: read + prune: + - method: tags + value: 5 + tag_pattern: nightly + - method: date + value: 5w diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml index 6ebb8c7..74216be 100644 --- a/tests/docker-compose.yml +++ b/tests/docker-compose.yml @@ -26,7 +26,7 @@ services: - "8089:8080" quay: - image: quay.io/projectquay/quay:v3.12.0 + image: quay.io/projectquay/quay:v3.13.1 volumes: - "./quay-config:/conf/stack:Z" - "./quay-delay.sh:/quay-registry/conf/init/a-delay.sh:ro" diff --git a/tests/integration/targets/quay_organization_prune/meta/main.yml b/tests/integration/targets/quay_organization_prune/meta/main.yml new file mode 100644 index 0000000..6f61d07 --- /dev/null +++ b/tests/integration/targets/quay_organization_prune/meta/main.yml @@ -0,0 +1,4 @@ +--- +dependencies: + - setup_organization +... diff --git a/tests/integration/targets/quay_organization_prune/tasks/main.yml b/tests/integration/targets/quay_organization_prune/tasks/main.yml new file mode 100644 index 0000000..17e49ef --- /dev/null +++ b/tests/integration/targets/quay_organization_prune/tasks/main.yml @@ -0,0 +1,217 @@ +--- +- name: ERROR EXPECTED Non-existing organization + infra.quay_configuration.quay_organization_prune: + namespace: nonexisting + method: tags + value: 5 + tag_pattern: "unstable" + tag_pattern_matches: true + quay_host: "{{ quay_url }}" + quay_token: "{{ quay_token }}" + validate_certs: false + ignore_errors: true + register: result + +- name: Ensure that the task failed + ansible.builtin.assert: + that: result['failed'] + fail_msg: The preceding task should have failed (non-existing organization) + +- name: ERROR EXPECTED Wrong number of tags + infra.quay_configuration.quay_organization_prune: + namespace: ansibletestorg + method: tags + value: wrong + tag_pattern: "unstable" + tag_pattern_matches: true + quay_host: "{{ quay_url }}" + quay_token: "{{ quay_token }}" + validate_certs: false + ignore_errors: true + register: result + +- name: Ensure that the task failed + ansible.builtin.assert: + that: result['failed'] + fail_msg: The preceding task should have failed (wrong number of tags) + +- name: ERROR EXPECTED Wrong date + infra.quay_configuration.quay_organization_prune: + namespace: ansibletestorg + method: date + value: wrong + tag_pattern: "unstable" + tag_pattern_matches: true + quay_host: "{{ quay_url }}" + quay_token: "{{ quay_token }}" + validate_certs: false + ignore_errors: true + register: result + +- name: Ensure that the task failed + ansible.builtin.assert: + that: result['failed'] + fail_msg: The preceding task should have failed (wrong date) + +- name: Ensure a policy does not exist in a non-existing org (no change) + infra.quay_configuration.quay_organization_prune: + namespace: nonexisting + method: tags + value: 5 + tag_pattern: "unstable" + tag_pattern_matches: true + quay_host: "{{ quay_url }}" + quay_token: "{{ quay_token }}" + validate_certs: false + state: absent + register: result + +- name: Ensure that the task did not change anything + ansible.builtin.assert: + that: not result['changed'] + fail_msg: The preceding task should not have changed anything + +- name: Ensure a tag auto-pruning policy exists + infra.quay_configuration.quay_organization_prune: + namespace: ansibletestorg + method: tags + value: 5 + tag_pattern: "unstable" + tag_pattern_matches: true + quay_host: "{{ quay_url }}" + quay_token: "{{ quay_token }}" + validate_certs: false + +- name: Ensure a tag auto-pruning policy exists (no change) + infra.quay_configuration.quay_organization_prune: + namespace: ansibletestorg + method: tags + value: 5 + tag_pattern: "unstable" + tag_pattern_matches: true + quay_host: "{{ quay_url }}" + quay_token: "{{ quay_token }}" + validate_certs: false + register: result + +- name: Ensure that the task did not change anything + ansible.builtin.assert: + that: not result['changed'] + fail_msg: The preceding task should not have changed anything + +- name: Ensure a date auto-pruning policy exists 1 + infra.quay_configuration.quay_organization_prune: + namespace: ansibletestorg + method: date + value: 5d + quay_host: "{{ quay_url }}" + quay_token: "{{ quay_token }}" + validate_certs: false + +- name: Ensure a date auto-pruning policy exists 2 + infra.quay_configuration.quay_organization_prune: + namespace: ansibletestorg + method: date + value: 6w + tag_pattern: "nightly" + tag_pattern_matches: true + quay_host: "{{ quay_url }}" + quay_token: "{{ quay_token }}" + validate_certs: false + +- name: Ensure non-existing policy is removed (no change) + infra.quay_configuration.quay_organization_prune: + namespace: ansibletestorg + method: date + value: 42w + tag_pattern: "nightly" + tag_pattern_matches: false + state: absent + quay_host: "{{ quay_url }}" + quay_token: "{{ quay_token }}" + validate_certs: false + register: result + +- name: Ensure that the task did not change anything + ansible.builtin.assert: + that: not result['changed'] + fail_msg: The preceding task should not have changed anything + +- name: Ensure a tag auto-pruning policy is removed + infra.quay_configuration.quay_organization_prune: + namespace: ansibletestorg + method: tags + value: 5 + tag_pattern: "unstable" + tag_pattern_matches: true + state: absent + quay_host: "{{ quay_url }}" + quay_token: "{{ quay_token }}" + validate_certs: false + +- name: Ensure only one tag auto-pruning policy exists + infra.quay_configuration.quay_organization_prune: + namespace: ansibletestorg + method: tags + value: 50 + tag_pattern: "foobar" + tag_pattern_matches: true + append: false + quay_host: "{{ quay_url }}" + quay_token: "{{ quay_token }}" + validate_certs: false + +- name: Ensure only one tag auto-pruning policy exists (no change) + infra.quay_configuration.quay_organization_prune: + namespace: ansibletestorg + method: tags + value: 50 + tag_pattern: "foobar" + tag_pattern_matches: true + append: false + quay_host: "{{ quay_url }}" + quay_token: "{{ quay_token }}" + validate_certs: false + register: result + +- name: Ensure that the task did not change anything + ansible.builtin.assert: + that: not result['changed'] + fail_msg: The preceding task should not have changed anything + +- name: Ensure a date auto-pruning policy exists + infra.quay_configuration.quay_organization_prune: + namespace: ansibletestorg + method: date + value: 42w + tag_pattern: "nightly" + tag_pattern_matches: false + quay_host: "{{ quay_url }}" + quay_token: "{{ quay_token }}" + validate_certs: false + +- name: Ensure only one tag auto-pruning policy is set + infra.quay_configuration.quay_organization_prune: + namespace: ansibletestorg + method: tags + value: 50 + tag_pattern: "foobar" + tag_pattern_matches: true + append: false + quay_host: "{{ quay_url }}" + quay_token: "{{ quay_token }}" + validate_certs: false + +- name: Ensure the tag auto-pruning policy is removed + infra.quay_configuration.quay_organization_prune: + namespace: ansibletestorg + method: tags + value: 50 + tag_pattern: "foobar" + tag_pattern_matches: true + append: false + quay_host: "{{ quay_url }}" + quay_token: "{{ quay_token }}" + validate_certs: false + state: absent +... diff --git a/tests/integration/targets/quay_repository_prune/meta/main.yml b/tests/integration/targets/quay_repository_prune/meta/main.yml new file mode 100644 index 0000000..6f61d07 --- /dev/null +++ b/tests/integration/targets/quay_repository_prune/meta/main.yml @@ -0,0 +1,4 @@ +--- +dependencies: + - setup_organization +... diff --git a/tests/integration/targets/quay_repository_prune/tasks/main.yml b/tests/integration/targets/quay_repository_prune/tasks/main.yml new file mode 100644 index 0000000..95989a5 --- /dev/null +++ b/tests/integration/targets/quay_repository_prune/tasks/main.yml @@ -0,0 +1,235 @@ +--- +# Supporting repository +- name: Ensure repository ansibletestrepo exists + infra.quay_configuration.quay_repository: + name: ansibletestorg/ansibletestrepo + visibility: private + state: present + quay_host: "{{ quay_url }}" + quay_token: "{{ quay_token }}" + validate_certs: false + +- name: ERROR EXPECTED Non-existing namespace + infra.quay_configuration.quay_repository_prune: + repository: nonexisting/ansibletestrepo + method: tags + value: 5 + tag_pattern: "unstable" + tag_pattern_matches: true + quay_host: "{{ quay_url }}" + quay_token: "{{ quay_token }}" + validate_certs: false + ignore_errors: true + register: result + +- name: Ensure that the task failed + ansible.builtin.assert: + that: result['failed'] + fail_msg: The preceding task should have failed (non-existing organization) + +- name: ERROR EXPECTED Wrong number of tags + infra.quay_configuration.quay_repository_prune: + repository: ansibletestorg/ansibletestrepo + method: tags + value: wrong + tag_pattern: "unstable" + tag_pattern_matches: true + quay_host: "{{ quay_url }}" + quay_token: "{{ quay_token }}" + validate_certs: false + ignore_errors: true + register: result + +- name: Ensure that the task failed + ansible.builtin.assert: + that: result['failed'] + fail_msg: The preceding task should have failed (wrong number of tags) + +- name: ERROR EXPECTED Wrong date + infra.quay_configuration.quay_repository_prune: + repository: ansibletestorg/ansibletestrepo + method: date + value: wrong + tag_pattern: "unstable" + tag_pattern_matches: true + quay_host: "{{ quay_url }}" + quay_token: "{{ quay_token }}" + validate_certs: false + ignore_errors: true + register: result + +- name: Ensure that the task failed + ansible.builtin.assert: + that: result['failed'] + fail_msg: The preceding task should have failed (wrong date) + +- name: Ensure a policy does not exist in a non-existing repository (no change) + infra.quay_configuration.quay_repository_prune: + repository: ansibletestorg/nonexisting + method: tags + value: 5 + tag_pattern: "unstable" + tag_pattern_matches: true + quay_host: "{{ quay_url }}" + quay_token: "{{ quay_token }}" + validate_certs: false + state: absent + register: result + +- name: Ensure that the task did not change anything + ansible.builtin.assert: + that: not result['changed'] + fail_msg: The preceding task should not have changed anything + +- name: Ensure a tag auto-pruning policy exists + infra.quay_configuration.quay_repository_prune: + repository: ansibletestorg/ansibletestrepo + method: tags + value: 5 + tag_pattern: "unstable" + tag_pattern_matches: false + quay_host: "{{ quay_url }}" + quay_token: "{{ quay_token }}" + validate_certs: false + +- name: Ensure a tag auto-pruning policy exists (no change) + infra.quay_configuration.quay_repository_prune: + repository: ansibletestorg/ansibletestrepo + method: tags + value: 5 + tag_pattern: "unstable" + tag_pattern_matches: false + quay_host: "{{ quay_url }}" + quay_token: "{{ quay_token }}" + validate_certs: false + register: result + +- name: Ensure that the task did not change anything + ansible.builtin.assert: + that: not result['changed'] + fail_msg: The preceding task should not have changed anything + +- name: Ensure a date auto-pruning policy exists 1 + infra.quay_configuration.quay_repository_prune: + repository: ansibletestorg/ansibletestrepo + method: date + value: 5d + quay_host: "{{ quay_url }}" + quay_token: "{{ quay_token }}" + validate_certs: false + +- name: Ensure a date auto-pruning policy exists 2 + infra.quay_configuration.quay_repository_prune: + repository: ansibletestorg/ansibletestrepo + method: date + value: 6w + tag_pattern: "nightly" + tag_pattern_matches: true + quay_host: "{{ quay_url }}" + quay_token: "{{ quay_token }}" + validate_certs: false + +- name: Ensure non-existing policy is removed (no change) + infra.quay_configuration.quay_repository_prune: + repository: ansibletestorg/ansibletestrepo + method: date + value: 42w + tag_pattern: "nightly" + tag_pattern_matches: false + state: absent + quay_host: "{{ quay_url }}" + quay_token: "{{ quay_token }}" + validate_certs: false + register: result + +- name: Ensure that the task did not change anything + ansible.builtin.assert: + that: not result['changed'] + fail_msg: The preceding task should not have changed anything + +- name: Ensure a tag auto-pruning policy is removed + infra.quay_configuration.quay_repository_prune: + repository: ansibletestorg/ansibletestrepo + method: tags + value: 5 + tag_pattern: "unstable" + tag_pattern_matches: false + state: absent + quay_host: "{{ quay_url }}" + quay_token: "{{ quay_token }}" + validate_certs: false + +- name: Ensure only one tag auto-pruning policy exists + infra.quay_configuration.quay_repository_prune: + repository: ansibletestorg/ansibletestrepo + method: tags + value: 50 + tag_pattern: "foobar" + tag_pattern_matches: true + append: false + quay_host: "{{ quay_url }}" + quay_token: "{{ quay_token }}" + validate_certs: false + +- name: Ensure only one tag auto-pruning policy exists (no change) + infra.quay_configuration.quay_repository_prune: + repository: ansibletestorg/ansibletestrepo + method: tags + value: 50 + tag_pattern: "foobar" + tag_pattern_matches: true + append: false + quay_host: "{{ quay_url }}" + quay_token: "{{ quay_token }}" + validate_certs: false + register: result + +- name: Ensure that the task did not change anything + ansible.builtin.assert: + that: not result['changed'] + fail_msg: The preceding task should not have changed anything + +- name: Ensure a date auto-pruning policy exists + infra.quay_configuration.quay_repository_prune: + repository: ansibletestorg/ansibletestrepo + method: date + value: 42w + tag_pattern: "nightly" + tag_pattern_matches: false + quay_host: "{{ quay_url }}" + quay_token: "{{ quay_token }}" + validate_certs: false + +- name: Ensure only one tag auto-pruning policy is set + infra.quay_configuration.quay_repository_prune: + repository: ansibletestorg/ansibletestrepo + method: tags + value: 50 + tag_pattern: "foobar" + tag_pattern_matches: true + append: false + quay_host: "{{ quay_url }}" + quay_token: "{{ quay_token }}" + validate_certs: false + +- name: Ensure the tag auto-pruning policy is removed + infra.quay_configuration.quay_repository_prune: + repository: ansibletestorg/ansibletestrepo + method: tags + value: 50 + tag_pattern: "foobar" + tag_pattern_matches: true + append: false + quay_host: "{{ quay_url }}" + quay_token: "{{ quay_token }}" + validate_certs: false + state: absent + +- name: Ensure the repository is removed + infra.quay_configuration.quay_repository: + name: ansibletestorg/ansibletestrepo + state: absent + quay_host: "{{ quay_url }}" + quay_token: "{{ quay_token }}" + validate_certs: false +... diff --git a/tests/integration/targets/role_quay_org/tasks/main.yml b/tests/integration/targets/role_quay_org/tasks/main.yml index 0d99aa2..833be42 100644 --- a/tests/integration/targets/role_quay_org/tasks/main.yml +++ b/tests/integration/targets/role_quay_org/tasks/main.yml @@ -8,14 +8,18 @@ quay_org_validate_certs: false quay_org_name: testorg quay_org_email: testorg@example.com - quay_org_auto_prune_method: tags - quay_org_auto_prune_value: 15 quay_org_cache_registry: public.ecr.aws/nginx quay_org_cache_expiration: 345600 quay_org_cache_insecure: true quay_org_quota: 1.5 TiB quay_org_warning_pct: 90 quay_org_reject_pct: 97 + quay_org_prune: + - method: tags + value: 25 + - method: date + value: 10w + tag_pattern: nightly quay_org_users: - username: testuser1 email: testuser1@example.com @@ -57,13 +61,18 @@ - name: testrepo1 description: Test repository 1 visibility: public - auto_prune_method: date - auto_prune_value: 10w perms: - name: testteam1 type: team role: read - name: testrepo2 + prune: + - method: tags + value: 5 + tag_pattern: prod + tag_pattern_matches: false + - method: date + value: 5w # Cleanup (by using quay_username and quay_password for testing purpose) - name: Ensure repositories are removed