diff --git a/roles/upload-forge/README.rst b/roles/upload-forge/README.rst new file mode 100644 index 000000000..a71d2ff46 --- /dev/null +++ b/roles/upload-forge/README.rst @@ -0,0 +1,24 @@ +Upload puppet module tarball to a Forge server + +This role requires the python requests module to be +installed where Ansible is executing this role. + +**Role Variables** + + .. zuul:rolevar:: forge_url + :default: https://forgeapi.puppet.com + + The URL to the Puppet Forge API. + + .. zuul:rolevar:: forge_username + + Username to use to log in to Puppet Forge. + + .. zuul:rolevar:: forge_password + + Password to use to log in to Puppet Forge. + + .. zuul:rolevar:: forge_tarball + + Absolute path to the module tarball that should be + uploaded. diff --git a/roles/upload-forge/__init__.py b/roles/upload-forge/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/roles/upload-forge/defaults/main.yaml b/roles/upload-forge/defaults/main.yaml new file mode 100644 index 000000000..b7324523b --- /dev/null +++ b/roles/upload-forge/defaults/main.yaml @@ -0,0 +1,2 @@ +--- +forge_url: "https://forgeapi.puppet.com" diff --git a/roles/upload-forge/library/__init__.py b/roles/upload-forge/library/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/roles/upload-forge/library/forge_upload.py b/roles/upload-forge/library/forge_upload.py new file mode 100644 index 000000000..d477c8ef6 --- /dev/null +++ b/roles/upload-forge/library/forge_upload.py @@ -0,0 +1,182 @@ +#!/usr/bin/env python + +# Copyright (c) 2019 Binero +# Author: Tobias Urdin +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +ANSIBLE_METADATA = { + 'metadata_version': '1.0', + 'status': ['preview'], + 'supported_by': 'community' +} + + +DOCUMENTATION = ''' +--- +module: forge_upload + +short_description: Uploads a puppet module tarball to a Forge server. + +description: + - "Uploads a puppet module tarball to a Forge server." + +options: + username: + description: + - The username to the Forge account + required: true + password: + description: + - The password to the Forge account + required: true + tarball: + description: + - The absolute path to the tarball of the puppet module + that should be uploaded + required: true + forgeapi: + description: + - This base url to the Forge server API, defaults to + https://forgeapi.puppet.com + required: false + +author: + - Tobias Urdin (@tobias-urdin) +''' + + +EXAMPLES = ''' +- name: Upload module + forge_upload: + username: 'myuser' + password: 'mypass' + tarball: '/home/myuser/test/pkg/myuser-test-0.1.0.tar.gz' +''' + + +RETURN = ''' +msg: + description: The output message from the module. +''' + + +from ansible.module_utils.basic import AnsibleModule # noqa +import os # noqa +import requests # noqa + + +# Client ID and secret from puppet-blacksmith +CLIENT_ID = 'b93eb708fd942cfc7b4ed71db6ce219b814954619dbe537ddfd208584e8cff8d' +CLIENT_SECRET = '216648059ad4afec3e4d77bd9e67817c095b2dcf94cdec18ac3d00584f863180' # noqa + +FORGEAPI = 'https://forgeapi.puppet.com' + + +def _get_url(forgeapi, path): + path = path[1:] if path.startswith('/') else path + return '%s/%s' % (forgeapi, path) + + +def _forge_auth(forgeapi, username, password): + url = _get_url(forgeapi, '/oauth/token') + data = { + 'client_id': CLIENT_ID, + 'client_secret': CLIENT_SECRET, + 'username': username, + 'password': password, + 'grant_type': 'password', + } + headers = { + 'User-Agent': 'forge_upload-ansible-module/1.0', + } + response = requests.post(url, json=data, headers=headers) + return response + + +def _forge_upload(forgeapi, token, tarball): + url = _get_url(forgeapi, '/v2/releases') + data = { + 'file': open(tarball, 'rb').read(), + } + headers = { + 'User-Agent': 'forge_upload-ansible-module/1.0', + 'Authorization': 'Bearer %s' % token, + } + response = requests.post(url, files=data, headers=headers) + return response + + +def run_module(): + module_args = dict( + username=dict(type='str', required=True), + password=dict(type='str', required=True, no_log=True), + tarball=dict(type='str', required=True), + forgeapi=dict(type='str', default=FORGEAPI), + ) + + result = dict( + changed=False, + ) + + module = AnsibleModule( + argument_spec=module_args, + supports_check_mode=True + ) + + tarball = module.params['tarball'] + if os.path.exists(tarball) is False: + module.fail_json(msg='Tarball %s does not exist' % tarball, **result) + + resp = _forge_auth(module.params['forgeapi'], + module.params['username'], + module.params['password']) + + if resp.status_code != 200: + msg = 'Forge API auth failed with code: %d' % resp.status_code + module.fail_json(msg=msg, **result) + + if module.check_mode: + return result + + auth = resp.json() + token = auth['access_token'] + + resp = _forge_upload(module.params['forgeapi'], token, tarball) + + if resp.status_code == 409: + msg = 'Module %s already exists on Forge' % tarball + module.exit_json(msg=msg, **result) + + if resp.status_code != 201: + try: + data = resp.json() + errors = ','.join(data['errors']) + except Exception: + errors = 'unknown' + msg = 'Forge API failed to upload tarball with code: %d errors: %s' % ( + resp.status_code, errors) + module.fail_json(msg=msg, **result) + + result['changed'] = True + module.exit_json(msg='Successfully uploaded tarball %s' % tarball, + **result) + + +def main(): + run_module() + + +if __name__ == '__main__': + main() diff --git a/roles/upload-forge/library/test_forge_upload.py b/roles/upload-forge/library/test_forge_upload.py new file mode 100644 index 000000000..d3b0d5de3 --- /dev/null +++ b/roles/upload-forge/library/test_forge_upload.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python + +# Copyright (c) 2019 Binero +# Author: Tobias Urdin +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +import testtools +from .forge_upload import _get_url + + +class TestForgeUpload(testtools.TestCase): + def test_get_url(self): + base_url = 'https://forgeapi.puppet.com' + expected = 'https://forgeapi.puppet.com/test' + self.assertEqual(_get_url(base_url, '/test'), expected) + self.assertEqual(_get_url(base_url, 'test'), expected) diff --git a/roles/upload-forge/tasks/main.yaml b/roles/upload-forge/tasks/main.yaml new file mode 100644 index 000000000..4bae16ae7 --- /dev/null +++ b/roles/upload-forge/tasks/main.yaml @@ -0,0 +1,13 @@ +- name: Check required variables + assert: + that: + - "forge_username is defined" + - "forge_password is defined" + - "forge_tarball is defined" + +- name: Upload module to Forge + forge_upload: + username: "{{ forge_username }}" + password: "{{ forge_password }}" + tarball: "{{ forge_tarball }}" + forgeapi: "{{ forge_url }}" diff --git a/roles/upload-puppetforge/README.rst b/roles/upload-puppetforge/README.rst deleted file mode 100644 index 5eae36a0e..000000000 --- a/roles/upload-puppetforge/README.rst +++ /dev/null @@ -1,22 +0,0 @@ -Upload puppet module to Puppet Forge - -**Role Variables** - - .. zuul:rolevar:: puppet_module_dir - :default: {{ zuul.project.src_dir }} - - The folder where the puppet module code is that it can - switch folder to. - - .. zuul:rolevar:: blacksmith_forge_url - :default: https://forgeapi.puppetlabs.com - - The URL to the Puppet Forge API. - - .. zuul:rolevar:: blacksmith_forge_username - - Username to use to log in to Puppet Forge. - - .. zuul:rolevar:: blacksmith_forge_password - - Password to use to log in to Puppet Forge. diff --git a/roles/upload-puppetforge/defaults/main.yaml b/roles/upload-puppetforge/defaults/main.yaml deleted file mode 100644 index c93ad6002..000000000 --- a/roles/upload-puppetforge/defaults/main.yaml +++ /dev/null @@ -1,3 +0,0 @@ ---- -puppet_module_dir: "{{ zuul.project.src_dir }}" -blacksmith_forge_url: "https://forgeapi.puppetlabs.com" diff --git a/roles/upload-puppetforge/tasks/main.yaml b/roles/upload-puppetforge/tasks/main.yaml deleted file mode 100644 index 3003ce20b..000000000 --- a/roles/upload-puppetforge/tasks/main.yaml +++ /dev/null @@ -1,52 +0,0 @@ -- name: Install ruby dependencies on RedHat/Suse based - package: - name: - - ruby-devel - - gcc-c++ - - make - state: present - become: yes - when: ansible_os_family == "RedHat" or ansible_os_family == "Suse" - -- name: Install ruby dependencies on Debian based - package: - name: - - ruby-dev - - g++ - - make - state: present - become: yes - when: ansible_os_family == "Debian" - -- name: Install required gems - gem: - name: "{{ item }}" - user_install: no - with_items: - - rake - - puppetlabs_spec_helper - - puppet-blacksmith - become: yes - -# NOTE(tobias.urdin): The build task is needed because puppet-blacksmith -# doesn't provide a build task so it fails, we don't need one anyway since -# we have already built the module before this role is called. -- name: Install new Rakefile - copy: - content: | - namespace 'module' do - task 'build' do - end - end - - require 'puppet_blacksmith/rake_tasks' - dest: "{{ puppet_module_dir }}/Rakefile" - -- name: Publish puppet module - command: "rake module:push" - args: - chdir: "{{ puppet_module_dir }}" - environment: - BLACKSMITH_FORGE_URL: "{{ blacksmith_forge_url }}" - BLACKSMITH_FORGE_USERNAME: "{{ blacksmith_forge_username }}" - BLACKSMITH_FORGE_PASSWORD: "{{ blacksmith_forge_password }}"