ominfra 0.0.0.dev158__tar.gz → 0.0.0.dev160__tar.gz

Sign up to get free protection for your applications and to get access to all the features.
Files changed (135) hide show
  1. {ominfra-0.0.0.dev158/ominfra.egg-info → ominfra-0.0.0.dev160}/PKG-INFO +3 -3
  2. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/manage/deploy/apps.py +13 -11
  3. ominfra-0.0.0.dev160/ominfra/manage/deploy/atomics.py +207 -0
  4. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/manage/deploy/commands.py +10 -1
  5. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/manage/deploy/git.py +32 -29
  6. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/manage/deploy/inject.py +11 -0
  7. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/manage/deploy/paths.py +46 -37
  8. ominfra-0.0.0.dev160/ominfra/manage/deploy/specs.py +56 -0
  9. ominfra-0.0.0.dev160/ominfra/manage/deploy/tmp.py +46 -0
  10. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/manage/deploy/types.py +1 -0
  11. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/manage/deploy/venvs.py +6 -1
  12. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/scripts/journald2aws.py +32 -18
  13. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/scripts/manage.py +417 -109
  14. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/scripts/supervisor.py +32 -18
  15. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160/ominfra.egg-info}/PKG-INFO +3 -3
  16. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra.egg-info/SOURCES.txt +2 -0
  17. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra.egg-info/requires.txt +2 -2
  18. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/pyproject.toml +3 -3
  19. ominfra-0.0.0.dev158/ominfra/manage/deploy/specs.py +0 -32
  20. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/LICENSE +0 -0
  21. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/MANIFEST.in +0 -0
  22. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/README.rst +0 -0
  23. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/.manifests.json +0 -0
  24. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/__about__.py +0 -0
  25. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/__init__.py +0 -0
  26. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/clouds/__init__.py +0 -0
  27. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/clouds/aws/__init__.py +0 -0
  28. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/clouds/aws/__main__.py +0 -0
  29. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/clouds/aws/auth.py +0 -0
  30. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/clouds/aws/cli.py +0 -0
  31. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/clouds/aws/dataclasses.py +0 -0
  32. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/clouds/aws/journald2aws/__init__.py +0 -0
  33. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/clouds/aws/journald2aws/__main__.py +0 -0
  34. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/clouds/aws/journald2aws/cursor.py +0 -0
  35. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/clouds/aws/journald2aws/driver.py +0 -0
  36. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/clouds/aws/journald2aws/main.py +0 -0
  37. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/clouds/aws/journald2aws/poster.py +0 -0
  38. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/clouds/aws/logs.py +0 -0
  39. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/clouds/aws/metadata.py +0 -0
  40. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/clouds/gcp/__init__.py +0 -0
  41. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/clouds/gcp/auth.py +0 -0
  42. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/cmds.py +0 -0
  43. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/configs.py +0 -0
  44. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/journald/__init__.py +0 -0
  45. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/journald/fields.py +0 -0
  46. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/journald/genmessages.py +0 -0
  47. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/journald/messages.py +0 -0
  48. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/journald/tailer.py +0 -0
  49. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/manage/__init__.py +0 -0
  50. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/manage/__main__.py +0 -0
  51. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/manage/bootstrap.py +0 -0
  52. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/manage/bootstrap_.py +0 -0
  53. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/manage/commands/__init__.py +0 -0
  54. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/manage/commands/base.py +0 -0
  55. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/manage/commands/inject.py +0 -0
  56. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/manage/commands/local.py +0 -0
  57. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/manage/commands/marshal.py +0 -0
  58. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/manage/commands/ping.py +0 -0
  59. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/manage/commands/subprocess.py +0 -0
  60. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/manage/commands/types.py +0 -0
  61. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/manage/config.py +0 -0
  62. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/manage/deploy/__init__.py +0 -0
  63. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/manage/deploy/config.py +0 -0
  64. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/manage/deploy/interp.py +0 -0
  65. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/manage/inject.py +0 -0
  66. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/manage/main.py +0 -0
  67. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/manage/marshal.py +0 -0
  68. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/manage/remote/__init__.py +0 -0
  69. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/manage/remote/_main.py +0 -0
  70. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/manage/remote/channel.py +0 -0
  71. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/manage/remote/config.py +0 -0
  72. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/manage/remote/connection.py +0 -0
  73. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/manage/remote/execution.py +0 -0
  74. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/manage/remote/inject.py +0 -0
  75. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/manage/remote/payload.py +0 -0
  76. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/manage/remote/spawning.py +0 -0
  77. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/manage/system/__init__.py +0 -0
  78. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/manage/system/commands.py +0 -0
  79. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/manage/system/config.py +0 -0
  80. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/manage/system/inject.py +0 -0
  81. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/manage/system/packages.py +0 -0
  82. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/manage/system/platforms.py +0 -0
  83. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/manage/targets/__init__.py +0 -0
  84. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/manage/targets/connection.py +0 -0
  85. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/manage/targets/inject.py +0 -0
  86. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/manage/targets/targets.py +0 -0
  87. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/pyremote.py +0 -0
  88. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/scripts/__init__.py +0 -0
  89. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/ssh.py +0 -0
  90. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/supervisor/LICENSE.txt +0 -0
  91. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/supervisor/__init__.py +0 -0
  92. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/supervisor/__main__.py +0 -0
  93. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/supervisor/configs.py +0 -0
  94. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/supervisor/dispatchers.py +0 -0
  95. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/supervisor/dispatchersimpl.py +0 -0
  96. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/supervisor/events.py +0 -0
  97. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/supervisor/exceptions.py +0 -0
  98. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/supervisor/groups.py +0 -0
  99. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/supervisor/groupsimpl.py +0 -0
  100. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/supervisor/http.py +0 -0
  101. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/supervisor/inject.py +0 -0
  102. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/supervisor/io.py +0 -0
  103. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/supervisor/main.py +0 -0
  104. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/supervisor/pipes.py +0 -0
  105. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/supervisor/privileges.py +0 -0
  106. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/supervisor/process.py +0 -0
  107. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/supervisor/processimpl.py +0 -0
  108. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/supervisor/setup.py +0 -0
  109. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/supervisor/setupimpl.py +0 -0
  110. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/supervisor/signals.py +0 -0
  111. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/supervisor/spawning.py +0 -0
  112. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/supervisor/spawningimpl.py +0 -0
  113. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/supervisor/states.py +0 -0
  114. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/supervisor/supervisor.py +0 -0
  115. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/supervisor/types.py +0 -0
  116. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/supervisor/utils/__init__.py +0 -0
  117. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/supervisor/utils/collections.py +0 -0
  118. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/supervisor/utils/diag.py +0 -0
  119. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/supervisor/utils/fds.py +0 -0
  120. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/supervisor/utils/fs.py +0 -0
  121. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/supervisor/utils/os.py +0 -0
  122. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/supervisor/utils/ostypes.py +0 -0
  123. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/supervisor/utils/signals.py +0 -0
  124. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/supervisor/utils/strings.py +0 -0
  125. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/supervisor/utils/users.py +0 -0
  126. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/tailscale/__init__.py +0 -0
  127. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/tailscale/api.py +0 -0
  128. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/tailscale/cli.py +0 -0
  129. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/threadworkers.py +0 -0
  130. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/tools/__init__.py +0 -0
  131. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra/tools/listresources.py +0 -0
  132. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra.egg-info/dependency_links.txt +0 -0
  133. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra.egg-info/entry_points.txt +0 -0
  134. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/ominfra.egg-info/top_level.txt +0 -0
  135. {ominfra-0.0.0.dev158 → ominfra-0.0.0.dev160}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ominfra
3
- Version: 0.0.0.dev158
3
+ Version: 0.0.0.dev160
4
4
  Summary: ominfra
5
5
  Author: wrmsr
6
6
  License: BSD-3-Clause
@@ -12,8 +12,8 @@ Classifier: Operating System :: OS Independent
12
12
  Classifier: Operating System :: POSIX
13
13
  Requires-Python: >=3.12
14
14
  License-File: LICENSE
15
- Requires-Dist: omdev==0.0.0.dev158
16
- Requires-Dist: omlish==0.0.0.dev158
15
+ Requires-Dist: omdev==0.0.0.dev160
16
+ Requires-Dist: omlish==0.0.0.dev160
17
17
  Provides-Extra: all
18
18
  Requires-Dist: paramiko~=3.5; extra == "all"
19
19
  Requires-Dist: asyncssh~=2.18; extra == "all"
@@ -12,6 +12,7 @@ from .paths import DeployPathOwner
12
12
  from .specs import DeploySpec
13
13
  from .types import DeployAppTag
14
14
  from .types import DeployHome
15
+ from .types import DeployKey
15
16
  from .types import DeployRev
16
17
  from .types import DeployTag
17
18
  from .venvs import DeployVenvManager
@@ -19,13 +20,15 @@ from .venvs import DeployVenvManager
19
20
 
20
21
  def make_deploy_tag(
21
22
  rev: DeployRev,
22
- now: ta.Optional[datetime.datetime] = None,
23
+ key: DeployKey,
24
+ *,
25
+ utcnow: ta.Optional[datetime.datetime] = None,
23
26
  ) -> DeployTag:
24
- if now is None:
25
- now = datetime.datetime.utcnow() # noqa
26
- now_fmt = '%Y%m%dT%H%M%S'
27
- now_str = now.strftime(now_fmt)
28
- return DeployTag('-'.join([now_str, rev]))
27
+ if utcnow is None:
28
+ utcnow = datetime.datetime.now(tz=datetime.timezone.utc) # noqa
29
+ now_fmt = '%Y%m%dT%H%M%SZ'
30
+ now_str = utcnow.strftime(now_fmt)
31
+ return DeployTag('-'.join([now_str, rev, key]))
29
32
 
30
33
 
31
34
  class DeployAppManager(DeployPathOwner):
@@ -46,7 +49,7 @@ class DeployAppManager(DeployPathOwner):
46
49
  def _dir(self) -> str:
47
50
  return os.path.join(check.non_empty_str(self._deploy_home), 'apps')
48
51
 
49
- def get_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
52
+ def get_owned_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
50
53
  return {
51
54
  DeployPath.parse('apps/@app/@tag'),
52
55
  }
@@ -54,15 +57,14 @@ class DeployAppManager(DeployPathOwner):
54
57
  async def prepare_app(
55
58
  self,
56
59
  spec: DeploySpec,
57
- ):
58
- app_tag = DeployAppTag(spec.app, make_deploy_tag(spec.rev))
60
+ ) -> None:
61
+ app_tag = DeployAppTag(spec.app, make_deploy_tag(spec.checkout.rev, spec.key()))
59
62
  app_dir = os.path.join(self._dir(), spec.app, app_tag.tag)
60
63
 
61
64
  #
62
65
 
63
66
  await self._git.checkout(
64
- spec.repo,
65
- spec.rev,
67
+ spec.checkout,
66
68
  app_dir,
67
69
  )
68
70
 
@@ -0,0 +1,207 @@
1
+ # ruff: noqa: UP006 UP007
2
+ import abc
3
+ import os
4
+ import shutil
5
+ import tempfile
6
+ import typing as ta
7
+
8
+ from omlish.lite.check import check
9
+ from omlish.lite.strings import attr_repr
10
+
11
+
12
+ DeployAtomicPathSwapKind = ta.Literal['dir', 'file']
13
+ DeployAtomicPathSwapState = ta.Literal['open', 'committed', 'aborted'] # ta.TypeAlias
14
+
15
+
16
+ ##
17
+
18
+
19
+ class DeployAtomicPathSwap(abc.ABC):
20
+ def __init__(
21
+ self,
22
+ kind: DeployAtomicPathSwapKind,
23
+ dst_path: str,
24
+ *,
25
+ auto_commit: bool = False,
26
+ ) -> None:
27
+ super().__init__()
28
+
29
+ self._kind = kind
30
+ self._dst_path = dst_path
31
+ self._auto_commit = auto_commit
32
+
33
+ self._state: DeployAtomicPathSwapState = 'open'
34
+
35
+ def __repr__(self) -> str:
36
+ return attr_repr(self, 'kind', 'dst_path', 'tmp_path')
37
+
38
+ @property
39
+ def kind(self) -> DeployAtomicPathSwapKind:
40
+ return self._kind
41
+
42
+ @property
43
+ def dst_path(self) -> str:
44
+ return self._dst_path
45
+
46
+ @property
47
+ @abc.abstractmethod
48
+ def tmp_path(self) -> str:
49
+ raise NotImplementedError
50
+
51
+ #
52
+
53
+ @property
54
+ def state(self) -> DeployAtomicPathSwapState:
55
+ return self._state
56
+
57
+ def _check_state(self, *states: DeployAtomicPathSwapState) -> None:
58
+ if self._state not in states:
59
+ raise RuntimeError(f'Atomic path swap not in correct state: {self._state}, {states}')
60
+
61
+ #
62
+
63
+ @abc.abstractmethod
64
+ def _commit(self) -> None:
65
+ raise NotImplementedError
66
+
67
+ def commit(self) -> None:
68
+ if self._state == 'committed':
69
+ return
70
+ self._check_state('open')
71
+ try:
72
+ self._commit()
73
+ except Exception: # noqa
74
+ self._abort()
75
+ raise
76
+ else:
77
+ self._state = 'committed'
78
+
79
+ #
80
+
81
+ @abc.abstractmethod
82
+ def _abort(self) -> None:
83
+ raise NotImplementedError
84
+
85
+ def abort(self) -> None:
86
+ if self._state == 'aborted':
87
+ return
88
+ self._abort()
89
+ self._state = 'aborted'
90
+
91
+ #
92
+
93
+ def __enter__(self) -> 'DeployAtomicPathSwap':
94
+ return self
95
+
96
+ def __exit__(self, exc_type, exc_val, exc_tb):
97
+ if (
98
+ exc_type is None and
99
+ self._auto_commit and
100
+ self._state == 'open'
101
+ ):
102
+ self.commit()
103
+ else:
104
+ self.abort()
105
+
106
+
107
+ #
108
+
109
+
110
+ class DeployAtomicPathSwapping(abc.ABC):
111
+ @abc.abstractmethod
112
+ def begin_atomic_path_swap(
113
+ self,
114
+ kind: DeployAtomicPathSwapKind,
115
+ dst_path: str,
116
+ *,
117
+ name_hint: ta.Optional[str] = None,
118
+ make_dirs: bool = False,
119
+ **kwargs: ta.Any,
120
+ ) -> DeployAtomicPathSwap:
121
+ raise NotImplementedError
122
+
123
+
124
+ ##
125
+
126
+
127
+ class OsRenameDeployAtomicPathSwap(DeployAtomicPathSwap):
128
+ def __init__(
129
+ self,
130
+ kind: DeployAtomicPathSwapKind,
131
+ dst_path: str,
132
+ tmp_path: str,
133
+ **kwargs: ta.Any,
134
+ ) -> None:
135
+ if kind == 'dir':
136
+ check.state(os.path.isdir(tmp_path))
137
+ elif kind == 'file':
138
+ check.state(os.path.isfile(tmp_path))
139
+ else:
140
+ raise TypeError(kind)
141
+
142
+ super().__init__(
143
+ kind,
144
+ dst_path,
145
+ **kwargs,
146
+ )
147
+
148
+ self._tmp_path = tmp_path
149
+
150
+ @property
151
+ def tmp_path(self) -> str:
152
+ return self._tmp_path
153
+
154
+ def _commit(self) -> None:
155
+ os.rename(self._tmp_path, self._dst_path)
156
+
157
+ def _abort(self) -> None:
158
+ shutil.rmtree(self._tmp_path, ignore_errors=True)
159
+
160
+
161
+ class TempDirDeployAtomicPathSwapping(DeployAtomicPathSwapping):
162
+ def __init__(
163
+ self,
164
+ *,
165
+ temp_dir: ta.Optional[str] = None,
166
+ root_dir: ta.Optional[str] = None,
167
+ ) -> None:
168
+ super().__init__()
169
+
170
+ if root_dir is not None:
171
+ root_dir = os.path.abspath(root_dir)
172
+ self._root_dir = root_dir
173
+ self._temp_dir = temp_dir
174
+
175
+ def begin_atomic_path_swap(
176
+ self,
177
+ kind: DeployAtomicPathSwapKind,
178
+ dst_path: str,
179
+ *,
180
+ name_hint: ta.Optional[str] = None,
181
+ make_dirs: bool = False,
182
+ **kwargs: ta.Any,
183
+ ) -> DeployAtomicPathSwap:
184
+ dst_path = os.path.abspath(dst_path)
185
+ if self._root_dir is not None and not dst_path.startswith(check.non_empty_str(self._root_dir)):
186
+ raise RuntimeError(f'Atomic path swap dst must be in root dir: {dst_path}, {self._root_dir}')
187
+
188
+ dst_dir = os.path.dirname(dst_path)
189
+ if make_dirs:
190
+ os.makedirs(dst_dir, exist_ok=True)
191
+ if not os.path.isdir(dst_dir):
192
+ raise RuntimeError(f'Atomic path swap dst dir does not exist: {dst_dir}')
193
+
194
+ if kind == 'dir':
195
+ tmp_path = tempfile.mkdtemp(prefix=name_hint, dir=self._temp_dir)
196
+ elif kind == 'file':
197
+ fd, tmp_path = tempfile.mkstemp(prefix=name_hint, dir=self._temp_dir)
198
+ os.close(fd)
199
+ else:
200
+ raise TypeError(kind)
201
+
202
+ return OsRenameDeployAtomicPathSwap(
203
+ kind,
204
+ dst_path,
205
+ tmp_path,
206
+ **kwargs,
207
+ )
@@ -5,6 +5,8 @@ from omlish.lite.logs import log
5
5
 
6
6
  from ..commands.base import Command
7
7
  from ..commands.base import CommandExecutor
8
+ from .apps import DeployAppManager
9
+ from .specs import DeploySpec
8
10
 
9
11
 
10
12
  ##
@@ -12,13 +14,20 @@ from ..commands.base import CommandExecutor
12
14
 
13
15
  @dc.dataclass(frozen=True)
14
16
  class DeployCommand(Command['DeployCommand.Output']):
17
+ spec: DeploySpec
18
+
15
19
  @dc.dataclass(frozen=True)
16
20
  class Output(Command.Output):
17
21
  pass
18
22
 
19
23
 
24
+ @dc.dataclass(frozen=True)
20
25
  class DeployCommandExecutor(CommandExecutor[DeployCommand, DeployCommand.Output]):
26
+ _apps: DeployAppManager
27
+
21
28
  async def execute(self, cmd: DeployCommand) -> DeployCommand.Output:
22
- log.info('Deploying!')
29
+ log.info('Deploying! %r', cmd.spec)
30
+
31
+ await self._apps.prepare_app(cmd.spec)
23
32
 
24
33
  return DeployCommand.Output()
@@ -14,11 +14,11 @@ import typing as ta
14
14
 
15
15
  from omlish.asyncs.asyncio.subprocesses import asyncio_subprocesses
16
16
  from omlish.lite.cached import async_cached_nullary
17
- from omlish.lite.cached import cached_nullary
18
17
  from omlish.lite.check import check
19
18
 
20
- from .paths import DeployPath
21
- from .paths import DeployPathOwner
19
+ from .atomics import DeployAtomicPathSwapping
20
+ from .paths import SingleDirDeployPathOwner
21
+ from .specs import DeployGitCheckout
22
22
  from .specs import DeployGitRepo
23
23
  from .types import DeployHome
24
24
  from .types import DeployRev
@@ -27,27 +27,22 @@ from .types import DeployRev
27
27
  ##
28
28
 
29
29
 
30
- class DeployGitManager(DeployPathOwner):
30
+ class DeployGitManager(SingleDirDeployPathOwner):
31
31
  def __init__(
32
32
  self,
33
33
  *,
34
34
  deploy_home: ta.Optional[DeployHome] = None,
35
+ atomics: DeployAtomicPathSwapping,
35
36
  ) -> None:
36
- super().__init__()
37
+ super().__init__(
38
+ owned_dir='git',
39
+ deploy_home=deploy_home,
40
+ )
37
41
 
38
- self._deploy_home = deploy_home
42
+ self._atomics = atomics
39
43
 
40
44
  self._repo_dirs: ta.Dict[DeployGitRepo, DeployGitManager.RepoDir] = {}
41
45
 
42
- @cached_nullary
43
- def _dir(self) -> str:
44
- return os.path.join(check.non_empty_str(self._deploy_home), 'git')
45
-
46
- def get_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
47
- return {
48
- DeployPath.parse('git'),
49
- }
50
-
51
46
  class RepoDir:
52
47
  def __init__(
53
48
  self,
@@ -59,7 +54,7 @@ class DeployGitManager(DeployPathOwner):
59
54
  self._git = git
60
55
  self._repo = repo
61
56
  self._dir = os.path.join(
62
- self._git._dir(), # noqa
57
+ self._git._make_dir(), # noqa
63
58
  check.non_empty_str(repo.host),
64
59
  check.non_empty_str(repo.path),
65
60
  )
@@ -75,12 +70,16 @@ class DeployGitManager(DeployPathOwner):
75
70
  else:
76
71
  return f'https://{self._repo.host}/{self._repo.path}'
77
72
 
73
+ #
74
+
78
75
  async def _call(self, *cmd: str) -> None:
79
76
  await asyncio_subprocesses.check_call(
80
77
  *cmd,
81
78
  cwd=self._dir,
82
79
  )
83
80
 
81
+ #
82
+
84
83
  @async_cached_nullary
85
84
  async def init(self) -> None:
86
85
  os.makedirs(self._dir, exist_ok=True)
@@ -94,20 +93,24 @@ class DeployGitManager(DeployPathOwner):
94
93
  await self.init()
95
94
  await self._call('git', 'fetch', '--depth=1', 'origin', rev)
96
95
 
97
- async def checkout(self, rev: DeployRev, dst_dir: str) -> None:
98
- check.state(not os.path.exists(dst_dir))
99
-
100
- await self.fetch(rev)
96
+ #
101
97
 
102
- # FIXME: temp dir swap
103
- os.makedirs(dst_dir)
98
+ async def checkout(self, checkout: DeployGitCheckout, dst_dir: str) -> None:
99
+ check.state(not os.path.exists(dst_dir))
100
+ with self._git._atomics.begin_atomic_path_swap( # noqa
101
+ 'dir',
102
+ dst_dir,
103
+ auto_commit=True,
104
+ make_dirs=True,
105
+ ) as dst_swap:
106
+ await self.fetch(checkout.rev)
104
107
 
105
- dst_call = functools.partial(asyncio_subprocesses.check_call, cwd=dst_dir)
106
- await dst_call('git', 'init')
108
+ dst_call = functools.partial(asyncio_subprocesses.check_call, cwd=dst_swap.tmp_path)
109
+ await dst_call('git', 'init')
107
110
 
108
- await dst_call('git', 'remote', 'add', 'local', self._dir)
109
- await dst_call('git', 'fetch', '--depth=1', 'local', rev)
110
- await dst_call('git', 'checkout', rev)
111
+ await dst_call('git', 'remote', 'add', 'local', self._dir)
112
+ await dst_call('git', 'fetch', '--depth=1', 'local', checkout.rev)
113
+ await dst_call('git', 'checkout', checkout.rev, *(checkout.subtrees or []))
111
114
 
112
115
  def get_repo_dir(self, repo: DeployGitRepo) -> RepoDir:
113
116
  try:
@@ -116,5 +119,5 @@ class DeployGitManager(DeployPathOwner):
116
119
  repo_dir = self._repo_dirs[repo] = DeployGitManager.RepoDir(self, repo)
117
120
  return repo_dir
118
121
 
119
- async def checkout(self, repo: DeployGitRepo, rev: DeployRev, dst_dir: str) -> None:
120
- await self.get_repo_dir(repo).checkout(rev, dst_dir)
122
+ async def checkout(self, checkout: DeployGitCheckout, dst_dir: str) -> None:
123
+ await self.get_repo_dir(checkout.repo).checkout(checkout, dst_dir)
@@ -8,12 +8,14 @@ from omlish.lite.inject import inj
8
8
 
9
9
  from ..commands.inject import bind_command
10
10
  from .apps import DeployAppManager
11
+ from .atomics import DeployAtomicPathSwapping
11
12
  from .commands import DeployCommand
12
13
  from .commands import DeployCommandExecutor
13
14
  from .config import DeployConfig
14
15
  from .git import DeployGitManager
15
16
  from .interp import InterpCommand
16
17
  from .interp import InterpCommandExecutor
18
+ from .tmp import DeployTmpManager
17
19
  from .types import DeployHome
18
20
  from .venvs import DeployVenvManager
19
21
 
@@ -25,10 +27,19 @@ def bind_deploy(
25
27
  lst: ta.List[InjectorBindingOrBindings] = [
26
28
  inj.bind(deploy_config),
27
29
 
30
+ #
31
+
28
32
  inj.bind(DeployAppManager, singleton=True),
33
+
29
34
  inj.bind(DeployGitManager, singleton=True),
35
+
36
+ inj.bind(DeployTmpManager, singleton=True),
37
+ inj.bind(DeployAtomicPathSwapping, to_key=DeployTmpManager),
38
+
30
39
  inj.bind(DeployVenvManager, singleton=True),
31
40
 
41
+ #
42
+
32
43
  bind_command(DeployCommand, DeployCommandExecutor),
33
44
  bind_command(InterpCommand, InterpCommandExecutor),
34
45
  ]
@@ -1,46 +1,22 @@
1
1
  # ruff: noqa: UP006 UP007
2
2
  """
3
- ~deploy
4
- deploy.pid (flock)
5
- /app
6
- /<appplaceholder> - shallow clone
7
- /conf
8
- /env
9
- <appplaceholder>.env
10
- /nginx
11
- <appplaceholder>.conf
12
- /supervisor
13
- <appplaceholder>.conf
14
- /venv
15
- /<appplaceholder>
16
-
17
- ?
18
- /logs
19
- /wrmsr--omlish--<placeholder>
20
-
21
- placeholder = <name>--<rev>--<when>
22
-
23
- ==
24
-
25
- for dn in [
26
- 'app',
27
- 'conf',
28
- 'conf/env',
29
- 'conf/nginx',
30
- 'conf/supervisor',
31
- 'venv',
32
- ]:
33
-
34
- ==
35
-
3
+ TODO:
4
+ - run/pidfile
5
+ - logs/...
6
+ - current symlink
7
+ - conf/{nginx,supervisor}
8
+ - env/?
36
9
  """
37
10
  import abc
38
11
  import dataclasses as dc
39
12
  import os.path
40
13
  import typing as ta
41
14
 
15
+ from omlish.lite.cached import cached_nullary
42
16
  from omlish.lite.check import check
43
17
 
18
+ from .types import DeployHome
19
+
44
20
 
45
21
  DeployPathKind = ta.Literal['dir', 'file'] # ta.TypeAlias
46
22
  DeployPathPlaceholder = ta.Literal['app', 'tag'] # ta.TypeAlias
@@ -54,7 +30,7 @@ DEPLOY_PATH_PLACEHOLDER_SEPARATORS = '-.'
54
30
 
55
31
  DEPLOY_PATH_PLACEHOLDERS: ta.FrozenSet[str] = frozenset([
56
32
  'app',
57
- 'tag', # <rev>-<dt>
33
+ 'tag',
58
34
  ])
59
35
 
60
36
 
@@ -181,6 +157,8 @@ class DeployPath:
181
157
  parts: ta.Sequence[DeployPathPart]
182
158
 
183
159
  def __post_init__(self) -> None:
160
+ hash(self)
161
+
184
162
  check.not_empty(self.parts)
185
163
  for p in self.parts[:-1]:
186
164
  check.equal(p.kind, 'dir')
@@ -215,10 +193,10 @@ class DeployPath:
215
193
  else:
216
194
  tail_parse = FileDeployPathPart.parse
217
195
  ps = check.non_empty_str(s).split('/')
218
- return cls([
196
+ return cls((
219
197
  *([DirDeployPathPart.parse(p) for p in ps[:-1]] if len(ps) > 1 else []),
220
198
  tail_parse(ps[-1]),
221
- ])
199
+ ))
222
200
 
223
201
 
224
202
  ##
@@ -226,5 +204,36 @@ class DeployPath:
226
204
 
227
205
  class DeployPathOwner(abc.ABC):
228
206
  @abc.abstractmethod
229
- def get_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
207
+ def get_owned_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
230
208
  raise NotImplementedError
209
+
210
+
211
+ class SingleDirDeployPathOwner(DeployPathOwner, abc.ABC):
212
+ def __init__(
213
+ self,
214
+ *args: ta.Any,
215
+ owned_dir: str,
216
+ deploy_home: ta.Optional[DeployHome],
217
+ **kwargs: ta.Any,
218
+ ) -> None:
219
+ super().__init__(*args, **kwargs)
220
+
221
+ check.not_in('/', owned_dir)
222
+ self._owned_dir: str = check.non_empty_str(owned_dir)
223
+
224
+ self._deploy_home = deploy_home
225
+
226
+ self._owned_deploy_paths = frozenset([DeployPath.parse(self._owned_dir + '/')])
227
+
228
+ @cached_nullary
229
+ def _dir(self) -> str:
230
+ return os.path.join(check.non_empty_str(self._deploy_home), self._owned_dir)
231
+
232
+ @cached_nullary
233
+ def _make_dir(self) -> str:
234
+ if not os.path.isdir(d := self._dir()):
235
+ os.makedirs(d, exist_ok=True)
236
+ return d
237
+
238
+ def get_owned_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
239
+ return self._owned_deploy_paths
@@ -0,0 +1,56 @@
1
+ # ruff: noqa: UP006 UP007
2
+ import dataclasses as dc
3
+ import hashlib
4
+ import typing as ta
5
+
6
+ from omlish.lite.cached import cached_nullary
7
+ from omlish.lite.check import check
8
+
9
+ from .types import DeployApp
10
+ from .types import DeployKey
11
+ from .types import DeployRev
12
+
13
+
14
+ ##
15
+
16
+
17
+ @dc.dataclass(frozen=True)
18
+ class DeployGitRepo:
19
+ host: ta.Optional[str] = None
20
+ username: ta.Optional[str] = None
21
+ path: ta.Optional[str] = None
22
+
23
+ def __post_init__(self) -> None:
24
+ check.not_in('..', check.non_empty_str(self.host))
25
+ check.not_in('.', check.non_empty_str(self.path))
26
+
27
+
28
+ @dc.dataclass(frozen=True)
29
+ class DeployGitCheckout:
30
+ repo: DeployGitRepo
31
+ rev: DeployRev
32
+
33
+ subtrees: ta.Optional[ta.Sequence[str]] = None
34
+
35
+ def __post_init__(self) -> None:
36
+ hash(self)
37
+ check.non_empty_str(self.rev)
38
+ if self.subtrees is not None:
39
+ for st in self.subtrees:
40
+ check.non_empty_str(st)
41
+
42
+
43
+ ##
44
+
45
+
46
+ @dc.dataclass(frozen=True)
47
+ class DeploySpec:
48
+ app: DeployApp
49
+ checkout: DeployGitCheckout
50
+
51
+ def __post_init__(self) -> None:
52
+ hash(self)
53
+
54
+ @cached_nullary
55
+ def key(self) -> DeployKey:
56
+ return DeployKey(hashlib.sha256(repr(self).encode('utf-8')).hexdigest()[:8])
@@ -0,0 +1,46 @@
1
+ # ruff: noqa: UP006 UP007
2
+ import typing as ta
3
+
4
+ from omlish.lite.cached import cached_nullary
5
+ from omlish.lite.check import check
6
+
7
+ from .atomics import DeployAtomicPathSwap
8
+ from .atomics import DeployAtomicPathSwapKind
9
+ from .atomics import DeployAtomicPathSwapping
10
+ from .atomics import TempDirDeployAtomicPathSwapping
11
+ from .paths import SingleDirDeployPathOwner
12
+ from .types import DeployHome
13
+
14
+
15
+ class DeployTmpManager(
16
+ SingleDirDeployPathOwner,
17
+ DeployAtomicPathSwapping,
18
+ ):
19
+ def __init__(
20
+ self,
21
+ *,
22
+ deploy_home: ta.Optional[DeployHome] = None,
23
+ ) -> None:
24
+ super().__init__(
25
+ owned_dir='tmp',
26
+ deploy_home=deploy_home,
27
+ )
28
+
29
+ @cached_nullary
30
+ def _swapping(self) -> DeployAtomicPathSwapping:
31
+ return TempDirDeployAtomicPathSwapping(
32
+ temp_dir=self._make_dir(),
33
+ root_dir=check.non_empty_str(self._deploy_home),
34
+ )
35
+
36
+ def begin_atomic_path_swap(
37
+ self,
38
+ kind: DeployAtomicPathSwapKind,
39
+ dst_path: str,
40
+ **kwargs: ta.Any,
41
+ ) -> DeployAtomicPathSwap:
42
+ return self._swapping().begin_atomic_path_swap(
43
+ kind,
44
+ dst_path,
45
+ **kwargs,
46
+ )