summaryrefslogtreecommitdiff
path: root/lib/ansible/modules/cloud/docker/docker_login.py
blob: 9df3969a2b808b6aca3f6ea329bdf968a758a045 (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
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
#!/usr/bin/python
#
# (c) 2016 Olaf Kilian <olaf.kilian@symanex.com>
#          Chris Houseknecht, <house@redhat.com>
#          James Tanner, <jtanner@redhat.com>
#
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

from __future__ import absolute_import, division, print_function
__metaclass__ = type


ANSIBLE_METADATA = {'metadata_version': '1.1',
                    'status': ['preview'],
                    'supported_by': 'community'}


DOCUMENTATION = '''
---
module: docker_login
short_description: Log into a Docker registry.
version_added: "2.0"
description:
    - Provides functionality similar to the "docker login" command.
    - Authenticate with a docker registry and add the credentials to your local Docker config file. Adding the
      credentials to the config files allows future connections to the registry using tools such as Ansible's Docker
      modules, the Docker CLI and docker-py without needing to provide credentials.
    - Running in check mode will perform the authentication without updating the config file.
options:
  registry_url:
    required: False
    description:
      - The registry URL.
    default: "https://index.docker.io/v1/"
    aliases:
      - registry
      - url
  username:
    description:
      - The username for the registry account
    required: True
  password:
    description:
      - The plaintext password for the registry account
    required: True
  email:
    required: False
    description:
      - "The email address for the registry account."
  reauthorize:
    description:
      - Refresh existing authentication found in the configuration file.
    type: bool
    default: 'no'
    aliases:
      - reauth
  config_path:
    description:
      - Custom path to the Docker CLI configuration file.
    default: ~/.docker/config.json
    aliases:
      - self.config_path
      - dockercfg_path
  state:
    version_added: '2.3'
    description:
      - This controls the current state of the user. C(present) will login in a user, C(absent) will log them out.
      - To logout you only need the registry server, which defaults to DockerHub.
      - Before 2.1 you could ONLY log in.
      - docker does not support 'logout' with a custom config file.
    choices: ['present', 'absent']
    default: 'present'

extends_documentation_fragment:
    - docker
requirements:
    - "python >= 2.6"
    - "docker-py >= 1.8.0"
    - "Please note that the L(docker-py,https://pypi.org/project/docker-py/) Python
       module has been superseded by L(docker,https://pypi.org/project/docker/)
       (see L(here,https://github.com/docker/docker-py/issues/1310) for details).
       For Python 2.6, C(docker-py) must be used. Otherwise, it is recommended to
       install the C(docker) Python module. Note that both modules should I(not)
       be installed at the same time. Also note that when both modules are installed
       and one of them is uninstalled, the other might no longer function and a
       reinstall of it is required."
    - "Docker API >= 1.20"
    - 'Only to be able to logout (state=absent): the docker command line utility'
author:
    - Olaf Kilian (@olsaki) <olaf.kilian@symanex.com>
    - Chris Houseknecht (@chouseknecht)
'''

EXAMPLES = '''

- name: Log into DockerHub
  docker_login:
    username: docker
    password: rekcod

- name: Log into private registry and force re-authorization
  docker_login:
    registry: your.private.registry.io
    username: yourself
    password: secrets3
    reauthorize: yes

- name: Log into DockerHub using a custom config file
  docker_login:
    username: docker
    password: rekcod
    config_path: /tmp/.mydockercfg

- name: Log out of DockerHub
  docker_login:
    state: absent
'''

RETURN = '''
login_results:
    description: Results from the login.
    returned: when state='present'
    type: dict
    sample: {
        "email": "testuer@yahoo.com",
        "serveraddress": "localhost:5000",
        "username": "testuser"
    }
'''

import base64
import json
import os
import re

from ansible.module_utils._text import to_bytes, to_text
from ansible.module_utils.docker_common import AnsibleDockerClient, DEFAULT_DOCKER_REGISTRY, DockerBaseClass, EMAIL_REGEX


class LoginManager(DockerBaseClass):

    def __init__(self, client, results):

        super(LoginManager, self).__init__()

        self.client = client
        self.results = results
        parameters = self.client.module.params
        self.check_mode = self.client.check_mode

        self.registry_url = parameters.get('registry_url')
        self.username = parameters.get('username')
        self.password = parameters.get('password')
        self.email = parameters.get('email')
        self.reauthorize = parameters.get('reauthorize')
        self.config_path = parameters.get('config_path')

        if parameters['state'] == 'present':
            self.login()
        else:
            self.logout()

    def fail(self, msg):
        self.client.fail(msg)

    def login(self):
        '''
        Log into the registry with provided username/password. On success update the config
        file with the new authorization.

        :return: None
        '''

        if self.email and not re.match(EMAIL_REGEX, self.email):
            self.fail("Parameter error: the email address appears to be incorrect. Expecting it to match "
                      "/%s/" % (EMAIL_REGEX))

        self.results['actions'].append("Logged into %s" % (self.registry_url))
        self.log("Log into %s with username %s" % (self.registry_url, self.username))
        try:
            response = self.client.login(
                self.username,
                password=self.password,
                email=self.email,
                registry=self.registry_url,
                reauth=self.reauthorize,
                dockercfg_path=self.config_path
            )
        except Exception as exc:
            self.fail("Logging into %s for user %s failed - %s" % (self.registry_url, self.username, str(exc)))

        # If user is already logged in, then response contains password for user
        # This returns correct password if user is logged in and wrong password is given.
        if 'password' in response:
            del response['password']
        self.results['login_result'] = response

        if not self.check_mode:
            self.update_config_file()

    def logout(self):
        '''
        Log out of the registry. On success update the config file.
        TODO: port to API once docker.py supports this.

        :return: None
        '''

        cmd = "%s logout " % self.client.module.get_bin_path('docker', True)
        # TODO: docker does not support config file in logout, restore this when they do
        # if self.config_path and self.config_file_exists(self.config_path):
        #     cmd += "--config '%s' " % self.config_path
        cmd += "'%s'" % self.registry_url

        (rc, out, err) = self.client.module.run_command(cmd)
        if rc != 0:
            self.fail("Could not log out: %s" % err)

    def config_file_exists(self, path):
        if os.path.exists(path):
            self.log("Configuration file %s exists" % (path))
            return True
        self.log("Configuration file %s not found." % (path))
        return False

    def create_config_file(self, path):
        '''
        Create a config file with a JSON blob containing an auths key.

        :return: None
        '''

        self.log("Creating docker config file %s" % (path))
        config_path_dir = os.path.dirname(path)
        if not os.path.exists(config_path_dir):
            try:
                os.makedirs(config_path_dir)
            except Exception as exc:
                self.fail("Error: failed to create %s - %s" % (config_path_dir, str(exc)))
        self.write_config(path, dict(auths=dict()))

    def write_config(self, path, config):
        try:
            json.dump(config, open(path, "w"), indent=5, sort_keys=True)
        except Exception as exc:
            self.fail("Error: failed to write config to %s - %s" % (path, str(exc)))

    def update_config_file(self):
        '''
        If the authorization not stored in the config file or reauthorize is True,
        update the config file with the new authorization.

        :return: None
        '''

        path = self.config_path
        if not self.config_file_exists(path):
            self.create_config_file(path)

        try:
            # read the existing config
            config = json.load(open(path, "r"))
        except ValueError:
            self.log("Error reading config from %s" % (path))
            config = dict()

        if not config.get('auths'):
            self.log("Adding auths dict to config.")
            config['auths'] = dict()

        if not config['auths'].get(self.registry_url):
            self.log("Adding registry_url %s to auths." % (self.registry_url))
            config['auths'][self.registry_url] = dict()

        b64auth = base64.b64encode(
            to_bytes(self.username) + b':' + to_bytes(self.password)
        )
        auth = to_text(b64auth)

        encoded_credentials = dict(
            auth=auth,
            email=self.email
        )

        if config['auths'][self.registry_url] != encoded_credentials or self.reauthorize:
            # Update the config file with the new authorization
            config['auths'][self.registry_url] = encoded_credentials
            self.log("Updating config file %s with new authorization for %s" % (path, self.registry_url))
            self.results['actions'].append("Updated config file %s with new authorization for %s" % (
                path, self.registry_url))
            self.results['changed'] = True
            self.write_config(path, config)


def main():

    argument_spec = dict(
        registry_url=dict(type='str', required=False, default=DEFAULT_DOCKER_REGISTRY, aliases=['registry', 'url']),
        username=dict(type='str', required=False),
        password=dict(type='str', required=False, no_log=True),
        email=dict(type='str'),
        reauthorize=dict(type='bool', default=False, aliases=['reauth']),
        state=dict(type='str', default='present', choices=['present', 'absent']),
        config_path=dict(type='path', default='~/.docker/config.json', aliases=['self.config_path', 'dockercfg_path']),
    )

    required_if = [
        ('state', 'present', ['username', 'password']),
    ]

    client = AnsibleDockerClient(
        argument_spec=argument_spec,
        supports_check_mode=True,
        required_if=required_if,
        min_docker_api_version='1.20',
    )

    results = dict(
        changed=False,
        actions=[],
        login_result={}
    )

    LoginManager(client, results)
    if 'actions' in results:
        del results['actions']
    client.module.exit_json(**results)


if __name__ == '__main__':
    main()