You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
ansible-portainer/plugins/modules/portainer_stack.py

250 lines
7.3 KiB
Python

#!/usr/bin/python
from __future__ import absolute_import, division, print_function
__metaclass__ = type
from ansible.module_utils.basic import AnsibleModule
try:
# FIXME: Hack to make imports work with IDE. The ansible import path is not valid for a regular python
# project.
from plugins.module_utils.portainer import *
except ImportError:
from ansible_collections.chatton.portainer.plugins.module_utils.portainer import (
PortainerClient,
_query_params_to_string,
)
DOCUMENTATION = r"""
---
module: portainer_stack
short_description: This is my test module
# If this is part of a collection, you need to use semantic versioning,
# i.e. the version is of the form "2.5.0" and not "2.4".
version_added: "1.0.0"
description: This is my longer description explaining my test module.
options:
name:
description: This is the message to send to the test module.
required: true
type: str
new:
description:
- Control to demo if the result of this module is changed or not.
- Parameter description can be a list as well.
required: false
type: bool
# Specify this value according to your collection
# in format of namespace.collection.doc_fragment_name
extends_documentation_fragment:
- my_namespace.my_collection.my_doc_fragment_name
author:
- Your Name (@chatton)
"""
EXAMPLES = r"""
# Deploy Gitea, Plex and Mealie stacks to portainer provided the files exist.
- name: Portainer | Update Stack
chatton.portainer.portainer_stack:
username: admin
password: "{{portainer.password}}"
docker_compose_file_path: "/etc/docker-compose/{{ item.name }}/docker-compose.yml"
stack_name: "{{ item.name }}"
endpoint_id: "{{ item.endpoint_id }}"
state: present
with_items:
- name: gitea
endpoint_id: 1
- name: plex
endpoint_id: 2
- name: mealie
endpoint_id: 3
# Delete plex stack
- name: Portainer | Delete Plex Stack
chatton.portainer.portainer_stack:
username: admin
password: "{{portainer.password}}"
stack_name: "plex"
endpoint_id: "2"
state: absent
"""
RETURN = r"""
# These are examples of possible return values, and in general should use other names for return values.
username:
description: The Portainer username.
type: str
returned: always
sample: 'admin'
password:
description: The provided user's password.
type: str
returned: never
sample: 'MyS00p3rS3cretPassw0rd'
docker_compose_file_path:
description: The path to a docker compose file which will be used to create the Portainer stack.
type: str
returned: never
sample: ''
"""
COMPOSE_STACK = 2
STRING_METHOD = "string"
def _create_stack(client, module, file_contents):
target_stack_name = module.params["stack_name"]
body = {
"name": target_stack_name,
"stackFileContent": file_contents,
}
query_params = {
"type": COMPOSE_STACK,
"method": STRING_METHOD,
"endpointId": client.endpoint,
}
return client.post("stacks", body=body, query_params=query_params)
def _update_stack(client, module, stack_id):
target_stack_name = module.params["stack_name"]
with open(module.params["docker_compose_file_path"]) as f:
file_contents = f.read()
return client.put(
f"stacks/{stack_id}?&endpointId={client.endpoint}",
body={
"name": target_stack_name,
"stackFileContent": file_contents,
},
)
def _handle_state_present(contents, result, client, module)
already_exists = False
target_stack_name = module.params["stack_name"]
for stack in stacks:
if stack["Name"] == target_stack_name and int(stack["EndpointId"]) == module.params["endpoint_id"]:
already_exists = True
result["stack_id"] = stack["Id"]
break
if not already_exists:
stack = _create_stack(client, module, contents)
result["changed"] = True
result["stack_id"] = stack["Id"]
module.exit_json(**result)
return
stack_id = result["stack_id"]
current_file_contents_resp = client.get(
f"stacks/{stack_id}/file", query_params={"endpointId": client.endpoint}
)
result["are_equal"] = (
current_file_contents_resp["StackFileContent"] == contents
)
if result["are_equal"]:
module.exit_json(**result)
return
# the stack exists and we have a new config.
_update_stack(client, module, stack_id)
result["changed"] = True
module.exit_json(**result)
def handle_state_present(client, module):
result = dict(changed=False, stack_name=module.params["stack_name"])
stacks = client.get("stacks")
result["stacks"] = stacks
contents = ""
if "docker_compose_file_path" in module.params:
with open(module.params["docker_compose_file_path"]) as f:
contents = f.read()
elif "stack_definition" in module.params:
contents = module.params["stack_definition"]
else:
raise ValueError("Should not be able to be here!")
_handle_state_present(contents, result, client, module)
def handle_state_absent(client, module):
result = dict(changed=False, stack_name=module.params["stack_name"])
already_exists = False
target_stack_name = module.params["stack_name"]
stacks = client.get("stacks")
for stack in stacks:
if stack["Name"] == target_stack_name:
already_exists = True
result["stack_id"] = stack["Id"]
break
if not already_exists:
module.exit_json(**result)
return
stack_id = result["stack_id"]
client.delete(
f"stacks/{stack_id}" + _query_params_to_string({"endpointId": client.endpoint})
)
result["changed"] = True
module.exit_json(**result)
def run_module():
# define available arguments/parameters a user can pass to the module
module_args = dict(
stack_name=dict(type="str", required=True),
docker_compose_file_path=dict(type="str"),
stack_definition=dict(type="str"),
username=dict(type="str", default="admin"),
password=dict(type="str", required=True, no_log=True),
endpoint_id=dict(type="int", required=True),
base_url=dict(type="str", default="http://localhost:9000"),
state=dict(type="str", default="present", choices=["present", "absent"]),
)
required_one_of = [
# docker compose file is only required if we are ensuring the stack is present.
["state", "present", ("docker_compose_file_path", "stack_definition")],
]
state_fns = {"present": handle_state_present, "absent": handle_state_absent}
# 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,
required_one_of=required_one_of,
# TODO: support check mode
supports_check_mode=False,
)
client = PortainerClient(
base_url=module.params["base_url"], endpoint=module.params["endpoint_id"]
)
client.login(module.params["username"], module.params["password"])
state_fns[module.params["state"]](client, module)
def main():
run_module()
if __name__ == "__main__":
main()