Skip to content

Commit 3b6068e

Browse files
authored
Add docker_context_info module (#1039)
* Vendor parts of the Docker SDK for Python This is a combination of the latest git version (https://github.com/docker/docker-py/tree/db7f8b8bb67e485a7192846906f600a52e0aa623) with some fixes to make it compatible with Python 2.7 and adjusting some imports. * Polishing. * Fix bug that prevents contexts to be found when no Docker config file is present. Ref: docker/docker-py#3190 * Linting. * Fix typos. * Adjust more to behavior of Docker CLI. * Add first iteration of docker_context_info module. * Improvements. * Add basic CI. * Add caveat on contexts[].config result.
1 parent ea3ac5f commit 3b6068e

File tree

16 files changed

+1180
-11
lines changed

16 files changed

+1180
-11
lines changed
Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
# -*- coding: utf-8 -*-
2+
# This code is part of the Ansible collection community.docker, but is an independent component.
3+
# This particular file, and this file only, is based on the Docker SDK for Python (https://github.com/docker/docker-py/)
4+
#
5+
# Copyright (c) 2016-2025 Docker, Inc.
6+
#
7+
# It is licensed under the Apache 2.0 license (see LICENSES/Apache-2.0.txt in this collection)
8+
# SPDX-License-Identifier: Apache-2.0
9+
10+
from __future__ import (absolute_import, division, print_function)
11+
__metaclass__ = type
12+
13+
import json
14+
import os
15+
16+
from ansible.module_utils.six import raise_from
17+
18+
from .. import errors
19+
20+
from .config import (
21+
METAFILE,
22+
get_current_context_name,
23+
get_meta_dir,
24+
write_context_name_to_docker_config,
25+
)
26+
from .context import Context
27+
28+
29+
def create_default_context():
30+
host = None
31+
if os.environ.get('DOCKER_HOST'):
32+
host = os.environ.get('DOCKER_HOST')
33+
return Context("default", "swarm", host, description="Current DOCKER_HOST based configuration")
34+
35+
36+
class ContextAPI(object):
37+
"""Context API.
38+
Contains methods for context management:
39+
create, list, remove, get, inspect.
40+
"""
41+
DEFAULT_CONTEXT = None
42+
43+
@classmethod
44+
def get_default_context(cls):
45+
context = cls.DEFAULT_CONTEXT
46+
if context is None:
47+
context = create_default_context()
48+
cls.DEFAULT_CONTEXT = context
49+
return context
50+
51+
@classmethod
52+
def create_context(
53+
cls, name, orchestrator=None, host=None, tls_cfg=None,
54+
default_namespace=None, skip_tls_verify=False):
55+
"""Creates a new context.
56+
Returns:
57+
(Context): a Context object.
58+
Raises:
59+
:py:class:`docker.errors.MissingContextParameter`
60+
If a context name is not provided.
61+
:py:class:`docker.errors.ContextAlreadyExists`
62+
If a context with the name already exists.
63+
:py:class:`docker.errors.ContextException`
64+
If name is default.
65+
66+
Example:
67+
68+
>>> from docker.context import ContextAPI
69+
>>> ctx = ContextAPI.create_context(name='test')
70+
>>> print(ctx.Metadata)
71+
{
72+
"Name": "test",
73+
"Metadata": {},
74+
"Endpoints": {
75+
"docker": {
76+
"Host": "unix:///var/run/docker.sock",
77+
"SkipTLSVerify": false
78+
}
79+
}
80+
}
81+
"""
82+
if not name:
83+
raise errors.MissingContextParameter("name")
84+
if name == "default":
85+
raise errors.ContextException(
86+
'"default" is a reserved context name')
87+
ctx = Context.load_context(name)
88+
if ctx:
89+
raise errors.ContextAlreadyExists(name)
90+
endpoint = "docker"
91+
if orchestrator and orchestrator != "swarm":
92+
endpoint = orchestrator
93+
ctx = Context(name, orchestrator)
94+
ctx.set_endpoint(
95+
endpoint, host, tls_cfg,
96+
skip_tls_verify=skip_tls_verify,
97+
def_namespace=default_namespace)
98+
ctx.save()
99+
return ctx
100+
101+
@classmethod
102+
def get_context(cls, name=None):
103+
"""Retrieves a context object.
104+
Args:
105+
name (str): The name of the context
106+
107+
Example:
108+
109+
>>> from docker.context import ContextAPI
110+
>>> ctx = ContextAPI.get_context(name='test')
111+
>>> print(ctx.Metadata)
112+
{
113+
"Name": "test",
114+
"Metadata": {},
115+
"Endpoints": {
116+
"docker": {
117+
"Host": "unix:///var/run/docker.sock",
118+
"SkipTLSVerify": false
119+
}
120+
}
121+
}
122+
"""
123+
if not name:
124+
name = get_current_context_name()
125+
if name == "default":
126+
return cls.get_default_context()
127+
return Context.load_context(name)
128+
129+
@classmethod
130+
def contexts(cls):
131+
"""Context list.
132+
Returns:
133+
(Context): List of context objects.
134+
Raises:
135+
:py:class:`docker.errors.APIError`
136+
If something goes wrong.
137+
"""
138+
names = []
139+
for dirname, dummy, fnames in os.walk(get_meta_dir()):
140+
for filename in fnames:
141+
if filename == METAFILE:
142+
filepath = os.path.join(dirname, filename)
143+
try:
144+
with open(filepath, "r") as f:
145+
data = json.load(f)
146+
name = data["Name"]
147+
if name == "default":
148+
raise ValueError('"default" is a reserved context name')
149+
names.append(name)
150+
except Exception as e:
151+
raise_from(errors.ContextException(
152+
"Failed to load metafile {filepath}: {e}".format(filepath=filepath, e=e),
153+
), e)
154+
155+
contexts = [cls.get_default_context()]
156+
for name in names:
157+
context = Context.load_context(name)
158+
if not context:
159+
raise errors.ContextException("Context {context} cannot be found".format(context=name))
160+
contexts.append(context)
161+
return contexts
162+
163+
@classmethod
164+
def get_current_context(cls):
165+
"""Get current context.
166+
Returns:
167+
(Context): current context object.
168+
"""
169+
return cls.get_context()
170+
171+
@classmethod
172+
def set_current_context(cls, name="default"):
173+
ctx = cls.get_context(name)
174+
if not ctx:
175+
raise errors.ContextNotFound(name)
176+
177+
err = write_context_name_to_docker_config(name)
178+
if err:
179+
raise errors.ContextException(
180+
'Failed to set current context: {err}'.format(err=err))
181+
182+
@classmethod
183+
def remove_context(cls, name):
184+
"""Remove a context. Similar to the ``docker context rm`` command.
185+
186+
Args:
187+
name (str): The name of the context
188+
189+
Raises:
190+
:py:class:`docker.errors.MissingContextParameter`
191+
If a context name is not provided.
192+
:py:class:`docker.errors.ContextNotFound`
193+
If a context with the name does not exist.
194+
:py:class:`docker.errors.ContextException`
195+
If name is default.
196+
197+
Example:
198+
199+
>>> from docker.context import ContextAPI
200+
>>> ContextAPI.remove_context(name='test')
201+
>>>
202+
"""
203+
if not name:
204+
raise errors.MissingContextParameter("name")
205+
if name == "default":
206+
raise errors.ContextException(
207+
'context "default" cannot be removed')
208+
ctx = Context.load_context(name)
209+
if not ctx:
210+
raise errors.ContextNotFound(name)
211+
if name == get_current_context_name():
212+
write_context_name_to_docker_config(None)
213+
ctx.remove()
214+
215+
@classmethod
216+
def inspect_context(cls, name="default"):
217+
"""Inspect a context. Similar to the ``docker context inspect`` command.
218+
219+
Args:
220+
name (str): The name of the context
221+
222+
Raises:
223+
:py:class:`docker.errors.MissingContextParameter`
224+
If a context name is not provided.
225+
:py:class:`docker.errors.ContextNotFound`
226+
If a context with the name does not exist.
227+
228+
Example:
229+
230+
>>> from docker.context import ContextAPI
231+
>>> ContextAPI.remove_context(name='test')
232+
>>>
233+
"""
234+
if not name:
235+
raise errors.MissingContextParameter("name")
236+
if name == "default":
237+
return cls.get_default_context()()
238+
ctx = Context.load_context(name)
239+
if not ctx:
240+
raise errors.ContextNotFound(name)
241+
242+
return ctx()
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# -*- coding: utf-8 -*-
2+
# This code is part of the Ansible collection community.docker, but is an independent component.
3+
# This particular file, and this file only, is based on the Docker SDK for Python (https://github.com/docker/docker-py/)
4+
#
5+
# Copyright (c) 2016-2025 Docker, Inc.
6+
#
7+
# It is licensed under the Apache 2.0 license (see LICENSES/Apache-2.0.txt in this collection)
8+
# SPDX-License-Identifier: Apache-2.0
9+
10+
from __future__ import (absolute_import, division, print_function)
11+
__metaclass__ = type
12+
13+
import hashlib
14+
import json
15+
import os
16+
17+
from ..constants import DEFAULT_UNIX_SOCKET, IS_WINDOWS_PLATFORM
18+
from ..utils.config import find_config_file, get_default_config_file
19+
from ..utils.utils import parse_host
20+
21+
METAFILE = "meta.json"
22+
23+
24+
def get_current_context_name_with_source():
25+
if os.environ.get('DOCKER_HOST'):
26+
return "default", "DOCKER_HOST environment variable set"
27+
if os.environ.get('DOCKER_CONTEXT'):
28+
return os.environ['DOCKER_CONTEXT'], "DOCKER_CONTEXT environment variable set"
29+
docker_cfg_path = find_config_file()
30+
if docker_cfg_path:
31+
try:
32+
with open(docker_cfg_path) as f:
33+
return json.load(f).get("currentContext", "default"), "configuration file {file}".format(file=docker_cfg_path)
34+
except Exception:
35+
pass
36+
return "default", "fallback value"
37+
38+
39+
def get_current_context_name():
40+
return get_current_context_name_with_source()[0]
41+
42+
43+
def write_context_name_to_docker_config(name=None):
44+
if name == 'default':
45+
name = None
46+
docker_cfg_path = find_config_file()
47+
config = {}
48+
if docker_cfg_path:
49+
try:
50+
with open(docker_cfg_path) as f:
51+
config = json.load(f)
52+
except Exception as e:
53+
return e
54+
current_context = config.get("currentContext", None)
55+
if current_context and not name:
56+
del config["currentContext"]
57+
elif name:
58+
config["currentContext"] = name
59+
else:
60+
return
61+
if not docker_cfg_path:
62+
docker_cfg_path = get_default_config_file()
63+
try:
64+
with open(docker_cfg_path, "w") as f:
65+
json.dump(config, f, indent=4)
66+
except Exception as e:
67+
return e
68+
69+
70+
def get_context_id(name):
71+
return hashlib.sha256(name.encode('utf-8')).hexdigest()
72+
73+
74+
def get_context_dir():
75+
docker_cfg_path = find_config_file() or get_default_config_file()
76+
return os.path.join(os.path.dirname(docker_cfg_path), "contexts")
77+
78+
79+
def get_meta_dir(name=None):
80+
meta_dir = os.path.join(get_context_dir(), "meta")
81+
if name:
82+
return os.path.join(meta_dir, get_context_id(name))
83+
return meta_dir
84+
85+
86+
def get_meta_file(name):
87+
return os.path.join(get_meta_dir(name), METAFILE)
88+
89+
90+
def get_tls_dir(name=None, endpoint=""):
91+
context_dir = get_context_dir()
92+
if name:
93+
return os.path.join(context_dir, "tls", get_context_id(name), endpoint)
94+
return os.path.join(context_dir, "tls")
95+
96+
97+
def get_context_host(path=None, tls=False):
98+
host = parse_host(path, IS_WINDOWS_PLATFORM, tls)
99+
if host == DEFAULT_UNIX_SOCKET:
100+
# remove http+ from default docker socket url
101+
if host.startswith("http+"):
102+
host = host[5:]
103+
return host

0 commit comments

Comments
 (0)