ominfra 0.0.0.dev189__py3-none-any.whl → 0.0.0.dev191__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.
@@ -1,10 +1,33 @@
1
1
  # ruff: noqa: UP006 UP007
2
2
  import datetime
3
+ import os.path
3
4
  import typing as ta
4
5
 
6
+ from omlish.lite.cached import cached_nullary
7
+ from omlish.lite.check import check
8
+ from omlish.lite.json import json_dumps_pretty
9
+ from omlish.lite.marshal import ObjMarshalerManager
5
10
  from omlish.lite.typing import Func0
11
+ from omlish.lite.typing import Func1
12
+ from omlish.os.paths import abs_real_path
13
+ from omlish.os.paths import relative_symlink
6
14
 
15
+ from .apps import DeployAppManager
16
+ from .conf.manager import DeployConfManager
17
+ from .paths.manager import DeployPathsManager
18
+ from .paths.owners import DeployPathOwner
19
+ from .paths.paths import DeployPath
20
+ from .specs import DeployAppSpec
21
+ from .specs import DeploySpec
22
+ from .systemd import DeploySystemdManager
23
+ from .tags import DeployApp
24
+ from .tags import DeployTagMap
7
25
  from .tags import DeployTime
26
+ from .tmp import DeployHomeAtomics
27
+ from .types import DeployHome
28
+
29
+
30
+ ##
8
31
 
9
32
 
10
33
  DEPLOY_TAG_DATETIME_FMT = '%Y%m%dT%H%M%SZ'
@@ -13,17 +36,65 @@ DEPLOY_TAG_DATETIME_FMT = '%Y%m%dT%H%M%SZ'
13
36
  DeployManagerUtcClock = ta.NewType('DeployManagerUtcClock', Func0[datetime.datetime])
14
37
 
15
38
 
16
- class DeployManager:
39
+ class DeployManager(DeployPathOwner):
17
40
  def __init__(
18
41
  self,
19
42
  *,
43
+ atomics: DeployHomeAtomics,
20
44
 
21
45
  utc_clock: ta.Optional[DeployManagerUtcClock] = None,
22
46
  ):
23
47
  super().__init__()
24
48
 
49
+ self._atomics = atomics
50
+
25
51
  self._utc_clock = utc_clock
26
52
 
53
+ #
54
+
55
+ # Home current link just points to CURRENT_DEPLOY_LINK, and is intended for user convenience.
56
+ HOME_CURRENT_LINK = DeployPath.parse('current')
57
+
58
+ DEPLOYS_DIR = DeployPath.parse('deploys/')
59
+
60
+ # Authoritative current symlink is not in deploy-home, just to prevent accidental corruption.
61
+ CURRENT_DEPLOY_LINK = DeployPath.parse(f'{DEPLOYS_DIR}current')
62
+ DEPLOYING_DEPLOY_LINK = DeployPath.parse(f'{DEPLOYS_DIR}deploying')
63
+
64
+ DEPLOY_DIR = DeployPath.parse(f'{DEPLOYS_DIR}@time--@deploy-key/')
65
+ DEPLOY_SPEC_FILE = DeployPath.parse(f'{DEPLOY_DIR}spec.json')
66
+
67
+ APPS_DEPLOY_DIR = DeployPath.parse(f'{DEPLOY_DIR}apps/')
68
+ APP_DEPLOY_LINK = DeployPath.parse(f'{APPS_DEPLOY_DIR}@app')
69
+
70
+ CONFS_DEPLOY_DIR = DeployPath.parse(f'{DEPLOY_DIR}conf/')
71
+ CONF_DEPLOY_DIR = DeployPath.parse(f'{CONFS_DEPLOY_DIR}@conf/')
72
+
73
+ @cached_nullary
74
+ def get_owned_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
75
+ return {
76
+ self.DEPLOYS_DIR,
77
+
78
+ self.CURRENT_DEPLOY_LINK,
79
+ self.DEPLOYING_DEPLOY_LINK,
80
+
81
+ self.DEPLOY_DIR,
82
+ self.DEPLOY_SPEC_FILE,
83
+
84
+ self.APPS_DEPLOY_DIR,
85
+ self.APP_DEPLOY_LINK,
86
+
87
+ self.CONFS_DEPLOY_DIR,
88
+ self.CONF_DEPLOY_DIR,
89
+ }
90
+
91
+ #
92
+
93
+ def render_path(self, home: DeployHome, pth: DeployPath, tags: ta.Optional[DeployTagMap] = None) -> str:
94
+ return os.path.join(check.non_empty_str(home), pth.render(tags))
95
+
96
+ #
97
+
27
98
  def _utc_now(self) -> datetime.datetime:
28
99
  if self._utc_clock is not None:
29
100
  return self._utc_clock() # noqa
@@ -32,3 +103,225 @@ class DeployManager:
32
103
 
33
104
  def make_deploy_time(self) -> DeployTime:
34
105
  return DeployTime(self._utc_now().strftime(DEPLOY_TAG_DATETIME_FMT))
106
+
107
+ #
108
+
109
+ def make_home_current_link(self, home: DeployHome) -> None:
110
+ home_current_link = os.path.join(check.non_empty_str(home), self.HOME_CURRENT_LINK.render())
111
+ current_deploy_link = os.path.join(check.non_empty_str(home), self.CURRENT_DEPLOY_LINK.render())
112
+ with self._atomics(home).begin_atomic_path_swap( # noqa
113
+ 'file',
114
+ home_current_link,
115
+ auto_commit=True,
116
+ ) as dst_swap:
117
+ os.unlink(dst_swap.tmp_path)
118
+ os.symlink(
119
+ os.path.relpath(current_deploy_link, os.path.dirname(dst_swap.dst_path)),
120
+ dst_swap.tmp_path,
121
+ )
122
+
123
+
124
+ ##
125
+
126
+
127
+ class DeployDriverFactory(Func1[DeploySpec, ta.ContextManager['DeployDriver']]):
128
+ pass
129
+
130
+
131
+ class DeployDriver:
132
+ def __init__(
133
+ self,
134
+ *,
135
+ spec: DeploySpec,
136
+ home: DeployHome,
137
+ time: DeployTime,
138
+
139
+ deploys: DeployManager,
140
+ paths: DeployPathsManager,
141
+ apps: DeployAppManager,
142
+ conf: DeployConfManager,
143
+ systemd: DeploySystemdManager,
144
+
145
+ msh: ObjMarshalerManager,
146
+ ) -> None:
147
+ super().__init__()
148
+
149
+ self._spec = spec
150
+ self._home = home
151
+ self._time = time
152
+
153
+ self._deploys = deploys
154
+ self._paths = paths
155
+ self._apps = apps
156
+ self._conf = conf
157
+ self._systemd = systemd
158
+
159
+ self._msh = msh
160
+
161
+ #
162
+
163
+ @property
164
+ def tags(self) -> DeployTagMap:
165
+ return DeployTagMap(
166
+ self._time,
167
+ self._spec.key(),
168
+ )
169
+
170
+ def render_path(self, pth: DeployPath, tags: ta.Optional[DeployTagMap] = None) -> str:
171
+ return os.path.join(self._home, pth.render(tags if tags is not None else self.tags))
172
+
173
+ @property
174
+ def dir(self) -> str:
175
+ return self.render_path(self._deploys.DEPLOY_DIR)
176
+
177
+ #
178
+
179
+ async def drive_deploy(self) -> None:
180
+ spec_json = json_dumps_pretty(self._msh.marshal_obj(self._spec))
181
+
182
+ #
183
+
184
+ das: ta.Set[DeployApp] = {a.app for a in self._spec.apps}
185
+ las: ta.Set[DeployApp] = set(self._spec.app_links.apps)
186
+ if (ras := das & las):
187
+ raise RuntimeError(f'Must not specify apps as both deploy and link: {sorted(a.s for a in ras)}')
188
+
189
+ #
190
+
191
+ self._paths.validate_deploy_paths()
192
+
193
+ #
194
+
195
+ os.makedirs(self.dir)
196
+
197
+ #
198
+
199
+ spec_file = self.render_path(self._deploys.DEPLOY_SPEC_FILE)
200
+ with open(spec_file, 'w') as f: # noqa
201
+ f.write(spec_json)
202
+
203
+ #
204
+
205
+ deploying_link = self.render_path(self._deploys.DEPLOYING_DEPLOY_LINK)
206
+ current_link = self.render_path(self._deploys.CURRENT_DEPLOY_LINK)
207
+
208
+ #
209
+
210
+ if os.path.exists(deploying_link):
211
+ os.unlink(deploying_link)
212
+ relative_symlink(
213
+ self.dir,
214
+ deploying_link,
215
+ target_is_directory=True,
216
+ make_dirs=True,
217
+ )
218
+
219
+ #
220
+
221
+ for md in [
222
+ self._deploys.APPS_DEPLOY_DIR,
223
+ self._deploys.CONFS_DEPLOY_DIR,
224
+ ]:
225
+ os.makedirs(self.render_path(md))
226
+
227
+ #
228
+
229
+ if not self._spec.app_links.exclude_unspecified:
230
+ cad = abs_real_path(os.path.join(current_link, 'apps'))
231
+ if os.path.exists(cad):
232
+ for d in os.listdir(cad):
233
+ if (da := DeployApp(d)) not in das:
234
+ las.add(da)
235
+
236
+ for la in self._spec.app_links.apps:
237
+ await self._drive_app_link(
238
+ la,
239
+ current_link,
240
+ )
241
+
242
+ for app in self._spec.apps:
243
+ await self._drive_app_deploy(
244
+ app,
245
+ )
246
+
247
+ #
248
+
249
+ os.replace(deploying_link, current_link)
250
+
251
+ #
252
+
253
+ await self._systemd.sync_systemd(
254
+ self._spec.systemd,
255
+ self._home,
256
+ os.path.join(self.dir, 'conf', 'systemd'), # FIXME
257
+ )
258
+
259
+ #
260
+
261
+ self._deploys.make_home_current_link(self._home)
262
+
263
+ #
264
+
265
+ async def _drive_app_deploy(self, app: DeployAppSpec) -> None:
266
+ pa = await self._apps.prepare_app(
267
+ app,
268
+ self._home,
269
+ self.tags,
270
+ )
271
+
272
+ #
273
+
274
+ app_link = self.render_path(self._deploys.APP_DEPLOY_LINK, pa.tags)
275
+ relative_symlink(
276
+ pa.dir,
277
+ app_link,
278
+ target_is_directory=True,
279
+ make_dirs=True,
280
+ )
281
+
282
+ #
283
+
284
+ await self._drive_app_configure(pa)
285
+
286
+ async def _drive_app_link(
287
+ self,
288
+ app: DeployApp,
289
+ current_link: str,
290
+ ) -> None:
291
+ app_link = os.path.join(abs_real_path(current_link), 'apps', app.s)
292
+ check.state(os.path.islink(app_link))
293
+
294
+ app_dir = abs_real_path(app_link)
295
+ check.state(os.path.isdir(app_dir))
296
+
297
+ #
298
+
299
+ pa = await self._apps.prepare_app_link(
300
+ self.tags,
301
+ app_dir,
302
+ )
303
+
304
+ #
305
+
306
+ relative_symlink(
307
+ app_dir,
308
+ os.path.join(self.dir, 'apps', app.s),
309
+ target_is_directory=True,
310
+ )
311
+
312
+ #
313
+
314
+ await self._drive_app_configure(pa)
315
+
316
+ async def _drive_app_configure(
317
+ self,
318
+ pa: DeployAppManager.PreparedApp,
319
+ ) -> None:
320
+ deploy_conf_dir = self.render_path(self._deploys.CONFS_DEPLOY_DIR)
321
+ if pa.spec.conf is not None:
322
+ await self._conf.link_app_conf(
323
+ pa.spec.conf,
324
+ pa.tags,
325
+ check.non_empty_str(pa.conf_dir),
326
+ deploy_conf_dir,
327
+ )
@@ -16,15 +16,16 @@ from .commands import DeployCommand
16
16
  from .commands import DeployCommandExecutor
17
17
  from .conf.inject import bind_deploy_conf
18
18
  from .config import DeployConfig
19
+ from .deploy import DeployDriver
20
+ from .deploy import DeployDriverFactory
19
21
  from .deploy import DeployManager
20
- from .driver import DeployDriver
21
- from .driver import DeployDriverFactory
22
22
  from .git import DeployGitManager
23
23
  from .inject_ import bind_deploy_manager
24
24
  from .interp import InterpCommand
25
25
  from .interp import InterpCommandExecutor
26
26
  from .paths.inject import bind_deploy_paths
27
27
  from .specs import DeploySpec
28
+ from .systemd import DeploySystemdManager
28
29
  from .tags import DeployTime
29
30
  from .tmp import DeployHomeAtomics
30
31
  from .tmp import DeployTmpManager
@@ -101,13 +102,10 @@ def bind_deploy(
101
102
 
102
103
  lst.extend([
103
104
  bind_deploy_manager(DeployAppManager),
104
-
105
105
  bind_deploy_manager(DeployGitManager),
106
-
107
106
  bind_deploy_manager(DeployManager),
108
-
107
+ bind_deploy_manager(DeploySystemdManager),
109
108
  bind_deploy_manager(DeployTmpManager),
110
-
111
109
  bind_deploy_manager(DeployVenvManager),
112
110
  ])
113
111
 
@@ -0,0 +1,8 @@
1
+ # ruff: noqa: UP006 UP007
2
+ """
3
+ verify - nginx -t
4
+ """
5
+
6
+
7
+ class DeployNginxManager:
8
+ pass
@@ -33,6 +33,10 @@ class DeployPathError(Exception):
33
33
 
34
34
 
35
35
  class DeployPathRenderable(abc.ABC):
36
+ @cached_nullary
37
+ def __str__(self) -> str:
38
+ return self.render(None)
39
+
36
40
  @abc.abstractmethod
37
41
  def render(self, tags: ta.Optional[DeployTagMap] = None) -> str:
38
42
  raise NotImplementedError
@@ -174,7 +178,7 @@ class FileDeployPathPart(DeployPathPart):
174
178
 
175
179
 
176
180
  @dc.dataclass(frozen=True)
177
- class DeployPath:
181
+ class DeployPath(DeployPathRenderable):
178
182
  parts: ta.Sequence[DeployPathPart]
179
183
 
180
184
  @property
@@ -91,6 +91,22 @@ class DeployAppSpec(DeploySpecKeyed[DeployAppKey]):
91
91
  return DeployAppKey(self._key_str())
92
92
 
93
93
 
94
+ @dc.dataclass(frozen=True)
95
+ class DeployAppLinksSpec:
96
+ apps: ta.Sequence[DeployApp] = ()
97
+
98
+ exclude_unspecified: bool = False
99
+
100
+
101
+ ##
102
+
103
+
104
+ @dc.dataclass(frozen=True)
105
+ class DeploySystemdSpec:
106
+ # ~/.config/systemd/user/
107
+ unit_dir: ta.Optional[str] = None
108
+
109
+
94
110
  ##
95
111
 
96
112
 
@@ -98,7 +114,11 @@ class DeployAppSpec(DeploySpecKeyed[DeployAppKey]):
98
114
  class DeploySpec(DeploySpecKeyed[DeployKey]):
99
115
  home: DeployHome
100
116
 
101
- apps: ta.Sequence[DeployAppSpec]
117
+ apps: ta.Sequence[DeployAppSpec] = ()
118
+
119
+ app_links: DeployAppLinksSpec = DeployAppLinksSpec()
120
+
121
+ systemd: ta.Optional[DeploySystemdSpec] = None
102
122
 
103
123
  def __post_init__(self) -> None:
104
124
  check.non_empty_str(self.home)
@@ -0,0 +1,131 @@
1
+ # ruff: noqa: UP006 UP007
2
+ """
3
+ TODO:
4
+ - verify - systemd-analyze
5
+ - sudo loginctl enable-linger "$USER"
6
+ - idemp kill services that shouldn't be running, start ones that should
7
+ - ideally only those defined by links to deploy home
8
+ - ominfra.systemd / x.sd_orphans
9
+ """
10
+ import os.path
11
+ import sys
12
+ import typing as ta
13
+
14
+ from omlish.asyncs.asyncio.subprocesses import asyncio_subprocesses
15
+ from omlish.lite.check import check
16
+ from omlish.os.paths import abs_real_path
17
+ from omlish.os.paths import is_path_in_dir
18
+
19
+ from .specs import DeploySystemdSpec
20
+ from .tmp import DeployHomeAtomics
21
+ from .types import DeployHome
22
+
23
+
24
+ class DeploySystemdManager:
25
+ def __init__(
26
+ self,
27
+ *,
28
+ atomics: DeployHomeAtomics,
29
+ ) -> None:
30
+ super().__init__()
31
+
32
+ self._atomics = atomics
33
+
34
+ def _scan_link_dir(
35
+ self,
36
+ d: str,
37
+ *,
38
+ strict: bool = False,
39
+ ) -> ta.Dict[str, str]:
40
+ o: ta.Dict[str, str] = {}
41
+ for f in os.listdir(d):
42
+ fp = os.path.join(d, f)
43
+ if strict:
44
+ check.state(os.path.islink(fp))
45
+ o[f] = abs_real_path(fp)
46
+ return o
47
+
48
+ async def sync_systemd(
49
+ self,
50
+ spec: ta.Optional[DeploySystemdSpec],
51
+ home: DeployHome,
52
+ conf_dir: str,
53
+ ) -> None:
54
+ check.non_empty_str(home)
55
+
56
+ if not spec:
57
+ return
58
+
59
+ #
60
+
61
+ if not (ud := spec.unit_dir):
62
+ return
63
+
64
+ ud = abs_real_path(os.path.expanduser(ud))
65
+
66
+ os.makedirs(ud, exist_ok=True)
67
+
68
+ #
69
+
70
+ uld = {
71
+ n: p
72
+ for n, p in self._scan_link_dir(ud).items()
73
+ if is_path_in_dir(home, p)
74
+ }
75
+
76
+ if os.path.exists(conf_dir):
77
+ cld = self._scan_link_dir(conf_dir, strict=True)
78
+ else:
79
+ cld = {}
80
+
81
+ #
82
+
83
+ ns = sorted(set(uld) | set(cld))
84
+
85
+ for n in ns:
86
+ cl = cld.get(n)
87
+ if cl is None:
88
+ os.unlink(os.path.join(ud, n))
89
+ else:
90
+ with self._atomics(home).begin_atomic_path_swap( # noqa
91
+ 'file',
92
+ os.path.join(ud, n),
93
+ auto_commit=True,
94
+ skip_root_dir_check=True,
95
+ ) as dst_swap:
96
+ os.unlink(dst_swap.tmp_path)
97
+ os.symlink(
98
+ os.path.relpath(cl, os.path.dirname(dst_swap.dst_path)),
99
+ dst_swap.tmp_path,
100
+ )
101
+
102
+ #
103
+
104
+ if sys.platform == 'linux':
105
+ async def reload() -> None:
106
+ await asyncio_subprocesses.check_call('systemctl', '--user', 'daemon-reload')
107
+
108
+ await reload()
109
+
110
+ num_deleted = 0
111
+ for n in ns:
112
+ if n.endswith('.service'):
113
+ cl = cld.get(n)
114
+ ul = uld.get(n)
115
+ if cl is not None:
116
+ if ul is None:
117
+ cs = ['enable', 'start']
118
+ else:
119
+ cs = ['restart']
120
+ else: # noqa
121
+ if ul is not None:
122
+ cs = ['stop']
123
+ num_deleted += 1
124
+ else:
125
+ cs = []
126
+
127
+ for c in cs:
128
+ await asyncio_subprocesses.check_call('systemctl', '--user', c, n)
129
+
130
+ if num_deleted:
131
+ await reload()
@@ -29,7 +29,7 @@ DEPLOY_TAG_ILLEGAL_STRS: ta.AbstractSet[str] = frozenset([
29
29
  ##
30
30
 
31
31
 
32
- @dc.dataclass(frozen=True)
32
+ @dc.dataclass(frozen=True, order=True)
33
33
  class DeployTag(abc.ABC): # noqa
34
34
  s: str
35
35
 
@@ -5,6 +5,7 @@ TODO:
5
5
  - share more code with pyproject?
6
6
  """
7
7
  import os.path
8
+ import shutil
8
9
 
9
10
  from omdev.interp.default import get_default_interp_resolver
10
11
  from omdev.interp.types import InterpSpecifier
@@ -12,14 +13,12 @@ from omlish.asyncs.asyncio.subprocesses import asyncio_subprocesses
12
13
  from omlish.lite.check import check
13
14
 
14
15
  from .specs import DeployVenvSpec
15
- from .types import DeployHome
16
16
 
17
17
 
18
18
  class DeployVenvManager:
19
19
  async def setup_venv(
20
20
  self,
21
21
  spec: DeployVenvSpec,
22
- home: DeployHome,
23
22
  git_dir: str,
24
23
  venv_dir: str,
25
24
  ) -> None:
@@ -46,9 +45,12 @@ class DeployVenvManager:
46
45
 
47
46
  if os.path.isfile(reqs_txt):
48
47
  if spec.use_uv:
49
- await asyncio_subprocesses.check_call(venv_exe, '-m', 'pip', 'install', 'uv')
50
- pip_cmd = ['-m', 'uv', 'pip']
48
+ if shutil.which('uv') is not None:
49
+ pip_cmd = ['uv', 'pip']
50
+ else:
51
+ await asyncio_subprocesses.check_call(venv_exe, '-m', 'pip', 'install', 'uv')
52
+ pip_cmd = [venv_exe, '-m', 'uv', 'pip']
51
53
  else:
52
- pip_cmd = ['-m', 'pip']
54
+ pip_cmd = [venv_exe, '-m', 'pip']
53
55
 
54
- await asyncio_subprocesses.check_call(venv_exe, *pip_cmd,'install', '-r', reqs_txt)
56
+ await asyncio_subprocesses.check_call(*pip_cmd, 'install', '-r', reqs_txt, cwd=venv_dir)