From 6af53cf0ef28d18ff17e241627b8566e799eaf73 Mon Sep 17 00:00:00 2001 From: Chris Houseknecht Date: Thu, 6 Jul 2017 09:22:04 -0400 Subject: [PATCH] Adds docker_secret module (#26469) --- .../modules/cloud/docker/docker_secret.py | 287 ++++++++++++++++++ .../integration/targets/docker_secret/aliases | 3 + .../targets/docker_secret/handlers/main.yml | 3 + .../targets/docker_secret/tasks/Fedora.yml | 17 ++ .../targets/docker_secret/tasks/OpenSuse.yml | 11 + .../targets/docker_secret/tasks/RedHat.yml | 21 ++ .../targets/docker_secret/tasks/Ubuntu.yml | 36 +++ .../targets/docker_secret/tasks/main.yml | 14 + .../docker_secret/tasks/test_secrets.yml | 104 +++++++ .../docker_secret/templates/virt.repo.j2 | 7 + 10 files changed, 503 insertions(+) create mode 100644 lib/ansible/modules/cloud/docker/docker_secret.py create mode 100644 test/integration/targets/docker_secret/aliases create mode 100644 test/integration/targets/docker_secret/handlers/main.yml create mode 100644 test/integration/targets/docker_secret/tasks/Fedora.yml create mode 100644 test/integration/targets/docker_secret/tasks/OpenSuse.yml create mode 100644 test/integration/targets/docker_secret/tasks/RedHat.yml create mode 100644 test/integration/targets/docker_secret/tasks/Ubuntu.yml create mode 100644 test/integration/targets/docker_secret/tasks/main.yml create mode 100644 test/integration/targets/docker_secret/tasks/test_secrets.yml create mode 100644 test/integration/targets/docker_secret/templates/virt.repo.j2 diff --git a/lib/ansible/modules/cloud/docker/docker_secret.py b/lib/ansible/modules/cloud/docker/docker_secret.py new file mode 100644 index 0000000000..c45aa651c0 --- /dev/null +++ b/lib/ansible/modules/cloud/docker/docker_secret.py @@ -0,0 +1,287 @@ +#!/usr/bin/python +# +# Copyright 2016 Red Hat | Ansible +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +ANSIBLE_METADATA = {'metadata_version': '1.0', + 'status': ['preview'], + 'supported_by': 'community'} + + +DOCUMENTATION = ''' +--- +module: docker_secret + +short_description: Manage docker secrets. + +version_added: "2.4" + +description: + - Create and remove Docker secrets in a Swarm environment. Similar to `docker secret create` and `docker secret rm`. + - Adds to the metadata of new secrets 'ansible_key', an encrypted hash representation of the data, which is then used + - in future runs to test if a secret has changed. + - If 'ansible_key is not present, then a secret will not be updated unless the C(force) option is set. + - Updates to secrets are performed by removing the secret and creating it again. +options: + data: + description: + - String. The value of the secret. Required when state is C(present). + required: false + labels: + description: + - "A map of key:value meta data, where both the I(key) and I(value) are expected to be a string." + - If new meta data is provided, or existing meta data is modified, the secret will be updated by removing it and creating it again. + required: false + force: + description: + - Boolean. Use with state C(present) to always remove and recreate an existing secret. + - If I(true), an existing secret will be replaced, even if it has not changed. + default: false + name: + description: + - The name of the secret. + required: true + state: + description: + - Set to C(present), if the secret should exist, and C(absent), if it should not. + required: false + default: present + choices: + - absent + - present + +extends_documentation_fragment: + - docker + +requirements: + - "docker-py >= 2.1.0" + - "Docker API >= 1.25" + +author: + - Chris Houseknecht (@chouseknecht) +''' + +EXAMPLES = ''' + +- name: Create secret foo + docker_secret: + name: foo + data: Hello World! + state: present + +- name: Change the secret data + docker_secret: + name: foo + data: Goodnight everyone! + labels: + bar: baz + one: '1' + state: present + +- name: Add a new label + docker_secret: + name: foo + data: Goodnight everyone! + labels: + bar: baz + one: '1' + # Adding a new label will cause a remove/create of the secret + two: '2' + state: present + +- name: No change + docker_secret: + name: foo + data: Goodnight everyone! + labels: + bar: baz + one: '1' + # Even though 'two' is missing, there is no change to the existing secret + state: present + +- name: Update an existing label + docker_secret: + name: foo + data: Goodnight everyone! + labels: + bar: monkey # Changing a label will cause a remove/create of the secret + one: '1' + state: present + +- name: Force the removal/creation of the secret + docker_secret: + name: foo + data: Goodnight everyone! + force: yes + state: present + +- name: Remove secret foo + docker_secret: + name: foo + state: absent +''' + +RETURN = ''' +secret_id: + description: + - The ID assigned by Docker to the secret object. + returned: success + type: string + sample: 'hzehrmyjigmcp2gb6nlhmjqcv' +''' + +import hashlib +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.docker_common import AnsibleDockerClient, DockerBaseClass, APIError +from ansible.module_utils._text import to_native, to_bytes + + +class SecretManager(DockerBaseClass): + + def __init__(self, client, results): + + super(SecretManager, self).__init__() + + self.client = client + self.results = results + self.check_mode = self.client.check_mode + + parameters = self.client.module.params + self.name = parameters.get('name') + self.state = parameters.get('state') + self.data = parameters.get('data') + self.labels = parameters.get('labels') + self.force = parameters.get('force') + self.data_key = None + + def __call__(self): + if self.state == 'present': + self.data_key = hashlib.sha224(to_bytes(self.data)).hexdigest() + self.present() + elif self.state == 'absent': + self.absent() + + def get_secret(self): + ''' Find an existing secret. ''' + try: + secrets = self.client.secrets(filters={'name': self.name}) + except APIError as exc: + self.client.fail("Error accessing secret %s: %s" % (self.name, to_native(exc))) + + for secret in secrets: + if secret['Spec']['Name'] == self.name: + return secret + return None + + def create_secret(self): + ''' Create a new secret ''' + secret_id = None + # We can't see the data after creation, so adding a label we can use for idempotency check + labels = { + 'ansible_key': self.data_key + } + if self.labels: + labels.update(self.labels) + + try: + if not self.check_mode: + secret_id = self.client.create_secret(self.name, self.data, labels=labels) + except APIError as exc: + self.client.fail("Error creating secret: %s" % to_native(exc)) + + if isinstance(secret_id, dict): + secret_id = secret_id['ID'] + + return secret_id + + def present(self): + ''' Handles state == 'present', creating or updating the secret ''' + secret = self.get_secret() + if secret: + self.results['secret_id'] = secret['ID'] + data_changed = False + attrs = secret.get('Spec', {}) + if attrs.get('Labels', {}).get('ansible_key'): + if attrs['Labels']['ansible_key'] != self.data_key: + data_changed = True + labels_changed = False + if self.labels and attrs.get('Labels'): + # check if user requested a label change + for label in attrs['Labels']: + if self.labels.get(label) and self.labels[label] != attrs['Labels'][label]: + labels_changed = True + # check if user added a label + labels_added = False + if self.labels: + if attrs.get('Labels'): + for label in self.labels: + if label not in attrs['Labels']: + labels_added = True + else: + labels_added = True + if data_changed or labels_added or labels_changed or self.force: + # if something changed or force, delete and re-create the secret + self.absent() + secret_id = self.create_secret() + self.results['changed'] = True + self.results['secret_id'] = secret_id + else: + self.results['changed'] = True + self.results['secret_id'] = self.create_secret() + + def absent(self): + ''' Handles state == 'absent', removing the secret ''' + secret = self.get_secret() + if secret: + try: + if not self.check_mode: + self.client.remove_secret(secret['ID']) + except APIError as exc: + self.client.fail("Error removing secret %s: %s" % (self.name, to_native(exc))) + self.results['changed'] = True + + +def main(): + argument_spec = dict( + name=dict(type='str', required=True), + state=dict(type='str', choices=['absent', 'present'], default='present'), + data=dict(type='str'), + labels=dict(type='dict'), + force=dict(type='bool', default=False) + ) + + required_if = [ + ('state', 'present', ['data']) + ] + + client = AnsibleDockerClient( + argument_spec=argument_spec, + supports_check_mode=True, + required_if=required_if + ) + + results = dict( + changed=False, + secret_id='' + ) + + SecretManager(client, results)() + client.module.exit_json(**results) + + +if __name__ == '__main__': + main() diff --git a/test/integration/targets/docker_secret/aliases b/test/integration/targets/docker_secret/aliases new file mode 100644 index 0000000000..c6de1f9e6b --- /dev/null +++ b/test/integration/targets/docker_secret/aliases @@ -0,0 +1,3 @@ +posix/ci/group2 +skip/osx +skip/freebsd diff --git a/test/integration/targets/docker_secret/handlers/main.yml b/test/integration/targets/docker_secret/handlers/main.yml new file mode 100644 index 0000000000..a0db5e451d --- /dev/null +++ b/test/integration/targets/docker_secret/handlers/main.yml @@ -0,0 +1,3 @@ +- name: disable_swarm + command: docker swarm leave --force + ignore_errors: yes diff --git a/test/integration/targets/docker_secret/tasks/Fedora.yml b/test/integration/targets/docker_secret/tasks/Fedora.yml new file mode 100644 index 0000000000..c1b1c840e1 --- /dev/null +++ b/test/integration/targets/docker_secret/tasks/Fedora.yml @@ -0,0 +1,17 @@ +- name: Install Docker pre-reqs + dnf: + name: "{{ item }}" + state: present + items: + - dnf-plugins-core + +- name: Add repository + command: dnf config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo + +- name: Update cache + command: dnf makecache fast + +- name: Install docker + dnf: + name: docker-ce + state: present diff --git a/test/integration/targets/docker_secret/tasks/OpenSuse.yml b/test/integration/targets/docker_secret/tasks/OpenSuse.yml new file mode 100644 index 0000000000..9288fa0792 --- /dev/null +++ b/test/integration/targets/docker_secret/tasks/OpenSuse.yml @@ -0,0 +1,11 @@ +- name: Template repo + template: + src: virt.repo.j2 + dest: /etc/zypp/repos.d/virt.repo + +- name: Install docker 17 + zypper: + name: docker-17.04.0_ce-203.6.x86_64 + force: yes + disable_gpg_check: yes + update_cache: yes diff --git a/test/integration/targets/docker_secret/tasks/RedHat.yml b/test/integration/targets/docker_secret/tasks/RedHat.yml new file mode 100644 index 0000000000..51f466d564 --- /dev/null +++ b/test/integration/targets/docker_secret/tasks/RedHat.yml @@ -0,0 +1,21 @@ +- name: Install Docker pre-reqs + yum: + name: "{{ item }}" + state: present + items: + - yum-utils + - device-mapper-persistent-data + - lvm2 + - python-crypto + - libseccomp + +- name: Add repository + command: yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo + +- name: Update cache + command: yum -y makecache fast + +- name: Install docker + yum: + name: docker-ce + state: present diff --git a/test/integration/targets/docker_secret/tasks/Ubuntu.yml b/test/integration/targets/docker_secret/tasks/Ubuntu.yml new file mode 100644 index 0000000000..8646409de1 --- /dev/null +++ b/test/integration/targets/docker_secret/tasks/Ubuntu.yml @@ -0,0 +1,36 @@ +- name: Get OS version + shell: uname -r + register: os_version + +- name: Install packages for Trusty + apt: + name: "{{ item }}" + state: present + update_cache: yes + with_items: + - "linux-image-extra-{{ os_version.stdout }}" + - linux-image-extra-virtual + when: ansible_distribution_release == 'trusty' + +- name: Install pre-reqs + apt: + name: "{{ item }}" + state: present + update_cache: yes + with_items: + - apt-transport-https + - ca-certificates + - curl + - software-properties-common + +- name: Add gpg key + shell: curl -fsSL https://download.docker.com/linux/ubuntu/gpg >key && apt-key add key + +- name: Add Docker repo + shell: add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" + +- name: Install Docker CE + apt: + name: docker-ce + state: present + update_cache: yes diff --git a/test/integration/targets/docker_secret/tasks/main.yml b/test/integration/targets/docker_secret/tasks/main.yml new file mode 100644 index 0000000000..a71923aaa4 --- /dev/null +++ b/test/integration/targets/docker_secret/tasks/main.yml @@ -0,0 +1,14 @@ +- include: RedHat.yml + when: ansible_os_family == 'RedHat' and ansible_distribution != 'Fedora' and ansible_distribution_major_version != '6' + +- include: Fedora.yml + when: ansible_distribution == 'Fedora' + +- include: OpenSuse.yml + when: ansible_os_family == 'Suse' + +- include: Ubuntu.yml + when: ansible_os_family == 'Debian' + +- include: test_secrets.yml + when: ansible_os_family != 'RedHat' or ansible_distribution_major_version != '6' diff --git a/test/integration/targets/docker_secret/tasks/test_secrets.yml b/test/integration/targets/docker_secret/tasks/test_secrets.yml new file mode 100644 index 0000000000..c49dafca10 --- /dev/null +++ b/test/integration/targets/docker_secret/tasks/test_secrets.yml @@ -0,0 +1,104 @@ +- name: Install Python requirements + pip: + state: present + name: "{{ item }}" + with_items: + - docker>=2.1.0 + +- name: Check if already in swarm + shell: docker node ls 2>&1 | grep 'docker swarm init' + register: output + ignore_errors: yes + +- name: Enable swarm mode + command: docker swarm init + when: output.rc == 0 + notify: disable_swarm + +- name: Parameter name should be required + docker_secret: + state: present + ignore_errors: yes + register: output + +- name: assert failure when called with no name + assert: + that: + - 'output.failed' + - 'output.msg == "missing required arguments: name"' + +- name: Test parameters + docker_secret: + name: foo + state: present + ignore_errors: yes + register: output + +- name: assert failure when called with no data + assert: + that: + - 'output.failed' + - 'output.msg == "state is present but the following are missing: data"' + +- name: Create secret + docker_secret: + name: db_password + data: opensesame! + state: present + register: output + +- name: Create variable secret_id + set_fact: + secret_id: "{{ output.secret_id }}" + +- name: Inspect secret + command: "docker secret inspect {{ secret_id }}" + register: inspect + +- debug: var=inspect + +- name: assert secret creation succeeded + assert: + that: + - "'db_password' in inspect.stdout" + - "'ansible_key' in inspect.stdout" + +- name: Create secret again + docker_secret: + name: db_password + data: opensesame! + state: present + register: output + +- name: assert create secret is idempotent + assert: + that: + - not output.changed + +- name: Update secret + docker_secret: + name: db_password + data: newpassword! + state: present + register: output + +- name: assert secret was updated + assert: + that: + - output.changed + - output.secret_id != secret_id + +- name: Remove secret + docker_secret: + name: db_password + state: absent + +- name: Check that secret is removed + command: "docker secret inspect {{ secret_id }}" + register: output + ignore_errors: yes + +- name: assert secret was removed + assert: + that: + - output.failed diff --git a/test/integration/targets/docker_secret/templates/virt.repo.j2 b/test/integration/targets/docker_secret/templates/virt.repo.j2 new file mode 100644 index 0000000000..2afd3b0ffd --- /dev/null +++ b/test/integration/targets/docker_secret/templates/virt.repo.j2 @@ -0,0 +1,7 @@ +[Virtualization_containers] +name=Virtualization:containers (openSUSE_Tumbleweed) +type=rpm-md +baseurl=http://download.opensuse.org/repositories/Virtualization:/containers/openSUSE_Tumbleweed/ +gpgcheck=1 +gpgkey=http://download.opensuse.org/repositories/Virtualization:/containers/openSUSE_Tumbleweed//repodata/repomd.xml.key +enabled=1