summaryrefslogtreecommitdiffstats
path: root/ansible_collections/community/general/plugins/modules/serverless.py
blob: 67d673d4d7af6d272d87f5830a3ff7fd6f1692df (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
#!/usr/bin/python
# -*- coding: utf-8 -*-

# Copyright (c) 2016, Ryan Scott Brown <ryansb@redhat.com>
# 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 absolute_import, division, print_function
__metaclass__ = type

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 'service_path' eg. 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 C(us-east-1).
    type: str
    default: ''
  deploy:
    description:
      - Whether or not to deploy artifacts after building them.
      - When this option is C(false) all the functions will be built, but no stack update will be 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
- yaml
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 IOError as e:
        module.fail_json(msg="Could not open serverless.yml in {0}. err: {1}".format(full_path, str(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 "{0}-{1}".format(config['service'], stage)

    return "{0}-{1}".format(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 = serverless_bin_path + " "
    else:
        command = module.get_bin_path("serverless") + " "

    if state == 'present':
        command += 'deploy '
    elif state == 'absent':
        command += 'remove '
    else:
        module.fail_json(msg="State must either be 'present' or 'absent'. Received: {0}".format(state))

    if state == 'present':
        if not deploy:
            command += '--noDeploy '
        elif force:
            command += '--force '

    if region:
        command += '--region {0} '.format(region)
    if stage:
        command += '--stage {0} '.format(stage)
    if verbose:
        command += '--verbose '

    rc, out, err = module.run_command(command, cwd=service_path)
    if rc != 0:
        if state == 'absent' and "-{0}' does not exist".format(stage) in out:
            module.exit_json(changed=False, state='absent', command=command,
                             out=out, service_name=get_service_name(module, stage))

        module.fail_json(msg="Failure when executing Serverless command. Exited {0}.\nstdout: {1}\nstderr: {2}".format(rc, out, 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()