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
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
|
#!/usr/bin/python
# -*- coding: utf-8 -*-
# (c) 2017 David Gunter <david.gunter@tivix.com>
# Copyright (c) 2017 Chris Hoffman <christopher.hoffman@gmail.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 = '''
---
module: yarn
short_description: Manage node.js packages with Yarn
description:
- Manage node.js packages with the Yarn package manager (https://yarnpkg.com/)
author:
- "David Gunter (@verkaufer)"
- "Chris Hoffman (@chrishoffman), creator of NPM Ansible module)"
extends_documentation_fragment:
- community.general.attributes
attributes:
check_mode:
support: full
diff_mode:
support: none
options:
name:
type: str
description:
- The name of a node.js library to install
- If omitted all packages in package.json are installed.
- To globally install from local node.js library. Prepend "file:" to the path of the node.js library.
required: false
path:
type: path
description:
- The base path where Node.js libraries will be installed.
- This is where the node_modules folder lives.
required: false
version:
type: str
description:
- The version of the library to be installed.
- Must be in semver format. If "latest" is desired, use "state" arg instead
required: false
global:
description:
- Install the node.js library globally
required: false
default: false
type: bool
executable:
type: path
description:
- The executable location for yarn.
required: false
ignore_scripts:
description:
- Use the --ignore-scripts flag when installing.
required: false
type: bool
default: false
production:
description:
- Install dependencies in production mode.
- Yarn will ignore any dependencies under devDependencies in package.json
required: false
type: bool
default: false
registry:
type: str
description:
- The registry to install modules from.
required: false
state:
type: str
description:
- Installation state of the named node.js library
- If absent is selected, a name option must be provided
required: false
default: present
choices: [ "present", "absent", "latest" ]
requirements:
- Yarn installed in bin path (typically /usr/local/bin)
'''
EXAMPLES = '''
- name: Install "imagemin" node.js package.
community.general.yarn:
name: imagemin
path: /app/location
- name: Install "imagemin" node.js package on version 5.3.1
community.general.yarn:
name: imagemin
version: '5.3.1'
path: /app/location
- name: Install "imagemin" node.js package globally.
community.general.yarn:
name: imagemin
global: true
- name: Remove the globally-installed package "imagemin".
community.general.yarn:
name: imagemin
global: true
state: absent
- name: Install "imagemin" node.js package from custom registry.
community.general.yarn:
name: imagemin
registry: 'http://registry.mysite.com'
- name: Install packages based on package.json.
community.general.yarn:
path: /app/location
- name: Update all packages in package.json to their latest version.
community.general.yarn:
path: /app/location
state: latest
'''
RETURN = '''
changed:
description: Whether Yarn changed any package data
returned: always
type: bool
sample: true
msg:
description: Provides an error message if Yarn syntax was incorrect
returned: failure
type: str
sample: "Package must be explicitly named when uninstalling."
invocation:
description: Parameters and values used during execution
returned: success
type: dict
sample: {
"module_args": {
"executable": null,
"globally": false,
"ignore_scripts": false,
"name": null,
"path": "/some/path/folder",
"production": false,
"registry": null,
"state": "present",
"version": null
}
}
out:
description: Output generated from Yarn.
returned: always
type: str
sample: "yarn add v0.16.1[1/4] Resolving packages...[2/4] Fetching packages...[3/4] Linking dependencies...[4/4]
Building fresh packages...success Saved lockfile.success Saved 1 new dependency..left-pad@1.1.3 Done in 0.59s."
'''
import os
import json
from ansible.module_utils.basic import AnsibleModule
class Yarn(object):
def __init__(self, module, **kwargs):
self.module = module
self.globally = kwargs['globally']
self.name = kwargs['name']
self.version = kwargs['version']
self.path = kwargs['path']
self.registry = kwargs['registry']
self.production = kwargs['production']
self.ignore_scripts = kwargs['ignore_scripts']
self.executable = kwargs['executable']
# Specify a version of package if version arg passed in
self.name_version = None
if kwargs['version'] and self.name is not None:
self.name_version = self.name + '@' + str(self.version)
elif self.name is not None:
self.name_version = self.name
def _exec(self, args, run_in_check_mode=False, check_rc=True, unsupported_with_global=False):
if not self.module.check_mode or (self.module.check_mode and run_in_check_mode):
with_global_arg = self.globally and not unsupported_with_global
if with_global_arg:
# Yarn global arg is inserted before the command (e.g. `yarn global {some-command}`)
args.insert(0, 'global')
cmd = self.executable + args
if self.production:
cmd.append('--production')
if self.ignore_scripts:
cmd.append('--ignore-scripts')
if self.registry:
cmd.append('--registry')
cmd.append(self.registry)
# If path is specified, cd into that path and run the command.
cwd = None
if self.path and not with_global_arg:
if not os.path.exists(self.path):
# Module will make directory if not exists.
os.makedirs(self.path)
if not os.path.isdir(self.path):
self.module.fail_json(msg="Path provided %s is not a directory" % self.path)
cwd = self.path
if not os.path.isfile(os.path.join(self.path, 'package.json')):
self.module.fail_json(msg="Package.json does not exist in provided path.")
rc, out, err = self.module.run_command(cmd, check_rc=check_rc, cwd=cwd)
return out, err
return None, None
def _process_yarn_error(self, err):
try:
# We need to filter for errors, since Yarn warnings are included in stderr
for line in err.splitlines():
if json.loads(line)['type'] == 'error':
self.module.fail_json(msg=err)
except Exception:
self.module.fail_json(msg="Unexpected stderr output from Yarn: %s" % err, stderr=err)
def list(self):
cmd = ['list', '--depth=0', '--json']
installed = list()
missing = list()
if not os.path.isfile(os.path.join(self.path, 'yarn.lock')):
missing.append(self.name)
return installed, missing
# `yarn global list` should be treated as "unsupported with global" even though it exists,
# because it only only lists binaries, but `yarn global add` can install libraries too.
result, error = self._exec(cmd, run_in_check_mode=True, check_rc=False, unsupported_with_global=True)
self._process_yarn_error(error)
for json_line in result.strip().split('\n'):
data = json.loads(json_line)
if data['type'] == 'tree':
dependencies = data['data']['trees']
for dep in dependencies:
name, version = dep['name'].rsplit('@', 1)
installed.append(name)
if self.name not in installed:
missing.append(self.name)
return installed, missing
def install(self):
if self.name_version:
# Yarn has a separate command for installing packages by name...
return self._exec(['add', self.name_version])
# And one for installing all packages in package.json
return self._exec(['install', '--non-interactive'])
def update(self):
return self._exec(['upgrade', '--latest'])
def uninstall(self):
return self._exec(['remove', self.name])
def list_outdated(self):
outdated = list()
if not os.path.isfile(os.path.join(self.path, 'yarn.lock')):
return outdated
cmd_result, err = self._exec(['outdated', '--json'], True, False, unsupported_with_global=True)
# the package.json in the global dir is missing a license field, so warnings are expected on stderr
self._process_yarn_error(err)
if not cmd_result:
return outdated
outdated_packages_data = cmd_result.splitlines()[1]
data = json.loads(outdated_packages_data)
try:
outdated_dependencies = data['data']['body']
except KeyError:
return outdated
for dep in outdated_dependencies:
# Outdated dependencies returned as a list of lists, where
# item at index 0 is the name of the dependency
outdated.append(dep[0])
return outdated
def main():
arg_spec = dict(
name=dict(default=None),
path=dict(default=None, type='path'),
version=dict(default=None),
production=dict(default=False, type='bool'),
executable=dict(default=None, type='path'),
registry=dict(default=None),
state=dict(default='present', choices=['present', 'absent', 'latest']),
ignore_scripts=dict(default=False, type='bool'),
)
arg_spec['global'] = dict(default=False, type='bool')
module = AnsibleModule(
argument_spec=arg_spec,
supports_check_mode=True
)
name = module.params['name']
path = module.params['path']
version = module.params['version']
globally = module.params['global']
production = module.params['production']
registry = module.params['registry']
state = module.params['state']
ignore_scripts = module.params['ignore_scripts']
# When installing globally, users should not be able to define a path for installation.
# Require a path if global is False, though!
if path is None and globally is False:
module.fail_json(msg='Path must be specified when not using global arg')
elif path and globally is True:
module.fail_json(msg='Cannot specify path if doing global installation')
if state == 'absent' and not name:
module.fail_json(msg='Package must be explicitly named when uninstalling.')
if state == 'latest':
version = 'latest'
if module.params['executable']:
executable = module.params['executable'].split(' ')
else:
executable = [module.get_bin_path('yarn', True)]
# When installing globally, use the defined path for global node_modules
if globally:
_rc, out, _err = module.run_command(executable + ['global', 'dir'], check_rc=True)
path = out.strip()
yarn = Yarn(module,
name=name,
path=path,
version=version,
globally=globally,
production=production,
executable=executable,
registry=registry,
ignore_scripts=ignore_scripts)
changed = False
out = ''
err = ''
if state == 'present':
if not name:
changed = True
out, err = yarn.install()
else:
installed, missing = yarn.list()
if len(missing):
changed = True
out, err = yarn.install()
elif state == 'latest':
if not name:
changed = True
out, err = yarn.install()
else:
installed, missing = yarn.list()
outdated = yarn.list_outdated()
if len(missing):
changed = True
out, err = yarn.install()
if len(outdated):
changed = True
out, err = yarn.update()
else:
# state == absent
installed, missing = yarn.list()
if name in installed:
changed = True
out, err = yarn.uninstall()
module.exit_json(changed=changed, out=out, err=err)
if __name__ == '__main__':
main()
|