omlish 0.0.0.dev40__py3-none-any.whl → 0.0.0.dev41__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 CHANGED
@@ -1,5 +1,5 @@
1
- __version__ = '0.0.0.dev40'
2
- __revision__ = '12eed3861e67278bb050cec10f1ebd2296c0bbe9-dirty'
1
+ __version__ = '0.0.0.dev41'
2
+ __revision__ = '9666ba3a1c40b95074dfcacd1c0c87ca5e267d6f'
3
3
 
4
4
 
5
5
  #
@@ -0,0 +1,27 @@
1
+ from .cli import ( # noqa
2
+ Inspect,
3
+ Port,
4
+ PsItem,
5
+ cli_inspect,
6
+ cli_ps,
7
+ has_cli,
8
+ parse_port,
9
+ )
10
+
11
+ from .compose import ( # noqa
12
+ ComposeConfig,
13
+ get_compose_port,
14
+ )
15
+
16
+ from .helpers import ( # noqa
17
+ DOCKER_FOR_MAC_HOSTNAME,
18
+ DOCKER_HOST_PLATFORM_KEY,
19
+ get_docker_host_platform,
20
+ is_likely_in_docker,
21
+ timebomb_payload,
22
+ )
23
+
24
+ from .hub import ( # noqa
25
+ HubRepoInfo,
26
+ get_hub_repo_info,
27
+ )
omlish/docker/cli.py ADDED
@@ -0,0 +1,101 @@
1
+ """
2
+ TODO:
3
+ - https://github.com/mag37/dockcheck/blob/3d122f2b868eb53a25a3014f0f6bd499390a3a29/dockcheck.sh
4
+ - https://github.com/regclient/regclient
5
+ - https://stackoverflow.com/questions/71409458/how-to-download-docker-image-using-http-api-using-docker-hub-credentials
6
+ - https://stackoverflow.com/questions/55386202/how-can-i-use-the-docker-registry-api-to-pull-information-about-a-container-get
7
+ - https://ops.tips/blog/inspecting-docker-image-without-pull/
8
+ """ # noqa
9
+ import datetime
10
+ import re
11
+ import subprocess
12
+ import typing as ta
13
+
14
+ from .. import check
15
+ from .. import dataclasses as dc
16
+ from .. import lang
17
+ from .. import marshal as msh
18
+ from ..formats import json
19
+
20
+
21
+ @dc.dataclass(frozen=True)
22
+ @msh.update_object_metadata(field_naming=msh.Naming.CAMEL, unknown_field='x')
23
+ @msh.update_fields_metadata(['id'], name='ID')
24
+ class PsItem(lang.Final):
25
+ command: str
26
+ created_at: datetime.datetime
27
+ id: str
28
+ image: str
29
+ labels: str
30
+ local_volumes: str
31
+ mounts: str
32
+ names: str
33
+ networks: str
34
+ ports: str
35
+ running_for: str
36
+ size: str
37
+ state: str
38
+ status: str
39
+
40
+ x: ta.Mapping[str, ta.Any] | None = None
41
+
42
+
43
+ class Port(ta.NamedTuple):
44
+ ip: str
45
+ from_port: int
46
+ to_port: int
47
+ proto: str
48
+
49
+
50
+ _PORT_PAT = re.compile(r'(?P<ip>[^:]+):(?P<from_port>\d+)->(?P<to_port>\d+)/(?P<proto>\w+)')
51
+
52
+
53
+ def parse_port(s: str) -> Port:
54
+ # '0.0.0.0:35221->22/tcp, 0.0.0.0:35220->8000/tcp'
55
+ m = check.not_none(_PORT_PAT.fullmatch(s))
56
+ return Port(
57
+ m.group('ip'),
58
+ int(m.group('from_port')),
59
+ int(m.group('to_port')),
60
+ m.group('proto'),
61
+ )
62
+
63
+
64
+ def cli_ps() -> list[PsItem]:
65
+ o = subprocess.check_output([
66
+ 'docker',
67
+ 'ps',
68
+ '--no-trunc',
69
+ '--format', '{{json .}}',
70
+ ])
71
+
72
+ ret: list[PsItem] = []
73
+ for l in o.decode().splitlines():
74
+ d = json.loads(l)
75
+ pi = msh.unmarshal(d, PsItem)
76
+ ret.append(pi)
77
+
78
+ return ret
79
+
80
+
81
+ @dc.dataclass(frozen=True)
82
+ @msh.update_object_metadata(field_naming=msh.Naming.CAMEL, unknown_field='x')
83
+ class Inspect(lang.Final):
84
+ id: str
85
+ created: datetime.datetime
86
+
87
+ x: ta.Mapping[str, ta.Any] | None = None
88
+
89
+
90
+ def cli_inspect(ids: list[str]) -> list[Inspect]:
91
+ o = subprocess.check_output(['docker', 'inspect', *ids])
92
+ return msh.unmarshal(json.loads(o.decode()), list[Inspect])
93
+
94
+
95
+ def has_cli() -> bool:
96
+ try:
97
+ proc = subprocess.run(['docker', '--version']) # noqa
98
+ except (FileNotFoundError, subprocess.CalledProcessError):
99
+ return False
100
+ else:
101
+ return not proc.returncode
@@ -0,0 +1,51 @@
1
+ """
2
+ TODO:
3
+ - merged compose configs: https://github.com/wrmsr/bane/blob/27647abdcfb323b73e6982a5c318c7029496b203/core/dev/docker/compose.go#L38
4
+ """ # noqa
5
+ import typing as ta
6
+
7
+ from .. import check
8
+ from .. import lang
9
+
10
+
11
+ if ta.TYPE_CHECKING:
12
+ import yaml
13
+ else:
14
+ yaml = lang.proxy_import('yaml')
15
+
16
+
17
+ class ComposeConfig:
18
+ def __init__(
19
+ self,
20
+ prefix: str,
21
+ *,
22
+ file_path: str | None = None,
23
+ ) -> None:
24
+ super().__init__()
25
+
26
+ self._prefix = prefix
27
+ self._file_path = file_path
28
+
29
+ @lang.cached_function
30
+ def get_config(self) -> ta.Mapping[str, ta.Any]:
31
+ with open(check.not_none(self._file_path)) as f:
32
+ buf = f.read()
33
+ return yaml.safe_load(buf)
34
+
35
+ @lang.cached_function
36
+ def get_services(self) -> ta.Mapping[str, ta.Any]:
37
+ ret = {}
38
+ for n, c in self.get_config()['services'].items():
39
+ check.state(n.startswith(self._prefix))
40
+ ret[n[len(self._prefix):]] = c
41
+
42
+ return ret
43
+
44
+
45
+ def get_compose_port(cfg: ta.Mapping[str, ta.Any], default: int) -> int:
46
+ return check.single(
47
+ int(l)
48
+ for p in cfg['ports']
49
+ for l, r in [p.split(':')]
50
+ if int(r) == default
51
+ )
@@ -0,0 +1,48 @@
1
+ import os
2
+ import re
3
+ import shlex
4
+ import sys
5
+
6
+
7
+ ##
8
+
9
+
10
+ _DEFAULT_TIMEBOMB_NAME = '-'.join([*__name__.split('.'), 'timebomb'])
11
+
12
+
13
+ def timebomb_payload(delay_s: float, name: str = _DEFAULT_TIMEBOMB_NAME) -> str:
14
+ return (
15
+ '('
16
+ f'echo {shlex.quote(name)} && '
17
+ f'sleep {delay_s:g} && '
18
+ 'sh -c \'killall5 -9 -o $PPID -o $$ ; kill 1\''
19
+ ') &'
20
+ )
21
+
22
+
23
+ ##
24
+
25
+
26
+ DOCKER_FOR_MAC_HOSTNAME = 'docker.for.mac.localhost'
27
+
28
+
29
+ _LIKELY_IN_DOCKER_PATTERN = re.compile(r'^overlay / .*/docker/')
30
+
31
+
32
+ def is_likely_in_docker() -> bool:
33
+ if getattr(sys, 'platform') != 'linux':
34
+ return False
35
+ with open('/proc/mounts') as f:
36
+ ls = f.readlines()
37
+ return any(_LIKELY_IN_DOCKER_PATTERN.match(l) for l in ls)
38
+
39
+
40
+ ##
41
+
42
+
43
+ # Set by pyproject, docker-dev script
44
+ DOCKER_HOST_PLATFORM_KEY = 'DOCKER_HOST_PLATFORM'
45
+
46
+
47
+ def get_docker_host_platform() -> str | None:
48
+ return os.environ.get(DOCKER_HOST_PLATFORM_KEY)
omlish/docker/hub.py ADDED
@@ -0,0 +1,75 @@
1
+ import typing as ta
2
+ import urllib.request
3
+
4
+ from .. import dataclasses as dc
5
+ from ..formats import json
6
+
7
+
8
+ @dc.dataclass(frozen=True)
9
+ class HubRepoInfo:
10
+ repo: str
11
+ tags: ta.Mapping[str, ta.Any]
12
+ latest_manifests: ta.Mapping[str, ta.Any]
13
+
14
+
15
+ def get_hub_repo_info(
16
+ repo: str,
17
+ *,
18
+ auth_url: str = 'https://auth.docker.io/',
19
+ api_url: str = 'https://registry-1.docker.io/v2/',
20
+ ) -> HubRepoInfo:
21
+ """
22
+ https://stackoverflow.com/a/39376254
23
+
24
+ ==
25
+
26
+ repo=library/nginx
27
+ token=$(
28
+ curl -s "https://auth.docker.io/token?service=registry.docker.io&scope=repository:${repo}:pull" \
29
+ | jq -r '.token' \
30
+ )
31
+ curl -H "Authorization: Bearer $token" -s "https://registry-1.docker.io/v2/${repo}/tags/list" | jq
32
+ curl \
33
+ -H "Accept: application/vnd.docker.distribution.manifest.v2+json" \
34
+ -H "Accept: application/vnd.docker.distribution.manifest.list.v2+json" \
35
+ -H "Authorization: Bearer $token" \
36
+ -s "https://registry-1.docker.io/v2/${repo}/manifests/latest" \
37
+ | jq .
38
+ """
39
+
40
+ auth_url = auth_url.rstrip('/')
41
+ api_url = api_url.rstrip('/')
42
+
43
+ #
44
+
45
+ def req_json(url: str, **kwargs: ta.Any) -> ta.Any:
46
+ with urllib.request.urlopen(urllib.request.Request(url, **kwargs)) as resp: # noqa
47
+ return json.loads(resp.read().decode('utf-8'))
48
+
49
+ #
50
+
51
+ token_dct = req_json(f'{auth_url}/token?service=registry.docker.io&scope=repository:{repo}:pull')
52
+ token = token_dct['token']
53
+
54
+ req_hdrs = {'Authorization': f'Bearer {token}'}
55
+
56
+ #
57
+
58
+ tags_dct = req_json(
59
+ f'{api_url}/{repo}/tags/list',
60
+ headers=req_hdrs,
61
+ )
62
+
63
+ latest_mani_dct = req_json(
64
+ f'{api_url}/{repo}/manifests/latest',
65
+ headers={
66
+ **req_hdrs,
67
+ 'Accept': 'application/vnd.docker.distribution.manifest.v2+json',
68
+ },
69
+ )
70
+
71
+ return HubRepoInfo(
72
+ repo,
73
+ tags_dct,
74
+ latest_mani_dct,
75
+ )
@@ -0,0 +1,166 @@
1
+ """
2
+ https://github.com/openshift/docker-distribution/blob/master/docs/spec/manifest-v2-2.md
3
+ """
4
+ import typing as ta
5
+
6
+ from .. import dataclasses as dc
7
+ from .. import lang
8
+ from .. import marshal as msh
9
+
10
+
11
+ SCHEMA_VERSION = 2
12
+
13
+
14
+ class MediaTypes(lang.Namespace):
15
+ # schema1 (existing manifest format)
16
+ MANIFEST_V1 = 'application/vnd.docker.distribution.manifest.v1+json'
17
+
18
+ # New image manifest format (schemaVersion = 2)
19
+ MANIFEST_V2 = 'application/vnd.docker.distribution.manifest.v2+json'
20
+
21
+ # Manifest list, aka "fat manifest"
22
+ MANIFEST_LIST = 'application/vnd.docker.distribution.manifest.list.v2+json'
23
+
24
+ # Container config JSON
25
+ CONTAINER_CONFIG = 'application/vnd.docker.container.image.v1+json'
26
+
27
+ # "Layer", as a gzipped tar
28
+ LAYER = 'application/vnd.docker.image.rootfs.diff.tar.gzip'
29
+
30
+ # "Layer", as a gzipped tar that should never be pushed
31
+ LAYER_NEVER_PUSH = 'application/vnd.docker.image.rootfs.foreign.diff.tar.gzip'
32
+
33
+ # Plugin config JSON
34
+ PLUGIN_CONFIG = 'application/vnd.docker.plugin.v1+json'
35
+
36
+
37
+ @dc.dataclass(frozen=True, kw_only=True)
38
+ @msh.update_object_metadata(field_naming=msh.Naming.LOW_CAMEL, unknown_field='x')
39
+ @msh.update_fields_metadata(['os_version'], name='os.version')
40
+ @msh.update_fields_metadata(['os_features'], name='os.features')
41
+ class Platform(lang.Final):
42
+ # The architecture field specifies the CPU architecture, for example amd64 or ppc64le.
43
+ architecture: str
44
+
45
+ # The os field specifies the operating system, for example linux or windows.
46
+ os: str
47
+
48
+ # The optional os.version field specifies the operating system version, for example 10.0.10586.
49
+ os_version: str | None = None
50
+
51
+ # The optional os.features field specifies an array of strings, each listing a required OS feature (for example on
52
+ # Windows win32k).
53
+ os_features: ta.Sequence[ta.Any] | None = None
54
+
55
+ # The optional variant field specifies a variant of the CPU, for example v6 to specify a particular CPU variant of
56
+ # the ARM CPU.
57
+ variant: str | None = None
58
+
59
+ # The optional features field specifies an array of strings, each listing a required CPU feature (for example sse4
60
+ # or aes).
61
+ features: ta.Sequence[ta.Any] | None = None
62
+
63
+ x: ta.Mapping[str, ta.Any] | None = None
64
+
65
+
66
+ @dc.dataclass(frozen=True)
67
+ @msh.update_object_metadata(field_naming=msh.Naming.LOW_CAMEL, unknown_field='x')
68
+ class Manifest(lang.Final):
69
+ # The MIME type of the referenced object. This will generally be
70
+ # application/vnd.docker.distribution.manifest.v2+json, but it could also be
71
+ # application/vnd.docker.distribution.manifest.v1+json if the manifest list references a legacy schema-1 manifest.
72
+ media_type: str
73
+
74
+ # The size in bytes of the object. This field exists so that a client will have an expected size for the content
75
+ # before validating. If the length of the retrieved content does not match the specified length, the content should
76
+ # not be trusted.
77
+ size: int
78
+
79
+ # The digest of the content, as defined by the Registry V2 HTTP API Specification.
80
+ digest: str
81
+
82
+ # The platform object describes the platform which the image in the manifest runs on. A full list of valid operating
83
+ # system and architecture values are listed in the Go language documentation for $GOOS and $GOARCH
84
+ platform: Platform | None = None
85
+
86
+ x: ta.Mapping[str, ta.Any] | None = None
87
+
88
+
89
+ @dc.dataclass(frozen=True)
90
+ @msh.update_object_metadata(field_naming=msh.Naming.LOW_CAMEL, unknown_field='x')
91
+ class ManifestList(lang.Final):
92
+ # This field specifies the image manifest schema version as an integer. This schema uses the version 2.
93
+ schema_version: int
94
+ dc.validate(lambda self: self.schema_version == SCHEMA_VERSION)
95
+
96
+ # The MIME type of the manifest list. This should be set to
97
+ # application/vnd.docker.distribution.manifest.list.v2+json.
98
+ media_type: str
99
+ dc.validate(lambda self: self.media_type == MediaTypes.MANIFEST_LIST)
100
+
101
+ # The manifests field contains a list of manifests for specific platforms.
102
+ manifests: ta.Sequence[Manifest]
103
+
104
+ x: ta.Mapping[str, ta.Any] | None = None
105
+
106
+
107
+ @dc.dataclass(frozen=True)
108
+ @msh.update_object_metadata(field_naming=msh.Naming.LOW_CAMEL, unknown_field='x')
109
+ class ImageManifest(lang.Final):
110
+ # This field specifies the image manifest schema version as an integer. This schema uses version 2.
111
+ schema_version: int
112
+ dc.validate(lambda self: self.schema_version == SCHEMA_VERSION)
113
+
114
+ # The MIME type of the manifest. This should be set to application/vnd.docker.distribution.manifest.v2+json.
115
+ media_type: str
116
+ dc.validate(lambda self: self.media_type == MediaTypes.MANIFEST_V2)
117
+
118
+ @dc.dataclass(frozen=True)
119
+ @msh.update_object_metadata(field_naming=msh.Naming.LOW_CAMEL, unknown_field='x')
120
+ class Config(lang.Final):
121
+ # The MIME type of the referenced object. This should generally be
122
+ # application/vnd.docker.container.image.v1+json.
123
+ media_type: str
124
+
125
+ # The size in bytes of the object. This field exists so that a client will have an expected size for the content
126
+ # before validating. If the length of the retrieved content does not match the specified length, the content
127
+ # should not be trusted.
128
+ size: int
129
+
130
+ # The digest of the content, as defined by the Registry V2 HTTP API Specification.
131
+ digest: str
132
+
133
+ x: ta.Mapping[str, ta.Any] | None = None
134
+
135
+ # The config field references a configuration object for a container, by digest. This configuration item is a JSON
136
+ # blob that the runtime uses to set up the container. This new schema uses a tweaked version of this configuration
137
+ # o allow image content-addressability on the daemon side.
138
+ config: Config | None = None
139
+
140
+ @dc.dataclass(frozen=True)
141
+ @msh.update_object_metadata(field_naming=msh.Naming.LOW_CAMEL, unknown_field='x')
142
+ class Layer(lang.Final):
143
+ # The MIME type of the referenced object. This should generally be
144
+ # application/vnd.docker.image.rootfs.diff.tar.gzip. Layers of type
145
+ # application/vnd.docker.image.rootfs.foreign.diff.tar.gzip may be pulled from a remote location but they should
146
+ # never be pushed.
147
+ media_type: str
148
+
149
+ # The size in bytes of the object. This field exists so that a client will have an expected size for the content
150
+ # before validating. If the length of the retrieved content does not match the specified length, the content
151
+ # should not be trusted.
152
+ size: int
153
+
154
+ # The digest of the content, as defined by the Registry V2 HTTP API Specification.
155
+ digest: str
156
+
157
+ # Provides a list of URLs from which the content may be fetched. Content must be verified against the digest and
158
+ # size. This field is optional and uncommon.
159
+ urls: ta.Sequence[str] | None = None
160
+
161
+ x: ta.Mapping[str, ta.Any] | None = None
162
+
163
+ # The layer list is ordered starting from the base image (opposite order of schema1).
164
+ layers: ta.Sequence[Layer] | None = None
165
+
166
+ x: ta.Mapping[str, ta.Any] | None = None
omlish/lang/__init__.py CHANGED
@@ -192,6 +192,7 @@ from .strings import ( # noqa
192
192
  is_sunder,
193
193
  prefix_delimited,
194
194
  prefix_lines,
195
+ replace_many,
195
196
  snake_case,
196
197
  strip_prefix,
197
198
  strip_suffix,
omlish/lang/strings.py CHANGED
@@ -2,63 +2,51 @@ import typing as ta
2
2
  import unicodedata
3
3
 
4
4
 
5
- ##
6
-
7
- @ta.overload
8
- def prefix_delimited(s: str, p: str, d: str) -> str:
9
- ...
5
+ StrOrBytes: ta.TypeAlias = str | bytes
6
+ StrOrBytesT = ta.TypeVar('StrOrBytesT', bound=StrOrBytes)
10
7
 
11
8
 
12
- @ta.overload
13
- def prefix_delimited(s: bytes, p: bytes, d: bytes) -> bytes:
14
- ...
9
+ ##
15
10
 
16
11
 
17
- def prefix_delimited(s, p, d):
18
- return d.join([p + l for l in s.split(d)])
12
+ def prefix_delimited(s: StrOrBytesT, p: StrOrBytesT, d: StrOrBytesT) -> StrOrBytesT:
13
+ return d.join([p + l for l in s.split(d)]) # type: ignore
19
14
 
20
15
 
21
- def prefix_lines(s: str, p: str) -> str:
22
- return prefix_delimited(s, p, '\n')
16
+ def prefix_lines(s: StrOrBytesT, p: StrOrBytesT) -> StrOrBytesT:
17
+ return prefix_delimited(s, p, '\n' if isinstance(s, str) else b'\n')
23
18
 
24
19
 
25
- def indent_lines(s: str, num: int) -> str:
26
- return prefix_lines(s, ' ' * num)
20
+ def indent_lines(s: StrOrBytesT, num: StrOrBytesT) -> StrOrBytesT:
21
+ return prefix_lines(s, (' ' if isinstance(s, str) else b' ') * num) # type: ignore
27
22
 
28
23
 
29
24
  ##
30
25
 
31
26
 
32
- @ta.overload
33
- def strip_prefix(s: str, pfx: str) -> str:
34
- ...
35
-
36
-
37
- @ta.overload
38
- def strip_prefix(s: bytes, pfx: bytes) -> bytes:
39
- ...
40
-
41
-
42
- def strip_prefix(s, pfx):
43
- if not s.startswith(pfx):
27
+ def strip_prefix(s: StrOrBytesT, pfx: StrOrBytesT) -> StrOrBytesT:
28
+ if not s.startswith(pfx): # type: ignore
44
29
  raise ValueError(f'{s!r} does not start with {pfx!r}')
45
- return s[len(pfx):]
30
+ return s[len(pfx):] # type: ignore
46
31
 
47
32
 
48
- @ta.overload
49
- def strip_suffix(s: str, sfx: str) -> str:
50
- ...
33
+ def strip_suffix(s: StrOrBytesT, sfx: StrOrBytesT) -> StrOrBytesT:
34
+ if not s.endswith(sfx): # type: ignore
35
+ raise ValueError(f'{s!r} does not end with {sfx!r}')
36
+ return s[:-len(sfx)] # type: ignore
51
37
 
52
38
 
53
- @ta.overload
54
- def strip_suffix(s: bytes, sfx: bytes) -> bytes:
55
- ...
39
+ ##
56
40
 
57
41
 
58
- def strip_suffix(s, sfx):
59
- if not s.endswith(sfx):
60
- raise ValueError(f'{s!r} does not end with {sfx!r}')
61
- return s[:-len(sfx)]
42
+ def replace_many(
43
+ s: StrOrBytesT,
44
+ old: ta.Iterable[StrOrBytesT],
45
+ new: StrOrBytesT, count_each: int = -1,
46
+ ) -> StrOrBytesT:
47
+ for o in old:
48
+ s = s.replace(o, new, count_each) # type: ignore
49
+ return s
62
50
 
63
51
 
64
52
  ##
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: omlish
3
- Version: 0.0.0.dev40
3
+ Version: 0.0.0.dev41
4
4
  Summary: omlish
5
5
  Author: wrmsr
6
6
  License: BSD-3-Clause
@@ -1,5 +1,5 @@
1
1
  omlish/.manifests.json,sha256=FPSgLg_3QDVzKlNOuqV3dMqhja2KG_TUfKdGmHE8Eg4,803
2
- omlish/__about__.py,sha256=RFPy3zw7QYcods5DomAlNdYeo4ySug48-5ISyCkIHJg,2925
2
+ omlish/__about__.py,sha256=7FsOWhuWPr63aM9w6D-8mkjM3PSH0TI7v26pjaJDmCg,2919
3
3
  omlish/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  omlish/argparse.py,sha256=QUUS6sv2ftfcmhDj4qU429mEE_lfWa0UN5yYpPG65B0,6265
5
5
  omlish/c3.py,sha256=4vogWgwPb8TbNS2KkZxpoWbwjj7MuHG2lQG-hdtkvjI,8062
@@ -7,7 +7,6 @@ omlish/cached.py,sha256=UAizxlH4eMWHPzQtmItmyE6FEpFEUFzIkxaO2BHWZ5s,196
7
7
  omlish/check.py,sha256=fgWiBoHvqZlE8tjaxK7OMW4J8z3_rEGRENTka3EohbI,10378
8
8
  omlish/datetimes.py,sha256=HajeM1kBvwlTa-uR1TTZHmZ3zTPnnUr1uGGQhiO1XQ0,2152
9
9
  omlish/defs.py,sha256=T3bq_7h_tO3nDB5RAFBn7DkdeQgqheXzkFColbOHZko,4890
10
- omlish/docker.py,sha256=vmyQA9HCmBQPnOMGcQVomiyAY3TsMKI-9e7Nt8ptrH8,6523
11
10
  omlish/dynamic.py,sha256=35C_cCX_Vq2HrHzGk5T-zbrMvmUdiIiwDzDNixczoDo,6541
12
11
  omlish/fnpairs.py,sha256=Sl8CMFNyDS-1JYAjSWqnT5FmUm9Lj6o7FxSRo7g4jww,10875
13
12
  omlish/fnpipes.py,sha256=AJkgz9nvRRm7oqw7ZgYyz21klu276LWi54oYCLg-vOg,2196
@@ -109,6 +108,12 @@ omlish/dispatch/_dispatch3.py,sha256=Vnu5DfoPWFJLodudBqoZBXGTi2wYk-Az56MXJgdQvwc
109
108
  omlish/dispatch/dispatch.py,sha256=8B66wOat30HckcIsCq4pnutBy20iSPwPQOqJ4msHaGU,3739
110
109
  omlish/dispatch/functions.py,sha256=S8ElsLi6DKxTdtFGigWaF0vAquwy2sK-3f4iRLaYq70,1522
111
110
  omlish/dispatch/methods.py,sha256=XHjwwC9Gn4iDWxbyLAcbdSwRgVaq-8Bnn5cAwf5oZdA,5403
111
+ omlish/docker/__init__.py,sha256=LGL5ByHrd7EaQnIDO6eLQvovDamngUiTfnpThV_4-MA,437
112
+ omlish/docker/cli.py,sha256=gtb9kitVfGnd4cr587NsVVk8D5Ok5y5SAsqD_SwGrSA,2565
113
+ omlish/docker/compose.py,sha256=4drmnGQzbkOFJ9B6XSg9rnXkJeZz1ETmdcMe1PE790U,1237
114
+ omlish/docker/helpers.py,sha256=j2eZIqIUpy34ZmoGyIzsYuKx9HeewwYBfrGNC99EFYk,928
115
+ omlish/docker/hub.py,sha256=YcDYOi6t1FA2Sp0RVrmZ9cBXbzFWQ8wTps3wOskA-K0,1955
116
+ omlish/docker/manifests.py,sha256=LR4FpOGNUT3bZQ-gTjB6r_-1C3YiG30QvevZjrsVUQM,7068
112
117
  omlish/formats/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
113
118
  omlish/formats/dotenv.py,sha256=UjZl3gac-0U24sDjCCGMcCqO1UCWG2Zs8PZ4JdAg2YE,17348
114
119
  omlish/formats/json.py,sha256=61XG6rveb3SSXmYrKvUmRdaVDyMD6C-7yVqXBBMu8t8,1017
@@ -163,7 +168,7 @@ omlish/inject/impl/privates.py,sha256=alpCYyk5VJ9lJknbRH2nLVNFYVvFhkj-VC1Vco3zCF
163
168
  omlish/inject/impl/providers.py,sha256=QnwhsujJFIHC0JTgd2Wlo1kP53i3CWTrj1nKU2DNxwg,2375
164
169
  omlish/inject/impl/proxy.py,sha256=1ko0VaKqzu9UG8bIldp9xtUrAVUOFTKWKTjOCqIGr4s,1636
165
170
  omlish/inject/impl/scopes.py,sha256=ASfULXgP_ETlsAqFJfrZmyEaZt64Zr8tNn5ScA-EoXk,5900
166
- omlish/lang/__init__.py,sha256=jDIv9mr3Y85S3DpUroaLty7q9iwMUjUlKwyUxs-zlXg,3618
171
+ omlish/lang/__init__.py,sha256=-DRmyoSAwSWOh7nJh4UrpR-w_hGQfe-e06S9qLjHZF8,3636
167
172
  omlish/lang/cached.py,sha256=92TvRZQ6sWlm7dNn4hgl7aWKbX0J1XUEo3DRjBpgVQk,7834
168
173
  omlish/lang/clsdct.py,sha256=AjtIWLlx2E6D5rC97zQ3Lwq2SOMkbg08pdO_AxpzEHI,1744
169
174
  omlish/lang/cmp.py,sha256=5vbzWWbqdzDmNKAGL19z6ZfUKe5Ci49e-Oegf9f4BsE,1346
@@ -178,7 +183,7 @@ omlish/lang/maybes.py,sha256=NYHZDjqDtwPMheDrj2VtUVujxRPf8Qpgk4ZlZCTvBZc,3492
178
183
  omlish/lang/objects.py,sha256=1dY8dX5voIZf5FBYUiN0BRsWg2JCdsgRbDl9fLG7OtY,4310
179
184
  omlish/lang/resolving.py,sha256=OuN2mDTPNyBUbcrswtvFKtj4xgH4H4WglgqSKv3MTy0,1606
180
185
  omlish/lang/resources.py,sha256=-NmVTrSMKFZ6smVfOMz46ekZYVGgYh8cPooxQlFpG6s,2135
181
- omlish/lang/strings.py,sha256=LqxR49cF5owHKpbUX8nUIBPsZ-4w7xqHPhlB-HIWqUg,3628
186
+ omlish/lang/strings.py,sha256=LWgUy9WghUyV0zmZ1c3HZjEfekLlNPy7Jl6J1Z5vzp0,3882
182
187
  omlish/lang/sys.py,sha256=UoZz_PJYVKLQAKqYxxn-LHz1okK_38I__maZgnXMcxU,406
183
188
  omlish/lang/timeouts.py,sha256=vECdWYhc_IZgcal1Ng1Y42wf2FV3KAx-i8As-MgGHIQ,1186
184
189
  omlish/lang/typing.py,sha256=lJ2NGe4Pmb61I0Tx4A_rOqXNFTws1XHOzafg2knRUio,4155
@@ -320,9 +325,9 @@ omlish/text/delimit.py,sha256=ubPXcXQmtbOVrUsNh5gH1mDq5H-n1y2R4cPL5_DQf68,4928
320
325
  omlish/text/glyphsplit.py,sha256=Ug-dPRO7x-OrNNr8g1y6DotSZ2KH0S-VcOmUobwa4B0,3296
321
326
  omlish/text/indent.py,sha256=6Jj6TFY9unaPa4xPzrnZemJ-fHsV53IamP93XGjSUHs,1274
322
327
  omlish/text/parts.py,sha256=7vPF1aTZdvLVYJ4EwBZVzRSy8XB3YqPd7JwEnNGGAOo,6495
323
- omlish-0.0.0.dev40.dist-info/LICENSE,sha256=B_hVtavaA8zCYDW99DYdcpDLKz1n3BBRjZrcbv8uG8c,1451
324
- omlish-0.0.0.dev40.dist-info/METADATA,sha256=cgGflLfobBAX38YnOg4rq76Ep04gAV4o75NXd0QGrd8,3817
325
- omlish-0.0.0.dev40.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
326
- omlish-0.0.0.dev40.dist-info/entry_points.txt,sha256=Lt84WvRZJskWCAS7xnQGZIeVWksprtUHj0llrvVmod8,35
327
- omlish-0.0.0.dev40.dist-info/top_level.txt,sha256=pePsKdLu7DvtUiecdYXJ78iO80uDNmBlqe-8hOzOmfs,7
328
- omlish-0.0.0.dev40.dist-info/RECORD,,
328
+ omlish-0.0.0.dev41.dist-info/LICENSE,sha256=B_hVtavaA8zCYDW99DYdcpDLKz1n3BBRjZrcbv8uG8c,1451
329
+ omlish-0.0.0.dev41.dist-info/METADATA,sha256=r8Q2E7Ch6npanZY7xAo-quF5Z9R9OWkH6eY2JATdOfs,3817
330
+ omlish-0.0.0.dev41.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
331
+ omlish-0.0.0.dev41.dist-info/entry_points.txt,sha256=Lt84WvRZJskWCAS7xnQGZIeVWksprtUHj0llrvVmod8,35
332
+ omlish-0.0.0.dev41.dist-info/top_level.txt,sha256=pePsKdLu7DvtUiecdYXJ78iO80uDNmBlqe-8hOzOmfs,7
333
+ omlish-0.0.0.dev41.dist-info/RECORD,,
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
- )