ominfra 0.0.0.dev166__py3-none-any.whl → 0.0.0.dev168__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -5,10 +5,12 @@ import typing as ta
5
5
 
6
6
  from omlish.lite.cached import cached_nullary
7
7
  from omlish.lite.check import check
8
+ from omlish.os.paths import relative_symlink
8
9
 
10
+ from .conf import DeployConfManager
9
11
  from .git import DeployGitManager
10
- from .paths import DeployPath
11
- from .paths import DeployPathOwner
12
+ from .paths.owners import DeployPathOwner
13
+ from .paths.paths import DeployPath
12
14
  from .specs import DeploySpec
13
15
  from .types import DeployAppTag
14
16
  from .types import DeployHome
@@ -36,39 +38,138 @@ class DeployAppManager(DeployPathOwner):
36
38
  self,
37
39
  *,
38
40
  deploy_home: ta.Optional[DeployHome] = None,
41
+
42
+ conf: DeployConfManager,
39
43
  git: DeployGitManager,
40
44
  venvs: DeployVenvManager,
41
45
  ) -> None:
42
46
  super().__init__()
43
47
 
44
48
  self._deploy_home = deploy_home
49
+
50
+ self._conf = conf
45
51
  self._git = git
46
52
  self._venvs = venvs
47
53
 
48
- @cached_nullary
49
- def _dir(self) -> str:
50
- return os.path.join(check.non_empty_str(self._deploy_home), 'apps')
54
+ #
55
+
56
+ _APP_TAG_DIR_STR = 'tags/apps/@app/@tag/'
57
+ _APP_TAG_DIR = DeployPath.parse(_APP_TAG_DIR_STR)
58
+
59
+ _CONF_TAG_DIR_STR = 'tags/conf/@tag--@app/'
60
+ _CONF_TAG_DIR = DeployPath.parse(_CONF_TAG_DIR_STR)
51
61
 
62
+ _DEPLOY_DIR_STR = 'deploys/@tag--@app/'
63
+ _DEPLOY_DIR = DeployPath.parse(_DEPLOY_DIR_STR)
64
+
65
+ _APP_DEPLOY_LINK = DeployPath.parse(f'{_DEPLOY_DIR_STR}apps/@app')
66
+ _CONF_DEPLOY_LINK = DeployPath.parse(f'{_DEPLOY_DIR_STR}conf')
67
+
68
+ @cached_nullary
52
69
  def get_owned_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
53
70
  return {
54
- DeployPath.parse('apps/@app/@tag/'),
71
+ self._APP_TAG_DIR,
72
+
73
+ self._CONF_TAG_DIR,
74
+
75
+ self._DEPLOY_DIR,
76
+
77
+ self._APP_DEPLOY_LINK,
78
+ self._CONF_DEPLOY_LINK,
79
+
80
+ *[
81
+ DeployPath.parse(f'{self._APP_TAG_DIR_STR}{sfx}/')
82
+ for sfx in [
83
+ 'conf',
84
+ 'git',
85
+ 'venv',
86
+ ]
87
+ ],
55
88
  }
56
89
 
90
+ #
91
+
57
92
  async def prepare_app(
58
93
  self,
59
94
  spec: DeploySpec,
60
95
  ) -> None:
61
- app_tag = DeployAppTag(spec.app, make_deploy_tag(spec.checkout.rev, spec.key()))
62
- app_dir = os.path.join(self._dir(), spec.app, app_tag.tag)
96
+ app_tag = DeployAppTag(spec.app, make_deploy_tag(spec.git.rev, spec.key()))
63
97
 
64
98
  #
65
99
 
100
+ deploy_home = check.non_empty_str(self._deploy_home)
101
+
102
+ def build_path(pth: DeployPath) -> str:
103
+ return os.path.join(deploy_home, pth.render(app_tag.placeholders()))
104
+
105
+ app_tag_dir = build_path(self._APP_TAG_DIR)
106
+ conf_tag_dir = build_path(self._CONF_TAG_DIR)
107
+ deploy_dir = build_path(self._DEPLOY_DIR)
108
+ app_deploy_link = build_path(self._APP_DEPLOY_LINK)
109
+ conf_deploy_link_file = build_path(self._CONF_DEPLOY_LINK)
110
+
111
+ #
112
+
113
+ os.makedirs(deploy_dir)
114
+
115
+ deploying_link = os.path.join(deploy_home, 'deploys/deploying')
116
+ relative_symlink(
117
+ deploy_dir,
118
+ deploying_link,
119
+ target_is_directory=True,
120
+ make_dirs=True,
121
+ )
122
+
123
+ #
124
+
125
+ os.makedirs(app_tag_dir)
126
+ relative_symlink(
127
+ app_tag_dir,
128
+ app_deploy_link,
129
+ target_is_directory=True,
130
+ make_dirs=True,
131
+ )
132
+
133
+ #
134
+
135
+ os.makedirs(conf_tag_dir)
136
+ relative_symlink(
137
+ conf_tag_dir,
138
+ conf_deploy_link_file,
139
+ target_is_directory=True,
140
+ make_dirs=True,
141
+ )
142
+
143
+ #
144
+
145
+ git_dir = os.path.join(app_tag_dir, 'git')
66
146
  await self._git.checkout(
67
- spec.checkout,
68
- app_dir,
147
+ spec.git,
148
+ git_dir,
69
149
  )
70
150
 
71
151
  #
72
152
 
73
153
  if spec.venv is not None:
74
- await self._venvs.setup_app_venv(app_tag, spec.venv)
154
+ venv_dir = os.path.join(app_tag_dir, 'venv')
155
+ await self._venvs.setup_venv(
156
+ spec.venv,
157
+ git_dir,
158
+ venv_dir,
159
+ )
160
+
161
+ #
162
+
163
+ if spec.conf is not None:
164
+ conf_dir = os.path.join(app_tag_dir, 'conf')
165
+ await self._conf.write_conf(
166
+ spec.conf,
167
+ app_tag,
168
+ conf_dir,
169
+ conf_tag_dir,
170
+ )
171
+
172
+ #
173
+
174
+ current_link = os.path.join(deploy_home, 'deploys/current')
175
+ os.replace(deploying_link, current_link)
@@ -5,7 +5,7 @@ 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
8
+ from .deploy import DeployManager
9
9
  from .specs import DeploySpec
10
10
 
11
11
 
@@ -23,11 +23,11 @@ class DeployCommand(Command['DeployCommand.Output']):
23
23
 
24
24
  @dc.dataclass(frozen=True)
25
25
  class DeployCommandExecutor(CommandExecutor[DeployCommand, DeployCommand.Output]):
26
- _apps: DeployAppManager
26
+ _deploy: DeployManager
27
27
 
28
28
  async def execute(self, cmd: DeployCommand) -> DeployCommand.Output:
29
29
  log.info('Deploying! %r', cmd.spec)
30
30
 
31
- await self._apps.prepare_app(cmd.spec)
31
+ await self._deploy.run_deploy(cmd.spec)
32
32
 
33
33
  return DeployCommand.Output()
@@ -0,0 +1,183 @@
1
+ # ruff: noqa: UP006 UP007
2
+ """
3
+ TODO:
4
+ - @conf DeployPathPlaceholder? :|
5
+ - post-deploy: remove any dir_links not present in new spec
6
+ - * only if succeeded * - otherwise, remove any dir_links present in new spec but not previously present?
7
+ - no such thing as 'previously present'.. build a 'deploy state' and pass it back?
8
+ - ** whole thing can be atomic **
9
+ - 1) new atomic temp dir
10
+ - 2) for each subdir not needing modification, hardlink into temp dir
11
+ - 3) for each subdir needing modification, new subdir, hardlink all files not needing modification
12
+ - 4) write (or if deleting, omit) new files
13
+ - 5) swap top level
14
+ - ** whole deploy can be atomic(-ish) - do this for everything **
15
+ - just a '/deploy/current' dir
16
+ - some things (venvs) cannot be moved, thus the /deploy/venvs dir
17
+ - ** ensure (enforce) equivalent relpath nesting
18
+ """
19
+ import os.path
20
+ import typing as ta
21
+
22
+ from omlish.lite.check import check
23
+ from omlish.os.paths import is_path_in_dir
24
+ from omlish.os.paths import relative_symlink
25
+
26
+ from .paths.paths import DEPLOY_PATH_PLACEHOLDER_SEPARATOR
27
+ from .specs import AppDeployConfLink
28
+ from .specs import DeployConfFile
29
+ from .specs import DeployConfLink
30
+ from .specs import DeployConfSpec
31
+ from .specs import TagDeployConfLink
32
+ from .types import DeployAppTag
33
+ from .types import DeployHome
34
+
35
+
36
+ class DeployConfManager:
37
+ def __init__(
38
+ self,
39
+ *,
40
+ deploy_home: ta.Optional[DeployHome] = None,
41
+ ) -> None:
42
+ super().__init__()
43
+
44
+ self._deploy_home = deploy_home
45
+
46
+ #
47
+
48
+ async def _write_conf_file(
49
+ self,
50
+ cf: DeployConfFile,
51
+ conf_dir: str,
52
+ ) -> None:
53
+ conf_file = os.path.join(conf_dir, cf.path)
54
+ check.arg(is_path_in_dir(conf_dir, conf_file))
55
+
56
+ os.makedirs(os.path.dirname(conf_file), exist_ok=True)
57
+
58
+ with open(conf_file, 'w') as f: # noqa
59
+ f.write(cf.body)
60
+
61
+ #
62
+
63
+ class _ComputedConfLink(ta.NamedTuple):
64
+ is_dir: bool
65
+ link_src: str
66
+ link_dst: str
67
+
68
+ def _compute_conf_link_dst(
69
+ self,
70
+ link: DeployConfLink,
71
+ app_tag: DeployAppTag,
72
+ conf_dir: str,
73
+ link_dir: str,
74
+ ) -> _ComputedConfLink:
75
+ link_src = os.path.join(conf_dir, link.src)
76
+ check.arg(is_path_in_dir(conf_dir, link_src))
77
+
78
+ #
79
+
80
+ if (is_dir := link.src.endswith('/')):
81
+ # @conf/ - links a directory in root of app conf dir to conf/@conf/@dst/
82
+ check.arg(link.src.count('/') == 1)
83
+ link_dst_pfx = link.src
84
+ link_dst_sfx = ''
85
+
86
+ elif '/' in link.src:
87
+ # @conf/file - links a single file in a single subdir to conf/@conf/@dst--file
88
+ d, f = os.path.split(link.src)
89
+ # TODO: check filename :|
90
+ link_dst_pfx = d + '/'
91
+ link_dst_sfx = DEPLOY_PATH_PLACEHOLDER_SEPARATOR + f
92
+
93
+ else: # noqa
94
+ # @conf(.ext)* - links a single file in root of app conf dir to conf/@conf/@dst(.ext)*
95
+ if '.' in link.src:
96
+ l, _, r = link.src.partition('.')
97
+ link_dst_pfx = l + '/'
98
+ link_dst_sfx = '.' + r
99
+ else:
100
+ link_dst_pfx = link.src + '/'
101
+ link_dst_sfx = ''
102
+
103
+ #
104
+
105
+ if isinstance(link, AppDeployConfLink):
106
+ link_dst_mid = str(app_tag.app)
107
+ elif isinstance(link, TagDeployConfLink):
108
+ link_dst_mid = DEPLOY_PATH_PLACEHOLDER_SEPARATOR.join([app_tag.app, app_tag.tag])
109
+ else:
110
+ raise TypeError(link)
111
+
112
+ #
113
+
114
+ link_dst_name = ''.join([
115
+ link_dst_pfx,
116
+ link_dst_mid,
117
+ link_dst_sfx,
118
+ ])
119
+ link_dst = os.path.join(link_dir, link_dst_name)
120
+
121
+ return DeployConfManager._ComputedConfLink(
122
+ is_dir=is_dir,
123
+ link_src=link_src,
124
+ link_dst=link_dst,
125
+ )
126
+
127
+ async def _make_conf_link(
128
+ self,
129
+ link: DeployConfLink,
130
+ app_tag: DeployAppTag,
131
+ conf_dir: str,
132
+ link_dir: str,
133
+ ) -> None:
134
+ comp = self._compute_conf_link_dst(
135
+ link,
136
+ app_tag,
137
+ conf_dir,
138
+ link_dir,
139
+ )
140
+
141
+ #
142
+
143
+ check.arg(is_path_in_dir(conf_dir, comp.link_src))
144
+ check.arg(is_path_in_dir(link_dir, comp.link_dst))
145
+
146
+ if comp.is_dir:
147
+ check.arg(os.path.isdir(comp.link_src))
148
+ else:
149
+ check.arg(os.path.isfile(comp.link_src))
150
+
151
+ #
152
+
153
+ relative_symlink( # noqa
154
+ comp.link_src,
155
+ comp.link_dst,
156
+ target_is_directory=comp.is_dir,
157
+ make_dirs=True,
158
+ )
159
+
160
+ #
161
+
162
+ async def write_conf(
163
+ self,
164
+ spec: DeployConfSpec,
165
+ app_tag: DeployAppTag,
166
+ conf_dir: str,
167
+ link_dir: str,
168
+ ) -> None:
169
+ for cf in spec.files or []:
170
+ await self._write_conf_file(
171
+ cf,
172
+ conf_dir,
173
+ )
174
+
175
+ #
176
+
177
+ for link in spec.links or []:
178
+ await self._make_conf_link(
179
+ link,
180
+ app_tag,
181
+ conf_dir,
182
+ link_dir,
183
+ )
@@ -0,0 +1,27 @@
1
+ # ruff: noqa: UP006 UP007
2
+ from .apps import DeployAppManager
3
+ from .paths.manager import DeployPathsManager
4
+ from .specs import DeploySpec
5
+
6
+
7
+ class DeployManager:
8
+ def __init__(
9
+ self,
10
+ *,
11
+ apps: DeployAppManager,
12
+ paths: DeployPathsManager,
13
+ ):
14
+ super().__init__()
15
+
16
+ self._apps = apps
17
+ self._paths = paths
18
+
19
+ async def run_deploy(
20
+ self,
21
+ spec: DeploySpec,
22
+ ) -> None:
23
+ self._paths.validate_deploy_paths()
24
+
25
+ #
26
+
27
+ await self._apps.prepare_app(spec)
@@ -17,9 +17,9 @@ from omlish.lite.cached import async_cached_nullary
17
17
  from omlish.lite.check import check
18
18
  from omlish.os.atomics import AtomicPathSwapping
19
19
 
20
- from .paths import SingleDirDeployPathOwner
21
- from .specs import DeployGitCheckout
20
+ from .paths.owners import SingleDirDeployPathOwner
22
21
  from .specs import DeployGitRepo
22
+ from .specs import DeployGitSpec
23
23
  from .types import DeployHome
24
24
  from .types import DeployRev
25
25
 
@@ -95,7 +95,7 @@ class DeployGitManager(SingleDirDeployPathOwner):
95
95
 
96
96
  #
97
97
 
98
- async def checkout(self, checkout: DeployGitCheckout, dst_dir: str) -> None:
98
+ async def checkout(self, spec: DeployGitSpec, dst_dir: str) -> None:
99
99
  check.state(not os.path.exists(dst_dir))
100
100
  with self._git._atomics.begin_atomic_path_swap( # noqa
101
101
  'dir',
@@ -103,14 +103,14 @@ class DeployGitManager(SingleDirDeployPathOwner):
103
103
  auto_commit=True,
104
104
  make_dirs=True,
105
105
  ) as dst_swap:
106
- await self.fetch(checkout.rev)
106
+ await self.fetch(spec.rev)
107
107
 
108
108
  dst_call = functools.partial(asyncio_subprocesses.check_call, cwd=dst_swap.tmp_path)
109
109
  await dst_call('git', 'init')
110
110
 
111
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 []))
112
+ await dst_call('git', 'fetch', '--depth=1', 'local', spec.rev)
113
+ await dst_call('git', 'checkout', spec.rev, *(spec.subtrees or []))
114
114
 
115
115
  def get_repo_dir(self, repo: DeployGitRepo) -> RepoDir:
116
116
  try:
@@ -119,5 +119,9 @@ class DeployGitManager(SingleDirDeployPathOwner):
119
119
  repo_dir = self._repo_dirs[repo] = DeployGitManager.RepoDir(self, repo)
120
120
  return repo_dir
121
121
 
122
- async def checkout(self, checkout: DeployGitCheckout, dst_dir: str) -> None:
123
- await self.get_repo_dir(checkout.repo).checkout(checkout, dst_dir)
122
+ async def checkout(
123
+ self,
124
+ spec: DeployGitSpec,
125
+ dst_dir: str,
126
+ ) -> None:
127
+ await self.get_repo_dir(spec.repo).checkout(spec, dst_dir)
@@ -11,10 +11,14 @@ from ..commands.inject import bind_command
11
11
  from .apps import DeployAppManager
12
12
  from .commands import DeployCommand
13
13
  from .commands import DeployCommandExecutor
14
+ from .conf import DeployConfManager
14
15
  from .config import DeployConfig
16
+ from .deploy import DeployManager
15
17
  from .git import DeployGitManager
16
18
  from .interp import InterpCommand
17
19
  from .interp import InterpCommandExecutor
20
+ from .paths.inject import bind_deploy_paths
21
+ from .paths.owners import DeployPathOwner
18
22
  from .tmp import DeployTmpManager
19
23
  from .types import DeployHome
20
24
  from .venvs import DeployVenvManager
@@ -27,22 +31,43 @@ def bind_deploy(
27
31
  lst: ta.List[InjectorBindingOrBindings] = [
28
32
  inj.bind(deploy_config),
29
33
 
30
- #
34
+ bind_deploy_paths(),
35
+ ]
36
+
37
+ #
38
+
39
+ def bind_manager(cls: type) -> InjectorBindings:
40
+ return inj.as_bindings(
41
+ inj.bind(cls, singleton=True),
42
+
43
+ *([inj.bind(DeployPathOwner, to_key=cls, array=True)] if issubclass(cls, DeployPathOwner) else []),
44
+ )
45
+
46
+ #
31
47
 
32
- inj.bind(DeployAppManager, singleton=True),
48
+ lst.extend([
49
+ bind_manager(DeployAppManager),
33
50
 
34
- inj.bind(DeployGitManager, singleton=True),
51
+ bind_manager(DeployConfManager),
35
52
 
36
- inj.bind(DeployTmpManager, singleton=True),
53
+ bind_manager(DeployGitManager),
54
+
55
+ bind_manager(DeployManager),
56
+
57
+ bind_manager(DeployTmpManager),
37
58
  inj.bind(AtomicPathSwapping, to_key=DeployTmpManager),
38
59
 
39
- inj.bind(DeployVenvManager, singleton=True),
60
+ bind_manager(DeployVenvManager),
61
+ ])
40
62
 
41
- #
63
+ #
42
64
 
65
+ lst.extend([
43
66
  bind_command(DeployCommand, DeployCommandExecutor),
44
67
  bind_command(InterpCommand, InterpCommandExecutor),
45
- ]
68
+ ])
69
+
70
+ #
46
71
 
47
72
  if (dh := deploy_config.deploy_home) is not None:
48
73
  dh = os.path.abspath(os.path.expanduser(dh))
File without changes
@@ -0,0 +1,21 @@
1
+ # ruff: noqa: UP006 UP007
2
+ import typing as ta
3
+
4
+ from omlish.lite.inject import InjectorBindingOrBindings
5
+ from omlish.lite.inject import InjectorBindings
6
+ from omlish.lite.inject import inj
7
+
8
+ from .manager import DeployPathsManager
9
+ from .owners import DeployPathOwner
10
+ from .owners import DeployPathOwners
11
+
12
+
13
+ def bind_deploy_paths() -> InjectorBindings:
14
+ lst: ta.List[InjectorBindingOrBindings] = [
15
+ inj.bind_array(DeployPathOwner),
16
+ inj.bind_array_type(DeployPathOwner, DeployPathOwners),
17
+
18
+ inj.bind(DeployPathsManager, singleton=True),
19
+ ]
20
+
21
+ return inj.as_bindings(*lst)
@@ -0,0 +1,36 @@
1
+ # ruff: noqa: UP006 UP007
2
+ import typing as ta
3
+
4
+ from omlish.lite.cached import cached_nullary
5
+
6
+ from ..types import DeployHome
7
+ from .owners import DeployPathOwner
8
+ from .owners import DeployPathOwners
9
+ from .paths import DeployPath
10
+ from .paths import DeployPathError
11
+
12
+
13
+ class DeployPathsManager:
14
+ def __init__(
15
+ self,
16
+ *,
17
+ deploy_home: ta.Optional[DeployHome],
18
+ deploy_path_owners: DeployPathOwners,
19
+ ) -> None:
20
+ super().__init__()
21
+
22
+ self._deploy_home = deploy_home
23
+ self._deploy_path_owners = deploy_path_owners
24
+
25
+ @cached_nullary
26
+ def owners_by_path(self) -> ta.Mapping[DeployPath, DeployPathOwner]:
27
+ dct: ta.Dict[DeployPath, DeployPathOwner] = {}
28
+ for o in self._deploy_path_owners:
29
+ for p in o.get_owned_deploy_paths():
30
+ if p in dct:
31
+ raise DeployPathError(f'Duplicate deploy path owner: {p}')
32
+ dct[p] = o
33
+ return dct
34
+
35
+ def validate_deploy_paths(self) -> None:
36
+ self.owners_by_path()
@@ -0,0 +1,50 @@
1
+ # ruff: noqa: UP006 UP007
2
+ import abc
3
+ import os.path
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 DeployHome
10
+ from .paths import DeployPath
11
+
12
+
13
+ class DeployPathOwner(abc.ABC):
14
+ @abc.abstractmethod
15
+ def get_owned_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
16
+ raise NotImplementedError
17
+
18
+
19
+ DeployPathOwners = ta.NewType('DeployPathOwners', ta.Sequence[DeployPathOwner])
20
+
21
+
22
+ class SingleDirDeployPathOwner(DeployPathOwner, abc.ABC):
23
+ def __init__(
24
+ self,
25
+ *args: ta.Any,
26
+ owned_dir: str,
27
+ deploy_home: ta.Optional[DeployHome],
28
+ **kwargs: ta.Any,
29
+ ) -> None:
30
+ super().__init__(*args, **kwargs)
31
+
32
+ check.not_in('/', owned_dir)
33
+ self._owned_dir: str = check.non_empty_str(owned_dir)
34
+
35
+ self._deploy_home = deploy_home
36
+
37
+ self._owned_deploy_paths = frozenset([DeployPath.parse(self._owned_dir + '/')])
38
+
39
+ @cached_nullary
40
+ def _dir(self) -> str:
41
+ return os.path.join(check.non_empty_str(self._deploy_home), self._owned_dir)
42
+
43
+ @cached_nullary
44
+ def _make_dir(self) -> str:
45
+ if not os.path.isdir(d := self._dir()):
46
+ os.makedirs(d, exist_ok=True)
47
+ return d
48
+
49
+ def get_owned_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
50
+ return self._owned_deploy_paths