1
0
Fork 0
forked from infra/ansible

communishift: added cleanup playbook cleanup-administration-delete-projects.yml

Signed-off-by: David Kirwan <davidkirwanirl@gmail.com>
This commit is contained in:
David Kirwan 2026-05-11 12:24:18 +01:00
commit c6704f92dc
Signed by untrusted user: dkirwan
GPG key ID: A5893AB6474AC37D
3 changed files with 268 additions and 65 deletions

View file

@ -0,0 +1,48 @@
---
# Communishift project deletion (inventory + per-project eligibility).
# Run after workloads are shut down via communishift_disable_project.yml.
# Uses communishift_projects from inventory (e.g. inventory/group_vars/all) and each
# entry's do_not_delete to skip exempt namespaces.
#
# Omitting communishift_delete_projects_dry_run applies changes. Preview removals only:
# ansible-playbook .../communishift_delete_projects.yml --tags communishift_delete_projects \
# -e communishift_delete_projects_dry_run=true
#
# Explicit live delete:
# ansible-playbook .../communishift_delete_projects.yml --tags communishift_delete_projects \
# -e communishift_delete_projects_dry_run=false
#
# Run with an inventory that merges group_vars/all so communishift_projects is defined.
#
- hosts: localhost
user: root
gather_facts: false
vars_files:
- /srv/web/infra/ansible/vars/global.yml
- "/srv/private/ansible/vars.yml"
- /srv/web/infra/ansible/vars/{{ ansible_distribution }}.yml
pre_tasks:
- name: Require communishift_projects from inventory
ansible.builtin.assert:
that:
- communishift_projects is defined
- communishift_projects is mapping
fail_msg: >
communishift_projects is missing. Run with an inventory that loads
inventory/group_vars/all (e.g. ansible-playbook -i path/to/inventory ...).
tags:
- always
tasks:
- name: Communishift delete eligible projects (namespace, EFS, group)
ansible.builtin.include_role:
name: communishift
tasks_from: cleanup-administration-delete-projects
apply:
tags:
- communishift_delete_projects
loop: "{{ lookup('dict', communishift_projects) }}"
tags:
- communishift_delete_projects

View file

@ -23,6 +23,13 @@ options:
description: The name of the Communishift Project.
required: true
type: str
state:
description: Create access point or remove access points for this project from the filesystem.
type: str
default: present
choices:
- present
- absent
aws_access_key_id:
description: The AWS API access_key_id.
required: true
@ -36,8 +43,11 @@ options:
required: true
type: str
aws_efs_filesystem_id:
description: The AWS EFS FileSystemId of the resource that was previously created.
required: true
description: >-
The AWS EFS FileSystemId. Required when I(state=present).
Optional when I(state=absent); if omitted the filesystem is found by CreationToken matching I(project_name),
consistent with community.aws.efs I(name).
required: false
type: str
author:
@ -57,88 +67,143 @@ EXAMPLES = r"""
{{create_efs_filesystem_response['efs']['file_system_id']}}
register: create_efs_accesspoint_response
ignore_errors: true
- name: Delete access points for a Communishift project
communishift_storage_efs:
project_name: "{{ item.value.name }}"
state: absent
aws_access_key_id: "{{ communishift_efs_access_key }}"
aws_secret_access_key: "{{ communishift_efs_secret_key }}"
aws_region: "{{ communishift_region }}"
"""
RETURN = r"""
# These are examples of possible return values, and in general should use other names for return values.
accesspoint_id:
description: The AccessPointId returned by the AWS EFS API creation request.
type: str
returned: If the EFS Filesystem exists and the AccessPoint been successfully created or already exists.
sample: 'fsap-0938462b9b5f77388'
full_response:
description: The response returned by the AWS EFS boto3 client.create_access_point() or client.describe_access_points().
description: The AccessPointId returned by create (present) when a single matching resource is summarized.
type: str
returned: If the EFS Filesystem exists and the AccessPoint has been successfully created or already exists.
sample: '{'ResponseMetadata': {'RequestId': '9c3d3e41-4332-4fe3-8388-f04ccf0400a2', 'HTTPStatusCode': 200, 'HTTPHeaders': {'x-amzn-requ
estid': '9c3d3e41-4332-4fe3-8388-f04ccf0400a2', 'content-type': 'application/json', 'content-length': '503', 'date': 'Tue, 16
Aug 2022 10:17:43 GMT'}, 'RetryAttempts': 0}, 'ClientToken': 'communishift_storage_efs', 'Tags': [{'Key': 'communishift', 'Val
ue': 'projectname'}], 'AccessPointId': 'fsap-0938462b9b5f77388', 'AccessPointArn': 'arn:aws:elasticfilesystem:us-east-1:XXXXXXXXXXXX:
access-point/fsap-0938462b9b5f77388', 'FileSystemId': 'fs-0343e73f7765a503b', 'PosixUser': {'Uid': 50000, 'Gid': 50000}
, 'RootDirectory': {'Path': '/', 'CreationInfo': {'OwnerUid': 50000, 'OwnerGid': 50000, 'Permissions': '775'}}, 'OwnerId': 'XXXX',
'LifeCycleState': 'creating'}'
deleted_access_point_ids:
description: Access point IDs deleted in absent mode.
type: list
elements: str
returned: absent
full_response:
description: Creation response or describe payload (present) or summarized absent action.
type: raw
msg:
description: The output message that the module generates.
type: str
returned: always
sample: 'AWS EFS AccessPoint already exists.'
"""
from ansible.module_utils.basic import AnsibleModule
def run_module():
# define available arguments/parameters a user can pass to the module
module_args = dict(
project_name=dict(type="str", required=True),
aws_access_key_id=dict(type="str", required=True),
aws_secret_access_key=dict(type="str", required=True),
aws_region=dict(type="str", required=True),
aws_efs_filesystem_id=dict(type="str", required=True),
)
# seed the result dict in the object
# we primarily care about changed and state
# changed is if this module effectively modified the target
# state will include any data that you want your module to pass back
# for consumption, for example, in a subsequent task
result = dict(changed=False, accesspoint_id="", full_response="", msg="")
# the AnsibleModule object will be our abstraction working with Ansible
# this includes instantiation, a couple of common attr would be the
# args/params passed to the execution, as well as if the module
# supports check mode
module = AnsibleModule(argument_spec=module_args, supports_check_mode=True)
# if the user is working with this module in only check mode we do not
# want to make any changes to the environment, just return the current
# state with no modifications
if module.check_mode:
module.exit_json(**result)
efs_client = boto3.client(
def build_efs_client(params):
return boto3.client(
"efs",
aws_access_key_id=module.params["aws_access_key_id"],
aws_secret_access_key=module.params["aws_secret_access_key"],
region_name=module.params["aws_region"],
aws_access_key_id=params["aws_access_key_id"],
aws_secret_access_key=params["aws_secret_access_key"],
region_name=params["aws_region"],
)
def find_filesystem_id_by_creation_token(efs_client, creation_token):
paginator = efs_client.get_paginator("describe_file_systems")
for page in paginator.paginate():
for fs in page.get("FileSystems", []):
if fs.get("CreationToken") == creation_token:
return fs["FileSystemId"]
return None
def matching_access_points(efs_client, filesystem_id, project_name):
expected_path = "/{0}".format(project_name)
matching = []
paginator = efs_client.get_paginator("describe_access_points")
for page in paginator.paginate(FileSystemId=filesystem_id):
for ap in page.get("AccessPoints", []):
tags = {t["Key"]: t["Value"] for t in ap.get("Tags", [])}
if tags.get("communishift") != project_name:
continue
root_path = ap.get("RootDirectory", {}).get("Path", "")
if root_path != expected_path:
continue
matching.append(ap["AccessPointId"])
return matching
def run_absent(module, efs_client, params, check_mode):
project_name = params["project_name"]
fs_id = params.get("aws_efs_filesystem_id") or ""
if not fs_id:
fs_id = find_filesystem_id_by_creation_token(efs_client, project_name)
if fs_id is None:
module.exit_json(
changed=False,
deleted_access_point_ids=[],
accesspoint_id="",
full_response=None,
msg="No EFS file system found with CreationToken matching project_name.",
)
access_point_ids = matching_access_points(efs_client, fs_id, project_name)
if not access_point_ids:
module.exit_json(
changed=False,
deleted_access_point_ids=[],
accesspoint_id="",
full_response={"file_system_id": fs_id},
msg="No matching Communishift-tagged access points to delete.",
)
if check_mode:
module.exit_json(
changed=True,
deleted_access_point_ids=[],
access_points_that_would_be_deleted=access_point_ids,
accesspoint_id="",
full_response={"file_system_id": fs_id},
msg="Check mode: would delete {0} access point(s)".format(len(access_point_ids)),
)
deleted = []
for ap_id in access_point_ids:
efs_client.delete_access_point(AccessPointId=ap_id)
deleted.append(ap_id)
module.exit_json(
changed=True,
deleted_access_point_ids=deleted,
accesspoint_id=deleted[-1] if deleted else "",
full_response={
"file_system_id": fs_id,
"deleted_access_point_ids": deleted,
},
msg="Deleted {0} Communishift EFS access point(s).".format(len(deleted)),
)
def run_present(module, efs_client, params, check_mode):
fs_id = params["aws_efs_filesystem_id"]
if check_mode:
module.exit_json(
changed=False,
accesspoint_id="",
full_response=None,
msg="Check mode: would create access point if missing.",
)
try:
response = efs_client.create_access_point(
ClientToken=module.params["project_name"],
ClientToken=params["project_name"],
Tags=[
{"Key": "communishift", "Value": module.params["project_name"]},
{"Key": "Name", "Value": module.params["project_name"]},
{"Key": "communishift", "Value": params["project_name"]},
{"Key": "Name", "Value": params["project_name"]},
],
FileSystemId=module.params["aws_efs_filesystem_id"],
FileSystemId=fs_id,
PosixUser={
"Uid": 1001,
"Gid": 1001,
"SecondaryGids": [ 0 ],
"SecondaryGids": [0],
},
RootDirectory={
"Path": f"/{ module.params['project_name'] }",
"Path": "/{0}".format(params["project_name"]),
"CreationInfo": {
"OwnerUid": 0,
"OwnerGid": 0,
@ -147,19 +212,49 @@ def run_module():
},
)
result["accesspoint_id"] = response["AccessPointId"]
result["full_response"] = response
result["changed"] = True
result["msg"] = "AWS EFS AccessPoint created successfully."
module.exit_json(**result)
except efs_client.exceptions.AccessPointAlreadyExists:
response = efs_client.describe_access_points(
FileSystemId=module.params["aws_efs_filesystem_id"]
module.exit_json(
changed=True,
accesspoint_id=response["AccessPointId"],
full_response=response,
deleted_access_point_ids=[],
msg="AWS EFS AccessPoint created successfully.",
)
result["accesspoint_id"] = response["AccessPoints"][0]["AccessPointId"]
result["full_response"] = response
result["msg"] = "AWS EFS AccessPoint already exists."
module.fail_json(**result)
except efs_client.exceptions.AccessPointAlreadyExists:
ap_ids = matching_access_points(efs_client, fs_id, params["project_name"])
chosen = ap_ids[0] if ap_ids else ""
module.exit_json(
changed=False,
accesspoint_id=chosen,
full_response=ap_ids,
deleted_access_point_ids=[],
msg="AWS EFS AccessPoint already exists.",
)
def run_module():
module_args = dict(
project_name=dict(type="str", required=True),
state=dict(type="str", default="present", choices=["present", "absent"]),
aws_access_key_id=dict(type="str", required=True),
aws_secret_access_key=dict(type="str", required=True),
aws_region=dict(type="str", required=True),
aws_efs_filesystem_id=dict(type="str", required=False),
)
module = AnsibleModule(argument_spec=module_args, supports_check_mode=True)
params = module.params
state = params["state"]
if state == "present" and not params.get("aws_efs_filesystem_id"):
module.fail_json(msg="aws_efs_filesystem_id is required when state=present.")
efs_client = build_efs_client(params)
if state == "absent":
run_absent(module, efs_client, params, module.check_mode)
else:
run_present(module, efs_client, params, module.check_mode)
def main():

View file

@ -0,0 +1,60 @@
---
- name: Set Communishift deletion context for this project
ansible.builtin.set_fact:
communishift_namespace: "{{ item.value.name }}"
communishift_protected: "{{ item.value.do_not_delete | default(false) | bool }}"
- name: Skip delete for protected Communishift project
ansible.builtin.debug:
msg: "Skipping deletion for {{ communishift_namespace }} (do_not_delete is true)"
when: communishift_protected | bool
- name: Dry-run - eligible project deletion preview
ansible.builtin.debug:
msg: >-
Would delete Namespace {{ communishift_namespace }}, Communishift EFS access points and filesystem {{ communishift_namespace }},
and Group {{ communishift_namespace }}-admins (set -e communishift_delete_projects_dry_run=false to apply).
when:
- not communishift_protected | bool
- communishift_delete_projects_dry_run | default(false) | bool
- name: Delete Communishift namespace cluster resources then AWS/OpenShift teardown
when:
- not communishift_protected | bool
- not (communishift_delete_projects_dry_run | default(false) | bool)
block:
- name: Remove Communishift Kubernetes namespace
kubernetes.core.k8s:
api_key: "{{ communishift_ocp_api_token }}"
host: "{{ communishift_ocp_api_host }}"
api_version: v1
kind: Namespace
name: "{{ communishift_namespace }}"
state: absent
wait: true
- name: Delete Communishift-tagged EFS access points for project
communishift_storage_efs:
project_name: "{{ communishift_namespace }}"
state: absent
aws_access_key_id: "{{ communishift_efs_access_key }}"
aws_secret_access_key: "{{ communishift_efs_secret_key }}"
aws_region: "{{ communishift_region }}"
- name: Remove Communishift EFS filesystem (matches community.aws.efs creation token)
community.aws.efs:
aws_access_key: "{{ communishift_efs_access_key }}"
aws_secret_key: "{{ communishift_efs_secret_key }}"
region: "{{ communishift_region }}"
state: absent
name: "{{ communishift_namespace }}"
wait: true
- name: Remove OpenShift group for project admins
community.okd.k8s:
api_key: "{{ communishift_ocp_api_token }}"
host: "{{ communishift_ocp_api_host }}"
name: "{{ communishift_namespace }}-admins"
api_version: user.openshift.io/v1
kind: Group
state: absent