#!/usr/bin/python # Copyright (c) 2016, Ryan Scott Brown # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations DOCUMENTATION = r""" module: serverless short_description: Manages a Serverless Framework project description: - Provides support for managing Serverless Framework (U(https://serverless.com/)) project deployments and stacks. extends_documentation_fragment: - community.general.attributes attributes: check_mode: support: none diff_mode: support: none options: state: description: - Goal state of given stage/project. type: str choices: [absent, present] default: present serverless_bin_path: description: - The path of a serverless framework binary relative to the O(service_path), for example V(node_module/.bin/serverless). type: path service_path: description: - The path to the root of the Serverless Service to be operated on. type: path required: true stage: description: - The name of the serverless framework project stage to deploy to. - This uses the serverless framework default "dev". type: str default: '' region: description: - AWS region to deploy the service to. - This parameter defaults to V(us-east-1). type: str default: '' deploy: description: - Whether or not to deploy artifacts after building them. - When this option is V(false) all the functions are built, but no stack update is run to send them out. - This is mostly useful for generating artifacts to be stored/deployed elsewhere. type: bool default: true force: description: - Whether or not to force full deployment, equivalent to serverless C(--force) option. type: bool default: false verbose: description: - Shows all stack events during deployment, and display any Stack Output. type: bool default: false notes: - Currently, the C(serverless) command must be in the path of the node executing the task. In the future this may be a flag. requirements: - serverless - PyYAML author: - Ryan Scott Brown (@ryansb) """ EXAMPLES = r""" - name: Basic deploy of a service community.general.serverless: service_path: '{{ project_dir }}' state: present - name: Deploy a project, then pull its resource list back into Ansible community.general.serverless: stage: dev region: us-east-1 service_path: '{{ project_dir }}' register: sls # The cloudformation stack is always named the same as the full service, so the # cloudformation_info module can get a full list of the stack resources, as # well as stack events and outputs - cloudformation_info: region: us-east-1 stack_name: '{{ sls.service_name }}' stack_resources: true - name: Deploy a project using a locally installed serverless binary community.general.serverless: stage: dev region: us-east-1 service_path: '{{ project_dir }}' serverless_bin_path: node_modules/.bin/serverless """ RETURN = r""" service_name: type: str description: The service name specified in the serverless.yml that was just deployed. returned: always sample: my-fancy-service-dev state: type: str description: Whether the stack for the serverless project is present/absent. returned: always command: type: str description: Full C(serverless) command run by this module, in case you want to re-run the command outside the module. returned: always sample: serverless deploy --stage production """ import os try: import yaml HAS_YAML = True except ImportError: HAS_YAML = False from ansible.module_utils.basic import AnsibleModule def read_serverless_config(module): path = module.params.get("service_path") full_path = os.path.join(path, "serverless.yml") try: with open(full_path) as sls_config: config = yaml.safe_load(sls_config.read()) return config except OSError as e: module.fail_json(msg=f"Could not open serverless.yml in {full_path}. err: {e}") def get_service_name(module, stage): config = read_serverless_config(module) if config.get("service") is None: module.fail_json(msg="Could not read `service` key from serverless.yml file") if stage: return f"{config['service']}-{stage}" return f"{config['service']}-{config.get('stage', 'dev')}" def main(): module = AnsibleModule( argument_spec=dict( service_path=dict(type="path", required=True), state=dict(type="str", default="present", choices=["absent", "present"]), region=dict(type="str", default=""), stage=dict(type="str", default=""), deploy=dict(type="bool", default=True), serverless_bin_path=dict(type="path"), force=dict(type="bool", default=False), verbose=dict(type="bool", default=False), ), ) if not HAS_YAML: module.fail_json(msg="yaml is required for this module") service_path = module.params.get("service_path") state = module.params.get("state") region = module.params.get("region") stage = module.params.get("stage") deploy = module.params.get("deploy", True) force = module.params.get("force", False) verbose = module.params.get("verbose", False) serverless_bin_path = module.params.get("serverless_bin_path") if serverless_bin_path is not None: command = f"{serverless_bin_path} " else: command = f"{module.get_bin_path('serverless')} " if state == "present": command += "deploy " elif state == "absent": command += "remove " else: module.fail_json(msg=f"State must either be 'present' or 'absent'. Received: {state}") if state == "present": if not deploy: command += "--noDeploy " elif force: command += "--force " if region: command += f"--region {region} " if stage: command += f"--stage {stage} " if verbose: command += "--verbose " rc, out, err = module.run_command(command, cwd=service_path) if rc != 0: if state == "absent" and f"-{stage}' does not exist" in out: module.exit_json( changed=False, state="absent", command=command, out=out, service_name=get_service_name(module, stage) ) module.fail_json(msg=f"Failure when executing Serverless command. Exited {rc}.\nstdout: {out}\nstderr: {err}") # gather some facts about the deployment module.exit_json( changed=True, state="present", out=out, command=command, service_name=get_service_name(module, stage) ) if __name__ == "__main__": main()