omlish 0.0.0.dev40__py3-none-any.whl → 0.0.0.dev42__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- omlish/__about__.py +2 -2
- omlish/dataclasses/__init__.py +1 -0
- omlish/dataclasses/impl/__init__.py +1 -0
- omlish/dataclasses/utils.py +4 -0
- omlish/docker/__init__.py +27 -0
- omlish/docker/cli.py +101 -0
- omlish/docker/compose.py +51 -0
- omlish/docker/helpers.py +48 -0
- omlish/docker/hub.py +75 -0
- omlish/docker/manifests.py +166 -0
- omlish/lang/__init__.py +1 -0
- omlish/lang/strings.py +25 -37
- omlish/marshal/__init__.py +1 -0
- omlish/marshal/dataclasses.py +68 -15
- omlish/marshal/helpers.py +2 -8
- omlish/marshal/objects.py +58 -15
- omlish/specs/openapi/__init__.py +5 -0
- omlish/specs/openapi/marshal.py +63 -0
- omlish/specs/openapi/openapi.py +443 -0
- {omlish-0.0.0.dev40.dist-info → omlish-0.0.0.dev42.dist-info}/METADATA +1 -1
- {omlish-0.0.0.dev40.dist-info → omlish-0.0.0.dev42.dist-info}/RECORD +25 -17
- omlish/docker.py +0 -273
- {omlish-0.0.0.dev40.dist-info → omlish-0.0.0.dev42.dist-info}/LICENSE +0 -0
- {omlish-0.0.0.dev40.dist-info → omlish-0.0.0.dev42.dist-info}/WHEEL +0 -0
- {omlish-0.0.0.dev40.dist-info → omlish-0.0.0.dev42.dist-info}/entry_points.txt +0 -0
- {omlish-0.0.0.dev40.dist-info → omlish-0.0.0.dev42.dist-info}/top_level.txt +0 -0
omlish/docker.py
DELETED
@@ -1,273 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
TODO:
|
3
|
-
- merged compose configs: https://github.com/wrmsr/bane/blob/27647abdcfb323b73e6982a5c318c7029496b203/core/dev/docker/compose.go#L38
|
4
|
-
- https://github.com/mag37/dockcheck/blob/3d122f2b868eb53a25a3014f0f6bd499390a3a29/dockcheck.sh
|
5
|
-
- https://github.com/regclient/regclient
|
6
|
-
- https://stackoverflow.com/questions/71409458/how-to-download-docker-image-using-http-api-using-docker-hub-credentials
|
7
|
-
- https://stackoverflow.com/questions/55386202/how-can-i-use-the-docker-registry-api-to-pull-information-about-a-container-get
|
8
|
-
- https://ops.tips/blog/inspecting-docker-image-without-pull/
|
9
|
-
|
10
|
-
""" # noqa
|
11
|
-
import datetime
|
12
|
-
import os
|
13
|
-
import re
|
14
|
-
import shlex
|
15
|
-
import subprocess
|
16
|
-
import sys
|
17
|
-
import typing as ta
|
18
|
-
import urllib.request
|
19
|
-
|
20
|
-
from . import check
|
21
|
-
from . import dataclasses as dc
|
22
|
-
from . import lang
|
23
|
-
from . import marshal as msh
|
24
|
-
from .formats import json
|
25
|
-
|
26
|
-
|
27
|
-
if ta.TYPE_CHECKING:
|
28
|
-
import yaml
|
29
|
-
else:
|
30
|
-
yaml = lang.proxy_import('yaml')
|
31
|
-
|
32
|
-
|
33
|
-
##
|
34
|
-
|
35
|
-
|
36
|
-
@dc.dataclass(frozen=True)
|
37
|
-
@msh.update_object_metadata(field_naming=msh.Naming.CAMEL, unknown_field='x')
|
38
|
-
@msh.update_fields_metadata(['id'], name='ID')
|
39
|
-
class PsItem(lang.Final):
|
40
|
-
command: str
|
41
|
-
created_at: datetime.datetime
|
42
|
-
id: str
|
43
|
-
image: str
|
44
|
-
labels: str
|
45
|
-
local_volumes: str
|
46
|
-
mounts: str
|
47
|
-
names: str
|
48
|
-
networks: str
|
49
|
-
ports: str
|
50
|
-
running_for: str
|
51
|
-
size: str
|
52
|
-
state: str
|
53
|
-
status: str
|
54
|
-
|
55
|
-
x: ta.Mapping[str, ta.Any] | None = None
|
56
|
-
|
57
|
-
|
58
|
-
class Port(ta.NamedTuple):
|
59
|
-
ip: str
|
60
|
-
from_port: int
|
61
|
-
to_port: int
|
62
|
-
proto: str
|
63
|
-
|
64
|
-
|
65
|
-
_PORT_PAT = re.compile(r'(?P<ip>[^:]+):(?P<from_port>\d+)->(?P<to_port>\d+)/(?P<proto>\w+)')
|
66
|
-
|
67
|
-
|
68
|
-
def parse_port(s: str) -> Port:
|
69
|
-
# '0.0.0.0:35221->22/tcp, 0.0.0.0:35220->8000/tcp'
|
70
|
-
m = check.not_none(_PORT_PAT.fullmatch(s))
|
71
|
-
return Port(
|
72
|
-
m.group('ip'),
|
73
|
-
int(m.group('from_port')),
|
74
|
-
int(m.group('to_port')),
|
75
|
-
m.group('proto'),
|
76
|
-
)
|
77
|
-
|
78
|
-
|
79
|
-
def cli_ps() -> list[PsItem]:
|
80
|
-
o = subprocess.check_output([
|
81
|
-
'docker',
|
82
|
-
'ps',
|
83
|
-
'--no-trunc',
|
84
|
-
'--format', '{{json .}}',
|
85
|
-
])
|
86
|
-
|
87
|
-
ret: list[PsItem] = []
|
88
|
-
for l in o.decode().splitlines():
|
89
|
-
d = json.loads(l)
|
90
|
-
pi = msh.unmarshal(d, PsItem)
|
91
|
-
ret.append(pi)
|
92
|
-
|
93
|
-
return ret
|
94
|
-
|
95
|
-
|
96
|
-
@dc.dataclass(frozen=True)
|
97
|
-
@msh.update_object_metadata(field_naming=msh.Naming.CAMEL, unknown_field='x')
|
98
|
-
class Inspect(lang.Final):
|
99
|
-
id: str
|
100
|
-
created: datetime.datetime
|
101
|
-
|
102
|
-
x: ta.Mapping[str, ta.Any] | None = None
|
103
|
-
|
104
|
-
|
105
|
-
def cli_inspect(ids: list[str]) -> list[Inspect]:
|
106
|
-
o = subprocess.check_output(['docker', 'inspect', *ids])
|
107
|
-
return msh.unmarshal(json.loads(o.decode()), list[Inspect])
|
108
|
-
|
109
|
-
|
110
|
-
def has_cli() -> bool:
|
111
|
-
try:
|
112
|
-
proc = subprocess.run(['docker', '--version']) # noqa
|
113
|
-
except (FileNotFoundError, subprocess.CalledProcessError):
|
114
|
-
return False
|
115
|
-
else:
|
116
|
-
return not proc.returncode
|
117
|
-
|
118
|
-
|
119
|
-
##
|
120
|
-
|
121
|
-
|
122
|
-
class ComposeConfig:
|
123
|
-
def __init__(
|
124
|
-
self,
|
125
|
-
prefix: str,
|
126
|
-
*,
|
127
|
-
file_path: str | None = None,
|
128
|
-
) -> None:
|
129
|
-
super().__init__()
|
130
|
-
|
131
|
-
self._prefix = prefix
|
132
|
-
self._file_path = file_path
|
133
|
-
|
134
|
-
@lang.cached_function
|
135
|
-
def get_config(self) -> ta.Mapping[str, ta.Any]:
|
136
|
-
with open(check.not_none(self._file_path)) as f:
|
137
|
-
buf = f.read()
|
138
|
-
return yaml.safe_load(buf)
|
139
|
-
|
140
|
-
@lang.cached_function
|
141
|
-
def get_services(self) -> ta.Mapping[str, ta.Any]:
|
142
|
-
ret = {}
|
143
|
-
for n, c in self.get_config()['services'].items():
|
144
|
-
check.state(n.startswith(self._prefix))
|
145
|
-
ret[n[len(self._prefix):]] = c
|
146
|
-
|
147
|
-
return ret
|
148
|
-
|
149
|
-
|
150
|
-
def get_compose_port(cfg: ta.Mapping[str, ta.Any], default: int) -> int:
|
151
|
-
return check.single(
|
152
|
-
int(l)
|
153
|
-
for p in cfg['ports']
|
154
|
-
for l, r in [p.split(':')]
|
155
|
-
if int(r) == default
|
156
|
-
)
|
157
|
-
|
158
|
-
|
159
|
-
##
|
160
|
-
|
161
|
-
|
162
|
-
_DEFAULT_TIMEBOMB_NAME = '-'.join([*__name__.split('.'), 'timebomb'])
|
163
|
-
|
164
|
-
|
165
|
-
def timebomb_payload(delay_s: float, name: str = _DEFAULT_TIMEBOMB_NAME) -> str:
|
166
|
-
return (
|
167
|
-
'('
|
168
|
-
f'echo {shlex.quote(name)} && '
|
169
|
-
f'sleep {delay_s:g} && '
|
170
|
-
'sh -c \'killall5 -9 -o $PPID -o $$ ; kill 1\''
|
171
|
-
') &'
|
172
|
-
)
|
173
|
-
|
174
|
-
|
175
|
-
##
|
176
|
-
|
177
|
-
|
178
|
-
DOCKER_FOR_MAC_HOSTNAME = 'docker.for.mac.localhost'
|
179
|
-
|
180
|
-
|
181
|
-
_LIKELY_IN_DOCKER_PATTERN = re.compile(r'^overlay / .*/docker/')
|
182
|
-
|
183
|
-
|
184
|
-
def is_likely_in_docker() -> bool:
|
185
|
-
if getattr(sys, 'platform') != 'linux':
|
186
|
-
return False
|
187
|
-
with open('/proc/mounts') as f:
|
188
|
-
ls = f.readlines()
|
189
|
-
return any(_LIKELY_IN_DOCKER_PATTERN.match(l) for l in ls)
|
190
|
-
|
191
|
-
|
192
|
-
##
|
193
|
-
|
194
|
-
|
195
|
-
# Set by pyproject, docker-dev script
|
196
|
-
DOCKER_HOST_PLATFORM_KEY = 'DOCKER_HOST_PLATFORM'
|
197
|
-
|
198
|
-
|
199
|
-
def get_docker_host_platform() -> str | None:
|
200
|
-
return os.environ.get(DOCKER_HOST_PLATFORM_KEY)
|
201
|
-
|
202
|
-
|
203
|
-
##
|
204
|
-
|
205
|
-
|
206
|
-
@dc.dataclass(frozen=True)
|
207
|
-
class HubRepoInfo:
|
208
|
-
repo: str
|
209
|
-
tags: ta.Mapping[str, ta.Any]
|
210
|
-
latest_manifests: ta.Mapping[str, ta.Any]
|
211
|
-
|
212
|
-
|
213
|
-
def get_hub_repo_info(
|
214
|
-
repo: str,
|
215
|
-
*,
|
216
|
-
auth_url: str = 'https://auth.docker.io/',
|
217
|
-
api_url: str = 'https://registry-1.docker.io/v2/',
|
218
|
-
) -> HubRepoInfo:
|
219
|
-
"""
|
220
|
-
https://stackoverflow.com/a/39376254
|
221
|
-
|
222
|
-
==
|
223
|
-
|
224
|
-
repo=library/nginx
|
225
|
-
token=$(
|
226
|
-
curl -s "https://auth.docker.io/token?service=registry.docker.io&scope=repository:${repo}:pull" \
|
227
|
-
| jq -r '.token' \
|
228
|
-
)
|
229
|
-
curl -H "Authorization: Bearer $token" -s "https://registry-1.docker.io/v2/${repo}/tags/list" | jq
|
230
|
-
curl \
|
231
|
-
-H "Accept: application/vnd.docker.distribution.manifest.v2+json" \
|
232
|
-
-H "Accept: application/vnd.docker.distribution.manifest.list.v2+json" \
|
233
|
-
-H "Authorization: Bearer $token" \
|
234
|
-
-s "https://registry-1.docker.io/v2/${repo}/manifests/latest" \
|
235
|
-
| jq .
|
236
|
-
"""
|
237
|
-
|
238
|
-
auth_url = auth_url.rstrip('/')
|
239
|
-
api_url = api_url.rstrip('/')
|
240
|
-
|
241
|
-
#
|
242
|
-
|
243
|
-
def req_json(url: str, **kwargs: ta.Any) -> ta.Any:
|
244
|
-
with urllib.request.urlopen(urllib.request.Request(url, **kwargs)) as resp: # noqa
|
245
|
-
return json.loads(resp.read().decode('utf-8'))
|
246
|
-
|
247
|
-
#
|
248
|
-
|
249
|
-
token_dct = req_json(f'{auth_url}/token?service=registry.docker.io&scope=repository:{repo}:pull')
|
250
|
-
token = token_dct['token']
|
251
|
-
|
252
|
-
req_hdrs = {'Authorization': f'Bearer {token}'}
|
253
|
-
|
254
|
-
#
|
255
|
-
|
256
|
-
tags_dct = req_json(
|
257
|
-
f'{api_url}/{repo}/tags/list',
|
258
|
-
headers=req_hdrs,
|
259
|
-
)
|
260
|
-
|
261
|
-
latest_mani_dct = req_json(
|
262
|
-
f'{api_url}/{repo}/manifests/latest',
|
263
|
-
headers={
|
264
|
-
**req_hdrs,
|
265
|
-
'Accept': 'application/vnd.docker.distribution.manifest.v2+json',
|
266
|
-
},
|
267
|
-
)
|
268
|
-
|
269
|
-
return HubRepoInfo(
|
270
|
-
repo,
|
271
|
-
tags_dct,
|
272
|
-
latest_mani_dct,
|
273
|
-
)
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|