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

240 lines
6.8 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(client, module):
result = dict(
changed=False,
stack_name=module.params["stack_name"]
)
already_exists = False
stacks = client.get("stacks")
result["stacks"] = stacks
with open(module.params["docker_compose_file_path"]) as f:
file_contents = f.read()
target_stack_name = module.params["stack_name"]
for stack in stacks:
if stack["Name"] == target_stack_name:
already_exists = True
result["stack_id"] = stack["Id"]
break
if not already_exists:
stack = _create_stack(client, module, file_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"] == file_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_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'),
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_if = [
# docker compose file is only required if we are ensuring the stack is present.
['state', 'present', ('docker_compose_file_path',)],
]
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_if=required_if,
# 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()