From aa1a9f1587fd8074afb04138910132259723c888 Mon Sep 17 00:00:00 2001 From: Pierre Date: Sat, 6 Oct 2018 01:40:34 +0000 Subject: [PATCH] Extract molecule to subgenerator (#36) * Add molecule subgenerator skel * Move molecule generation to its own subgenerator * Remove extra prompts overrides * Mock molecule subgenerator * Spy on mocked generator * Add molecule subgenerator options * Extract prompts to module * Fix options * 0.3.0 --- __tests__/__helpers__/mockGenerator.js | 21 ++ __tests__/__snapshots__/app.js.snap | 136 ---------- __tests__/__snapshots__/molecule.js.snap | 231 ++++++++++++++++ __tests__/app.js | 122 ++------- __tests__/molecule.js | 246 ++++++++++++++++++ generators/app/index.js | 202 +------------- generators/app/prompts.js | 150 +++++++++++ generators/molecule/index.js | 98 +++++++ generators/molecule/prompts.js | 38 +++ generators/molecule/templates/.yamllint | 10 + .../{app => molecule}/templates/create.yml | 0 .../{app => molecule}/templates/destroy.yml | 0 generators/molecule/templates/gitignore | 2 + .../templates/molecule.yml.ejs | 3 + .../templates/playbook.yml.ejs | 2 +- .../templates/test_default.py | 0 package-lock.json | 2 +- package.json | 2 +- 18 files changed, 826 insertions(+), 439 deletions(-) create mode 100644 __tests__/__helpers__/mockGenerator.js create mode 100644 __tests__/__snapshots__/molecule.js.snap create mode 100644 __tests__/molecule.js create mode 100644 generators/app/prompts.js create mode 100644 generators/molecule/index.js create mode 100644 generators/molecule/prompts.js create mode 100644 generators/molecule/templates/.yamllint rename generators/{app => molecule}/templates/create.yml (100%) rename generators/{app => molecule}/templates/destroy.yml (100%) create mode 100644 generators/molecule/templates/gitignore rename generators/{app => molecule}/templates/molecule.yml.ejs (81%) rename generators/{app => molecule}/templates/playbook.yml.ejs (60%) rename generators/{app => molecule}/templates/test_default.py (100%) diff --git a/__tests__/__helpers__/mockGenerator.js b/__tests__/__helpers__/mockGenerator.js new file mode 100644 index 0000000..deb54a2 --- /dev/null +++ b/__tests__/__helpers__/mockGenerator.js @@ -0,0 +1,21 @@ +var Generator = require('yeoman-generator'); + +const spy = jest.fn(); +const mockGenerator = () => + class extends Generator { + constructor(args, opts) { + super(args, opts); + spy(args, opts); + } + + test() { + // This method is required so that it doesn't complain about the + // generator having no method to run. It doesn't do anything useful other + // than that. + } + }; + +module.exports = { + mockGenerator, + spy, +}; diff --git a/__tests__/__snapshots__/app.js.snap b/__tests__/__snapshots__/app.js.snap index e05d9ad..30b3301 100644 --- a/__tests__/__snapshots__/app.js.snap +++ b/__tests__/__snapshots__/app.js.snap @@ -199,112 +199,6 @@ galaxy_info: " `; -exports[`generator-molecule-lxd-role:app when all the prompts have answers molecule/create.yml is correctly formatted 1`] = ` -"--- -- name: Create - hosts: localhost - connection: local - gather_facts: false - no_log: \\"{{ not lookup('env', 'MOLECULE_DEBUG') | bool }}\\" - tasks: - - name: Create molecule instance(s) - lxd_container: - name: \\"{{ item.name }}\\" - state: started - source: - type: image - mode: pull - server: https://images.linuxcontainers.org - protocol: simplestreams - alias: \\"{{ item.alias }}/amd64\\" - profiles: [\\"default\\"] - wait_for_ipv4_addresses: true - timeout: 600 - with_items: \\"{{ molecule_yml.platforms }}\\" - - - name: Install Python in container - delegate_to: \\"{{ item.name }}\\" - raw: apt-get install -y python - with_items: \\"{{ molecule_yml.platforms }}\\" -" -`; - -exports[`generator-molecule-lxd-role:app when all the prompts have answers molecule/default/molecule.yml is correctly formatted 1`] = ` -"--- -dependency: - name: galaxy -driver: - name: lxd -lint: - name: yamllint -platforms: - - name: ubuntu-trusty - alias: ubuntu/trusty - - name: ubuntu-xenial - alias: ubuntu/xenial - - name: ubuntu-bionic - alias: ubuntu/bionic - -provisioner: - name: ansible - playbooks: - create: ../create.yml - destroy: ../destroy.yml - lint: - name: ansible-lint -scenario: - name: default -verifier: - name: testinfra - lint: - name: flake8 -" -`; - -exports[`generator-molecule-lxd-role:app when all the prompts have answers molecule/default/playbook.yml is correctly formatted 1`] = ` -"--- -- name: Converge - hosts: all - roles: - - name: ansible-role-test-role -" -`; - -exports[`generator-molecule-lxd-role:app when all the prompts have answers molecule/default/tests/test_default.py is correctly formatted 1`] = ` -"import os - -import testinfra.utils.ansible_runner - -testinfra_hosts = testinfra.utils.ansible_runner.AnsibleRunner( - os.environ['MOLECULE_INVENTORY_FILE']).get_hosts('all') - - -def test_example(host): - file = host.file('/etc/hosts') - - assert file.exists - assert file.user == 'root' - assert file.group == 'root' -" -`; - -exports[`generator-molecule-lxd-role:app when all the prompts have answers molecule/destroy.yml is correctly formatted 1`] = ` -"--- -- name: Destroy - hosts: localhost - connection: local - gather_facts: false - no_log: \\"{{ not lookup('env', 'MOLECULE_DEBUG') | bool }}\\" - tasks: - - name: Destroy molecule instance(s) - lxd_container: - name: \\"{{ item.name }}\\" - state: absent - force_stop: \\"{{ item.force_stop | default(true) }}\\" - with_items: \\"{{ molecule_yml.platforms }}\\" -" -`; - exports[`generator-molecule-lxd-role:app when all the prompts have answers tasks/main.yml is correctly formatted 1`] = ` "--- " @@ -315,36 +209,6 @@ exports[`generator-molecule-lxd-role:app when all the prompts have answers vars/ " `; -exports[`generator-molecule-lxd-role:app when targetting another distribution than Ubuntu molecule/default/molecule.yml formats the platform list properly 1`] = ` -"--- -dependency: - name: galaxy -driver: - name: lxd -lint: - name: yamllint -platforms: - - name: debian-jessie - alias: debian/jessie - - name: debian-stretch - alias: debian/stretch - -provisioner: - name: ansible - playbooks: - create: ../create.yml - destroy: ../destroy.yml - lint: - name: ansible-lint -scenario: - name: default -verifier: - name: testinfra - lint: - name: flake8 -" -`; - exports[`generator-molecule-lxd-role:app when there are no dependencies README.md skips the dependencies section 1`] = ` "Test role ========= diff --git a/__tests__/__snapshots__/molecule.js.snap b/__tests__/__snapshots__/molecule.js.snap new file mode 100644 index 0000000..209caef --- /dev/null +++ b/__tests__/__snapshots__/molecule.js.snap @@ -0,0 +1,231 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`generator-molecule-lxd-role:molecule when all the prompts have answers .gitignore is correctly formatted 1`] = ` +"*.pyc +*.log +" +`; + +exports[`generator-molecule-lxd-role:molecule when all the prompts have answers .yamllint is correctly formatted 1`] = ` +"extends: default + +rules: + braces: + max-spaces-inside: 1 + level: error + brackets: + max-spaces-inside: 1 + level: error + line-length: disable +" +`; + +exports[`generator-molecule-lxd-role:molecule when all the prompts have answers molecule/create.yml is correctly formatted 1`] = ` +"--- +- name: Create + hosts: localhost + connection: local + gather_facts: false + no_log: \\"{{ not lookup('env', 'MOLECULE_DEBUG') | bool }}\\" + tasks: + - name: Create molecule instance(s) + lxd_container: + name: \\"{{ item.name }}\\" + state: started + source: + type: image + mode: pull + server: https://images.linuxcontainers.org + protocol: simplestreams + alias: \\"{{ item.alias }}/amd64\\" + profiles: [\\"default\\"] + wait_for_ipv4_addresses: true + timeout: 600 + with_items: \\"{{ molecule_yml.platforms }}\\" + + - name: Install Python in container + delegate_to: \\"{{ item.name }}\\" + raw: apt-get install -y python + with_items: \\"{{ molecule_yml.platforms }}\\" +" +`; + +exports[`generator-molecule-lxd-role:molecule when all the prompts have answers molecule/default/molecule.yml is correctly formatted 1`] = ` +"--- +dependency: + name: galaxy +driver: + name: lxd +lint: + name: yamllint +platforms: + - name: ubuntu-trusty + alias: ubuntu/trusty + - name: ubuntu-xenial + alias: ubuntu/xenial + - name: ubuntu-bionic + alias: ubuntu/bionic + +provisioner: + name: ansible + playbooks: + create: ../create.yml + destroy: ../destroy.yml + lint: + name: ansible-lint +scenario: + name: default +verifier: + name: testinfra + lint: + name: flake8 +" +`; + +exports[`generator-molecule-lxd-role:molecule when all the prompts have answers molecule/default/playbook.yml is correctly formatted 1`] = ` +"--- +- name: Converge + hosts: all + roles: + - name: ansible-role-test-role +" +`; + +exports[`generator-molecule-lxd-role:molecule when all the prompts have answers molecule/default/tests/test_default.py is correctly formatted 1`] = ` +"import os + +import testinfra.utils.ansible_runner + +testinfra_hosts = testinfra.utils.ansible_runner.AnsibleRunner( + os.environ['MOLECULE_INVENTORY_FILE']).get_hosts('all') + + +def test_example(host): + file = host.file('/etc/hosts') + + assert file.exists + assert file.user == 'root' + assert file.group == 'root' +" +`; + +exports[`generator-molecule-lxd-role:molecule when all the prompts have answers molecule/destroy.yml is correctly formatted 1`] = ` +"--- +- name: Destroy + hosts: localhost + connection: local + gather_facts: false + no_log: \\"{{ not lookup('env', 'MOLECULE_DEBUG') | bool }}\\" + tasks: + - name: Destroy molecule instance(s) + lxd_container: + name: \\"{{ item.name }}\\" + state: absent + force_stop: \\"{{ item.force_stop | default(true) }}\\" + with_items: \\"{{ molecule_yml.platforms }}\\" +" +`; + +exports[`generator-molecule-lxd-role:molecule when generating for a playbook molecule/default/molecule.yml is correctly formatted 1`] = ` +"--- +dependency: + name: galaxy +driver: + name: lxd +lint: + name: yamllint +platforms: + - name: ubuntu-trusty + alias: ubuntu/trusty + - name: ubuntu-xenial + alias: ubuntu/xenial + - name: ubuntu-bionic + alias: ubuntu/bionic + +provisioner: + name: ansible + playbooks: + create: ../create.yml + converge: ../../playbook.yml + destroy: ../destroy.yml + lint: + name: ansible-lint +scenario: + name: default +verifier: + name: testinfra + lint: + name: flake8 +" +`; + +exports[`generator-molecule-lxd-role:molecule when generating for a role molecule/default/molecule.yml is correctly formatted 1`] = ` +"--- +dependency: + name: galaxy +driver: + name: lxd +lint: + name: yamllint +platforms: + - name: ubuntu-trusty + alias: ubuntu/trusty + - name: ubuntu-xenial + alias: ubuntu/xenial + - name: ubuntu-bionic + alias: ubuntu/bionic + +provisioner: + name: ansible + playbooks: + create: ../create.yml + destroy: ../destroy.yml + lint: + name: ansible-lint +scenario: + name: default +verifier: + name: testinfra + lint: + name: flake8 +" +`; + +exports[`generator-molecule-lxd-role:molecule when generating for a role molecule/default/playbook.yml is correctly formatted 1`] = ` +"--- +- name: Converge + hosts: all + roles: + - name: ansible-role-test-role +" +`; + +exports[`generator-molecule-lxd-role:molecule when targetting another distribution than Ubuntu molecule/default/molecule.yml formats the platform list properly 1`] = ` +"--- +dependency: + name: galaxy +driver: + name: lxd +lint: + name: yamllint +platforms: + - name: debian-jessie + alias: debian/jessie + - name: debian-stretch + alias: debian/stretch + +provisioner: + name: ansible + playbooks: + create: ../create.yml + destroy: ../destroy.yml + lint: + name: ansible-lint +scenario: + name: default +verifier: + name: testinfra + lint: + name: flake8 +" +`; diff --git a/__tests__/app.js b/__tests__/app.js index 663bc69..8694c80 100644 --- a/__tests__/app.js +++ b/__tests__/app.js @@ -6,6 +6,12 @@ const assert = require('yeoman-assert'); const helpers = require('yeoman-test'); const path = require('path'); +const { mockGenerator, spy } = require('./__helpers__/mockGenerator'); + +jest.mock('../generators/molecule', () => { + return mockGenerator(); +}); + describe('generator-molecule-lxd-role:app', () => { const defaultResponses = { roleName: 'Test role', @@ -54,83 +60,18 @@ describe('generator-molecule-lxd-role:app', () => { const clonedResponses = clone(defaultResponses); beforeAll(() => { + spy.mockClear(); return helpers .run(path.join(__dirname, '../generators/app')) .withPrompts(clonedResponses); }); - describe('README.md', () => { - const filePath = 'README.md'; - - it('exists', () => { - assert.file(filePath); - }); - - it('is correctly formatted', () => { - const actual = readFileSync(filePath, 'utf8'); - - expect(actual).toMatchSnapshot(); - }); + it('uses the molecule subgenerator', () => { + expect(spy).toHaveBeenCalledWith([], expect.objectContaining({ mode: 'role' })); }); - describe('molecule/create.yml', () => { - const filePath = 'molecule/create.yml'; - - it('exists', () => { - assert.file(filePath); - }); - - it('is correctly formatted', () => { - const actual = readFileSync(filePath, 'utf8'); - - expect(actual).toMatchSnapshot(); - }); - }); - - describe('molecule/destroy.yml', () => { - const filePath = 'molecule/destroy.yml'; - - it('exists', () => { - assert.file(filePath); - }); - - it('is correctly formatted', () => { - const actual = readFileSync(filePath, 'utf8'); - - expect(actual).toMatchSnapshot(); - }); - }); - - describe('molecule/default/molecule.yml', () => { - const filePath = 'molecule/default/molecule.yml'; - - it('exists', () => { - assert.file(filePath); - }); - - it('is correctly formatted', () => { - const actual = readFileSync(filePath, 'utf8'); - - expect(actual).toMatchSnapshot(); - }); - }); - - describe('molecule/default/playbook.yml', () => { - const filePath = 'molecule/default/playbook.yml'; - - it('exists', () => { - assert.file(filePath); - }); - - it('is correctly formatted', () => { - const actual = readFileSync(filePath, 'utf8'); - - expect(actual).toMatchSnapshot(); - }); - }); - - describe('molecule/default/tests/test_default.py', () => { - const filePath = 'molecule/default/tests/test_default.py'; + describe('README.md', () => { + const filePath = 'README.md'; it('exists', () => { assert.file(filePath); @@ -281,6 +222,10 @@ describe('generator-molecule-lxd-role:app', () => { .withPrompts(clonedResponses); }); + it('uses the molecule subgenerator', () => { + expect(spy).toHaveBeenCalledWith([], expect.objectContaining({ mode: 'role' })); + }); + describe('README.md', () => { it('formats the author information properly', () => { const actual = readFileSync('README.md', 'utf8'); @@ -408,43 +353,6 @@ describe('generator-molecule-lxd-role:app', () => { }); }); - describe('when targetting another distribution than Ubuntu', () => { - const clonedResponses = clone(defaultResponses); - clonedResponses.targetDistributions = ['DEBIAN']; - clonedResponses.targetVersions = [ - { - family: 'debian', - distribution: 'debian', - codeName: 'jessie', - versionNumber: '8', - tags: ['current'], - }, - { - family: 'debian', - distribution: 'debian', - codeName: 'stretch', - versionNumber: '9', - tags: ['lts'], - }, - ]; - - beforeAll(() => { - return helpers - .run(path.join(__dirname, '../generators/app')) - .withPrompts(clonedResponses); - }); - - describe('molecule/default/molecule.yml', () => { - const filePath = 'molecule/default/molecule.yml'; - - it('formats the platform list properly', () => { - const actual = readFileSync(filePath, 'utf8'); - - expect(actual).toMatchSnapshot(); - }); - }); - }); - describe('when Travis disabled', () => { const clonedResponses = clone(defaultResponses); clonedResponses.useTravis = false; diff --git a/__tests__/molecule.js b/__tests__/molecule.js new file mode 100644 index 0000000..631e57c --- /dev/null +++ b/__tests__/molecule.js @@ -0,0 +1,246 @@ +'use strict'; +const { clone } = require('ramda'); + +const { readFileSync } = require('fs'); +const assert = require('yeoman-assert'); +const helpers = require('yeoman-test'); +const path = require('path'); + +describe('generator-molecule-lxd-role:molecule', () => { + const subgeneratorPath = '../generators/molecule'; + const defaultResponses = { + repoName: 'ansible-role-test-role', + targetDistributions: ['UBUNTU'], + targetVersions: [ + { + family: 'debian', + distribution: 'ubuntu', + codeName: 'trusty', + versionNumber: '14.04', + tags: ['lts', 'current'], + }, + { + family: 'debian', + distribution: 'ubuntu', + codeName: 'xenial', + versionNumber: '16.04', + tags: ['lts', 'current'], + }, + { + family: 'debian', + distribution: 'ubuntu', + codeName: 'bionic', + versionNumber: '18.04', + tags: ['lts', 'current'], + }, + ], + }; + describe('when all the prompts have answers', () => { + const clonedResponses = clone(defaultResponses); + + beforeAll(() => { + return helpers + .run(path.join(__dirname, subgeneratorPath)) + .withPrompts(clonedResponses); + }); + + describe('molecule/create.yml', () => { + const filePath = 'molecule/create.yml'; + + it('exists', () => { + assert.file(filePath); + }); + + it('is correctly formatted', () => { + const actual = readFileSync(filePath, 'utf8'); + + expect(actual).toMatchSnapshot(); + }); + }); + + describe('molecule/destroy.yml', () => { + const filePath = 'molecule/destroy.yml'; + + it('exists', () => { + assert.file(filePath); + }); + + it('is correctly formatted', () => { + const actual = readFileSync(filePath, 'utf8'); + + expect(actual).toMatchSnapshot(); + }); + }); + + describe('molecule/default/molecule.yml', () => { + const filePath = 'molecule/default/molecule.yml'; + + it('exists', () => { + assert.file(filePath); + }); + + it('is correctly formatted', () => { + const actual = readFileSync(filePath, 'utf8'); + + expect(actual).toMatchSnapshot(); + }); + }); + + describe('molecule/default/playbook.yml', () => { + const filePath = 'molecule/default/playbook.yml'; + + it('exists', () => { + assert.file(filePath); + }); + + it('is correctly formatted', () => { + const actual = readFileSync(filePath, 'utf8'); + + expect(actual).toMatchSnapshot(); + }); + }); + + describe('molecule/default/tests/test_default.py', () => { + const filePath = 'molecule/default/tests/test_default.py'; + + it('exists', () => { + assert.file(filePath); + }); + + it('is correctly formatted', () => { + const actual = readFileSync(filePath, 'utf8'); + + expect(actual).toMatchSnapshot(); + }); + }); + + describe('.yamllint', () => { + const filePath = '.yamllint'; + + it('exists', () => { + assert.file(filePath); + }); + + it('is correctly formatted', () => { + const actual = readFileSync(filePath, 'utf8'); + + expect(actual).toMatchSnapshot(); + }); + }); + + describe('.gitignore', () => { + const filePath = '.gitignore'; + + it('exists', () => { + assert.file(filePath); + }); + + it('is correctly formatted', () => { + const actual = readFileSync(filePath, 'utf8'); + + expect(actual).toMatchSnapshot(); + }); + }); + }); + + describe('when targetting another distribution than Ubuntu', () => { + const clonedResponses = clone(defaultResponses); + clonedResponses.targetDistributions = ['DEBIAN']; + clonedResponses.targetVersions = [ + { + family: 'debian', + distribution: 'debian', + codeName: 'jessie', + versionNumber: '8', + tags: ['current'], + }, + { + family: 'debian', + distribution: 'debian', + codeName: 'stretch', + versionNumber: '9', + tags: ['lts'], + }, + ]; + + beforeAll(() => { + return helpers + .run(path.join(__dirname, subgeneratorPath)) + .withPrompts(clonedResponses); + }); + + describe('molecule/default/molecule.yml', () => { + const filePath = 'molecule/default/molecule.yml'; + + it('formats the platform list properly', () => { + const actual = readFileSync(filePath, 'utf8'); + + expect(actual).toMatchSnapshot(); + }); + }); + }); + + describe('when generating for a role', () => { + const clonedResponses = clone(defaultResponses); + + beforeAll(() => { + return helpers + .run(path.join(__dirname, subgeneratorPath)) + .withOptions({ mode: 'role' }) + .withPrompts(clonedResponses); + }); + + describe('molecule/default/molecule.yml', () => { + const filePath = 'molecule/default/molecule.yml'; + + it('is correctly formatted', () => { + const actual = readFileSync(filePath, 'utf8'); + + expect(actual).toMatchSnapshot(); + }); + }); + + describe('molecule/default/playbook.yml', () => { + const filePath = 'molecule/default/playbook.yml'; + + it('exists', () => { + assert.file(filePath); + }); + + it('is correctly formatted', () => { + const actual = readFileSync(filePath, 'utf8'); + + expect(actual).toMatchSnapshot(); + }); + }); + }); + + describe('when generating for a playbook', () => { + const clonedResponses = clone(defaultResponses); + + beforeAll(() => { + return helpers + .run(path.join(__dirname, subgeneratorPath)) + .withOptions({ mode: 'playbook' }) + .withPrompts(clonedResponses); + }); + + describe('molecule/default/molecule.yml', () => { + const filePath = 'molecule/default/molecule.yml'; + + it('is correctly formatted', () => { + const actual = readFileSync(filePath, 'utf8'); + + expect(actual).toMatchSnapshot(); + }); + }); + + describe('molecule/default/playbook.yml', () => { + const filePath = 'molecule/default/playbook.yml'; + + it('does not exist', () => { + assert.noFile(filePath); + }); + }); + }); +}); diff --git a/generators/app/index.js b/generators/app/index.js index b8d400c..5093cf1 100644 --- a/generators/app/index.js +++ b/generators/app/index.js @@ -1,31 +1,13 @@ 'use strict'; -const { - either, - forEach, - isEmpty, - isNil, - map, - not, - prop, - split, - toUpper, -} = require('ramda'); +const { forEach } = require('ramda'); const { paramCase } = require('change-case'); -const { safeDump } = require('js-yaml'); const Generator = require('yeoman-generator'); -const chalk = require('chalk'); const mkdirp = require('mkdirp'); const path = require('path'); -const { ANSIBLE_VERSIONS, LICENSES, PLATFORMS, URLS } = require('../constants'); -const { - indentYaml, - listPlatforms, - listVersions, - moleculePlatforms, - parseDeps, -} = require('../helpers'); +const { indentYaml, parseDeps } = require('../helpers'); +const prompts = require('./prompts'); module.exports = class extends Generator { prompting() { @@ -33,153 +15,17 @@ module.exports = class extends Generator { 'This generator will create an Ansible role and test it with Molecule using LXD containers.', ); - const prompts = [ - { - type: 'input', - name: 'roleName', - message: "What is the new role's name?", - validate: answer => not(either(isEmpty, isNil)(answer)), - }, - { - type: 'input', - name: 'repoName', - message: 'What directory will the role be created in?', - default: answers => `ansible-role-${paramCase(answers.roleName)}`, - }, - { - type: 'input', - name: 'authorName', - message: "Who is this role's author (full name or nickname)?", - store: true, - validate: answer => not(either(isEmpty, isNil)(answer)), - }, - { - type: 'input', - name: 'authorOrganization', - message: `Which organization is this role published under? ${chalk.reset.gray.italic( - '(optional)', - )}`, - store: true, - default: '', - }, - { - type: 'input', - name: 'authorWebsite', - message: `What is the website for the company/author of this role? ${chalk.reset.gray.italic( - '(optional)', - )}`, - store: true, - default: '', - }, - { - type: 'input', - name: 'roleDesc', - message: `How would you describe this role's purpose in a few words? ${chalk.reset.gray.italic( - 'Markdown supported.', - )}`, - validate: answer => not(either(isEmpty, isNil)(answer)), - }, - { - type: 'list', - name: 'minAnsibleVer', - message: 'What is the minimal Ansible version required to run this role?', - choices: ANSIBLE_VERSIONS, - default: '2.4', - store: true, - }, - { - type: 'checkbox', - name: 'targetDistributions', - message: `Which distributions does this role target? ${chalk.reset.gray.italic( - 'Is your favourite distribution missing? Let us know here: ' + URLS.ISSUES, - )}`, - choices: listPlatforms(PLATFORMS), - store: true, - validate: answer => not(either(isEmpty, isNil)(answer)), - filter: map(toUpper), - }, - { - type: 'checkbox', - name: 'targetVersions', - message: `Which versions does this role support? ${chalk.reset.gray.italic( - 'Are the versions outdated? File an issue here: ' + URLS.ISSUES, - )}`, - choices: answers => listVersions(prop('targetDistributions', answers)), - store: true, - validate: answer => not(either(isEmpty, isNil)(answer)), - }, - { - type: 'confirm', - name: 'useTravis', - message: 'Use Travis CI?', - default: true, - store: true, - }, - { - when: answers => answers.useTravis, - type: 'input', - name: 'travisUsername', - message: 'What is your Travis CI username?', - store: true, - validate: answer => not(either(isEmpty, isNil)(answer)), - }, - { - type: 'list', - name: 'license', - message: `Which license for this role? ${chalk.reset.gray.italic( - 'For help choosing, see ' + URLS.LICENSE_INFO, - )}`, - choices: LICENSES, - default: 'MIT', - store: true, - }, - { - type: 'input', - name: 'galaxyTags', - message: `Which Galaxy tags to give the role? ${chalk.reset.gray.italic( - '(optional; single words, comma separated)', - )}`, - filter: answer => split(', ', answer), - default: '', - }, - { - type: 'confirm', - name: 'hasReqs', - message: 'Does this role have any particular requirements?', - default: false, - }, - { - when: answers => answers.hasReqs, - type: 'editor', - name: 'roleReqs', - message: `Enter this role's requirements. ${chalk.reset.gray.italic( - 'Usually details specific OS requirements, assumptions, etc. Markdown valid here.', - )}`, - validate: answer => not(either(isEmpty, isNil)(answer)), - }, - { - type: 'confirm', - name: 'hasDeps', - message: 'Does this role depend on any other?', - default: false, - }, - { - when: answers => answers.hasDeps, - type: 'editor', - name: 'roleDeps', - message: - 'Enter the roles on which your role will depend. See ' + - URLS.DEPENDENCIES_FORMAT + - " for how to format your entries, they'll be inserted verbatim into a requirements.yml file.", - validate: answer => not(either(isEmpty, isNil)(answer)), - }, - ]; - return this.prompt(prompts).then(props => { this.props = props; }); } + default() { + this.composeWith(require.resolve('../molecule'), { + mode: 'role', + }); + } + writing() { const p = this.props; const destinationPath = p.repoName; @@ -195,7 +41,6 @@ module.exports = class extends Generator { 'defaults', 'handlers', 'meta', - 'molecule/default/tests', 'tasks', 'templates', 'files', @@ -229,35 +74,6 @@ module.exports = class extends Generator { this.fs.copy(this.templatePath('gitignore'), this.destinationPath('.gitignore')); - this.fs.copy( - this.templatePath('create.yml'), - this.destinationPath('molecule/create.yml'), - ); - - this.fs.copy( - this.templatePath('destroy.yml'), - this.destinationPath('molecule/destroy.yml'), - ); - - this.fs.copyTpl( - this.templatePath('molecule.yml.ejs'), - this.destinationPath('molecule/default/molecule.yml'), - { - platforms: safeDump({ platforms: moleculePlatforms(p.targetVersions) }), - }, - ); - - this.fs.copyTpl( - this.templatePath('playbook.yml.ejs'), - this.destinationPath('molecule/default/playbook.yml'), - { repoName: p.repoName }, - ); - - this.fs.copy( - this.templatePath('test_default.py'), - this.destinationPath('molecule/default/tests/test_default.py'), - ); - const mains = ['defaults', 'handlers', 'tasks', 'vars']; forEach( f => diff --git a/generators/app/prompts.js b/generators/app/prompts.js new file mode 100644 index 0000000..63e8155 --- /dev/null +++ b/generators/app/prompts.js @@ -0,0 +1,150 @@ +const { either, isEmpty, isNil, map, not, prop, split, toUpper } = require('ramda'); +const { paramCase } = require('change-case'); +const chalk = require('chalk'); + +const { ANSIBLE_VERSIONS, LICENSES, PLATFORMS, URLS } = require('../constants'); +const { listPlatforms, listVersions } = require('../helpers'); + +const prompts = [ + { + type: 'input', + name: 'roleName', + message: "What is the new role's name?", + validate: answer => not(either(isEmpty, isNil)(answer)), + }, + { + type: 'input', + name: 'repoName', + message: 'What directory will the role be created in?', + default: answers => `ansible-role-${paramCase(answers.roleName)}`, + }, + { + type: 'input', + name: 'authorName', + message: "Who is this role's author (full name or nickname)?", + store: true, + validate: answer => not(either(isEmpty, isNil)(answer)), + }, + { + type: 'input', + name: 'authorOrganization', + message: `Which organization is this role published under? ${chalk.reset.gray.italic( + '(optional)', + )}`, + store: true, + default: '', + }, + { + type: 'input', + name: 'authorWebsite', + message: `What is the website for the company/author of this role? ${chalk.reset.gray.italic( + '(optional)', + )}`, + store: true, + default: '', + }, + { + type: 'input', + name: 'roleDesc', + message: `How would you describe this role's purpose in a few words? ${chalk.reset.gray.italic( + 'Markdown supported.', + )}`, + validate: answer => not(either(isEmpty, isNil)(answer)), + }, + { + type: 'list', + name: 'minAnsibleVer', + message: 'What is the minimal Ansible version required to run this role?', + choices: ANSIBLE_VERSIONS, + default: '2.4', + store: true, + }, + { + type: 'checkbox', + name: 'targetDistributions', + message: `Which distributions does this role target? ${chalk.reset.gray.italic( + 'Is your favourite distribution missing? Let us know here: ' + URLS.ISSUES, + )}`, + choices: listPlatforms(PLATFORMS), + store: true, + validate: answer => not(either(isEmpty, isNil)(answer)), + filter: map(toUpper), + }, + { + type: 'checkbox', + name: 'targetVersions', + message: `Which versions does this role support? ${chalk.reset.gray.italic( + 'Are the versions outdated? File an issue here: ' + URLS.ISSUES, + )}`, + choices: answers => listVersions(prop('targetDistributions', answers)), + store: true, + validate: answer => not(either(isEmpty, isNil)(answer)), + }, + { + type: 'confirm', + name: 'useTravis', + message: 'Use Travis CI?', + default: true, + store: true, + }, + { + when: answers => answers.useTravis, + type: 'input', + name: 'travisUsername', + message: 'What is your Travis CI username?', + store: true, + validate: answer => not(either(isEmpty, isNil)(answer)), + }, + { + type: 'list', + name: 'license', + message: `Which license for this role? ${chalk.reset.gray.italic( + 'For help choosing, see ' + URLS.LICENSE_INFO, + )}`, + choices: LICENSES, + default: 'MIT', + store: true, + }, + { + type: 'input', + name: 'galaxyTags', + message: `Which Galaxy tags to give the role? ${chalk.reset.gray.italic( + '(optional; single words, comma separated)', + )}`, + filter: answer => split(', ', answer), + default: '', + }, + { + type: 'confirm', + name: 'hasReqs', + message: 'Does this role have any particular requirements?', + default: false, + }, + { + when: answers => answers.hasReqs, + type: 'editor', + name: 'roleReqs', + message: `Enter this role's requirements. ${chalk.reset.gray.italic( + 'Usually details specific OS requirements, assumptions, etc. Markdown valid here.', + )}`, + validate: answer => not(either(isEmpty, isNil)(answer)), + }, + { + type: 'confirm', + name: 'hasDeps', + message: 'Does this role depend on any other?', + default: false, + }, + { + when: answers => answers.hasDeps, + type: 'editor', + name: 'roleDeps', + message: + 'Enter the roles on which your role will depend. See ' + + URLS.DEPENDENCIES_FORMAT + + " for how to format your entries, they'll be inserted verbatim into a requirements.yml file.", + validate: answer => not(either(isEmpty, isNil)(answer)), + }, +]; + +module.exports = prompts; diff --git a/generators/molecule/index.js b/generators/molecule/index.js new file mode 100644 index 0000000..7fdf666 --- /dev/null +++ b/generators/molecule/index.js @@ -0,0 +1,98 @@ +'use strict'; +const { forEach } = require('ramda'); +const { safeDump } = require('js-yaml'); +const Generator = require('yeoman-generator'); +const mkdirp = require('mkdirp'); +const path = require('path'); + +const { moleculePlatforms } = require('../helpers'); +const prompts = require('./prompts'); + +module.exports = class extends Generator { + constructor(args, opts) { + super(args, opts); + + this.option('mode', { + type: String, + required: false, + default: 'role', + desc: + 'Whether to generate molecule files for a role or for a playbook. Acceptable values are "role" or "playbook".', + }); + + this.option('convergePath', { + type: String, + required: false, + default: '../../playbook.yml', + desc: + 'Path to the playbook to test, relative to /molecule/default/. Defaults to "../../playbook.yml", i.e. "/playbook.yml".', + }); + } + + prompting() { + this.log( + 'This generator will create an Ansible role and test it with Molecule using LXD containers.', + ); + + return this.prompt(prompts).then(props => { + this.props = props; + }); + } + + writing() { + const p = this.props; + const destinationPath = p.repoName; + + // Create role directory if it doesn't already exist and set it as the root + if (path.basename(this.destinationPath()) !== destinationPath) { + this.log(`Creating your new role in ${destinationPath}...`); + mkdirp(destinationPath); + this.destinationRoot(this.destinationPath(destinationPath)); + } + // Create the rest of the directories + const dirs = ['molecule/default/tests']; + + forEach(mkdirp, dirs); + + // Copy files + this.fs.copy(this.templatePath('gitignore'), this.destinationPath('.gitignore')); + + this.fs.copy( + this.templatePath('create.yml'), + this.destinationPath('molecule/create.yml'), + ); + + this.fs.copy( + this.templatePath('destroy.yml'), + this.destinationPath('molecule/destroy.yml'), + ); + + this.fs.copyTpl( + this.templatePath('molecule.yml.ejs'), + this.destinationPath('molecule/default/molecule.yml'), + { + platforms: safeDump({ platforms: moleculePlatforms(p.targetVersions) }), + playbookMode: this.options.mode === 'playbook', + convergePath: this.options.convergePath, + }, + ); + + // When testing a playbook, the converge playbook is the playbook under + // but when testing a role, a default converge playbook running the role is + // needed + if (this.options.mode === 'role') { + this.fs.copyTpl( + this.templatePath('playbook.yml.ejs'), + this.destinationPath('molecule/default/playbook.yml'), + { roleName: p.repoName }, + ); + } + + this.fs.copy( + this.templatePath('test_default.py'), + this.destinationPath('molecule/default/tests/test_default.py'), + ); + + this.fs.copy(this.templatePath('.yamllint'), this.destinationPath('.yamllint')); + } +}; diff --git a/generators/molecule/prompts.js b/generators/molecule/prompts.js new file mode 100644 index 0000000..f5fef1f --- /dev/null +++ b/generators/molecule/prompts.js @@ -0,0 +1,38 @@ +const { paramCase } = require('change-case'); +const { either, isEmpty, isNil, map, not, prop, toUpper } = require('ramda'); +const chalk = require('chalk'); + +const { PLATFORMS, URLS } = require('../constants'); +const { listPlatforms, listVersions } = require('../helpers'); + +const prompts = [ + { + type: 'input', + name: 'repoName', + message: "What is the project's directory name?", + default: answers => `ansible-role-${paramCase(answers.roleName)}`, + }, + { + type: 'checkbox', + name: 'targetDistributions', + message: `Which distributions does this role target? ${chalk.reset.gray.italic( + 'Is your favourite distribution missing? Let us know here: ' + URLS.ISSUES, + )}`, + choices: listPlatforms(PLATFORMS), + store: true, + validate: answer => not(either(isEmpty, isNil)(answer)), + filter: map(toUpper), + }, + { + type: 'checkbox', + name: 'targetVersions', + message: `Which versions does this role support? ${chalk.reset.gray.italic( + 'Are the versions outdated? File an issue here: ' + URLS.ISSUES, + )}`, + choices: answers => listVersions(prop('targetDistributions', answers)), + store: true, + validate: answer => not(either(isEmpty, isNil)(answer)), + }, +]; + +module.exports = prompts; diff --git a/generators/molecule/templates/.yamllint b/generators/molecule/templates/.yamllint new file mode 100644 index 0000000..a4d53d6 --- /dev/null +++ b/generators/molecule/templates/.yamllint @@ -0,0 +1,10 @@ +extends: default + +rules: + braces: + max-spaces-inside: 1 + level: error + brackets: + max-spaces-inside: 1 + level: error + line-length: disable diff --git a/generators/app/templates/create.yml b/generators/molecule/templates/create.yml similarity index 100% rename from generators/app/templates/create.yml rename to generators/molecule/templates/create.yml diff --git a/generators/app/templates/destroy.yml b/generators/molecule/templates/destroy.yml similarity index 100% rename from generators/app/templates/destroy.yml rename to generators/molecule/templates/destroy.yml diff --git a/generators/molecule/templates/gitignore b/generators/molecule/templates/gitignore new file mode 100644 index 0000000..83658ec --- /dev/null +++ b/generators/molecule/templates/gitignore @@ -0,0 +1,2 @@ +*.pyc +*.log diff --git a/generators/app/templates/molecule.yml.ejs b/generators/molecule/templates/molecule.yml.ejs similarity index 81% rename from generators/app/templates/molecule.yml.ejs rename to generators/molecule/templates/molecule.yml.ejs index 142869a..82a78c4 100644 --- a/generators/app/templates/molecule.yml.ejs +++ b/generators/molecule/templates/molecule.yml.ejs @@ -10,6 +10,9 @@ provisioner: name: ansible playbooks: create: ../create.yml +<% if (playbookMode) { -%> + converge: ../../playbook.yml +<% } -%> destroy: ../destroy.yml lint: name: ansible-lint diff --git a/generators/app/templates/playbook.yml.ejs b/generators/molecule/templates/playbook.yml.ejs similarity index 60% rename from generators/app/templates/playbook.yml.ejs rename to generators/molecule/templates/playbook.yml.ejs index c872562..b4806ab 100644 --- a/generators/app/templates/playbook.yml.ejs +++ b/generators/molecule/templates/playbook.yml.ejs @@ -2,4 +2,4 @@ - name: Converge hosts: all roles: - - name: <%- repoName %> + - name: <%- roleName %> diff --git a/generators/app/templates/test_default.py b/generators/molecule/templates/test_default.py similarity index 100% rename from generators/app/templates/test_default.py rename to generators/molecule/templates/test_default.py diff --git a/package-lock.json b/package-lock.json index d30084c..47f8550 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "generator-molecule-lxd-role", - "version": "0.2.2", + "version": "0.3.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 684f61c..fa6b945 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "generator-molecule-lxd-role", - "version": "0.2.2", + "version": "0.3.0", "description": "Ansible role testing with molecule using LXD containers with optional Travis CI integration", "homepage": "", "author": {