ominfra 0.0.0.dev189__py3-none-any.whl → 0.0.0.dev190__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.
ominfra/scripts/manage.py CHANGED
@@ -6635,6 +6635,7 @@ class AtomicPathSwapping(abc.ABC):
6635
6635
  *,
6636
6636
  name_hint: ta.Optional[str] = None,
6637
6637
  make_dirs: bool = False,
6638
+ skip_root_dir_check: bool = False,
6638
6639
  **kwargs: ta.Any,
6639
6640
  ) -> AtomicPathSwap:
6640
6641
  raise NotImplementedError
@@ -6698,10 +6699,15 @@ class TempDirAtomicPathSwapping(AtomicPathSwapping):
6698
6699
  *,
6699
6700
  name_hint: ta.Optional[str] = None,
6700
6701
  make_dirs: bool = False,
6702
+ skip_root_dir_check: bool = False,
6701
6703
  **kwargs: ta.Any,
6702
6704
  ) -> AtomicPathSwap:
6703
6705
  dst_path = os.path.abspath(dst_path)
6704
- if self._root_dir is not None and not dst_path.startswith(check.non_empty_str(self._root_dir)):
6706
+ if (
6707
+ not skip_root_dir_check and
6708
+ self._root_dir is not None and
6709
+ not dst_path.startswith(check.non_empty_str(self._root_dir))
6710
+ ):
6705
6711
  raise RuntimeError(f'Atomic path swap dst must be in root dir: {dst_path}, {self._root_dir}')
6706
6712
 
6707
6713
  dst_dir = os.path.dirname(dst_path)
@@ -6726,6 +6732,54 @@ class TempDirAtomicPathSwapping(AtomicPathSwapping):
6726
6732
  )
6727
6733
 
6728
6734
 
6735
+ ########################################
6736
+ # ../../../omlish/text/indent.py
6737
+
6738
+
6739
+ class IndentWriter:
6740
+ DEFAULT_INDENT = ' ' * 4
6741
+
6742
+ def __init__(
6743
+ self,
6744
+ *,
6745
+ buf: ta.Optional[io.StringIO] = None,
6746
+ indent: ta.Optional[str] = None,
6747
+ ) -> None:
6748
+ super().__init__()
6749
+
6750
+ self._buf = buf if buf is not None else io.StringIO()
6751
+ self._indent = check.isinstance(indent, str) if indent is not None else self.DEFAULT_INDENT
6752
+ self._level = 0
6753
+ self._has_indented = False
6754
+
6755
+ @contextlib.contextmanager
6756
+ def indent(self, num: int = 1) -> ta.Iterator[None]:
6757
+ self._level += num
6758
+ try:
6759
+ yield
6760
+ finally:
6761
+ self._level -= num
6762
+
6763
+ def write(self, s: str) -> None:
6764
+ indent = self._indent * self._level
6765
+ i = 0
6766
+ while i < len(s):
6767
+ if not self._has_indented:
6768
+ self._buf.write(indent)
6769
+ self._has_indented = True
6770
+ try:
6771
+ n = s.index('\n', i)
6772
+ except ValueError:
6773
+ self._buf.write(s[i:])
6774
+ break
6775
+ self._buf.write(s[i:n + 1])
6776
+ self._has_indented = False
6777
+ i = n + 2
6778
+
6779
+ def getvalue(self) -> str:
6780
+ return self._buf.getvalue()
6781
+
6782
+
6729
6783
  ########################################
6730
6784
  # ../../../omdev/interp/types.py
6731
6785
 
@@ -7784,6 +7838,78 @@ class AbstractAsyncSubprocesses(BaseSubprocesses):
7784
7838
  return ret.decode().strip()
7785
7839
 
7786
7840
 
7841
+ ########################################
7842
+ # ../../../omserv/nginx/configs.py
7843
+ """
7844
+ TODO:
7845
+ - omnibus/jmespath
7846
+
7847
+ https://nginx.org/en/docs/dev/development_guide.html
7848
+ https://nginx.org/en/docs/dev/development_guide.html#config_directives
7849
+ https://nginx.org/en/docs/example.html
7850
+
7851
+ https://github.com/yandex/gixy
7852
+ """
7853
+
7854
+
7855
+ @dc.dataclass()
7856
+ class NginxConfigItems:
7857
+ lst: ta.List['NginxConfigItem']
7858
+
7859
+ @classmethod
7860
+ def of(cls, obj: ta.Any) -> 'NginxConfigItems':
7861
+ if isinstance(obj, NginxConfigItems):
7862
+ return obj
7863
+ return cls([NginxConfigItem.of(e) for e in check.isinstance(obj, list)])
7864
+
7865
+
7866
+ @dc.dataclass()
7867
+ class NginxConfigItem:
7868
+ name: str
7869
+ args: ta.Optional[ta.List[str]] = None
7870
+ block: ta.Optional[NginxConfigItems] = None
7871
+
7872
+ @classmethod
7873
+ def of(cls, obj: ta.Any) -> 'NginxConfigItem':
7874
+ if isinstance(obj, NginxConfigItem):
7875
+ return obj
7876
+ args = check.isinstance(check.not_isinstance(obj, str), collections.abc.Sequence)
7877
+ name, args = check.isinstance(args[0], str), args[1:]
7878
+ if args and not isinstance(args[-1], str):
7879
+ block, args = NginxConfigItems.of(args[-1]), args[:-1]
7880
+ else:
7881
+ block = None
7882
+ return NginxConfigItem(name, [check.isinstance(e, str) for e in args], block=block)
7883
+
7884
+
7885
+ def render_nginx_config(wr: IndentWriter, obj: ta.Any) -> None:
7886
+ if isinstance(obj, NginxConfigItem):
7887
+ wr.write(obj.name)
7888
+ for e in obj.args or ():
7889
+ wr.write(' ')
7890
+ wr.write(e)
7891
+ if obj.block:
7892
+ wr.write(' {\n')
7893
+ with wr.indent():
7894
+ render_nginx_config(wr, obj.block)
7895
+ wr.write('}\n')
7896
+ else:
7897
+ wr.write(';\n')
7898
+
7899
+ elif isinstance(obj, NginxConfigItems):
7900
+ for e2 in obj.lst:
7901
+ render_nginx_config(wr, e2)
7902
+
7903
+ else:
7904
+ raise TypeError(obj)
7905
+
7906
+
7907
+ def render_nginx_config_str(obj: ta.Any) -> str:
7908
+ iw = IndentWriter()
7909
+ render_nginx_config(iw, obj)
7910
+ return iw.getvalue()
7911
+
7912
+
7787
7913
  ########################################
7788
7914
  # ../../../omdev/interp/providers/base.py
7789
7915
  """
@@ -7900,6 +8026,15 @@ class IniDeployAppConfContent(DeployAppConfContent):
7900
8026
  sections: IniConfigSectionSettingsMap
7901
8027
 
7902
8028
 
8029
+ #
8030
+
8031
+
8032
+ @register_single_field_type_obj_marshaler('items')
8033
+ @dc.dataclass(frozen=True)
8034
+ class NginxDeployAppConfContent(DeployAppConfContent):
8035
+ items: ta.Any
8036
+
8037
+
7903
8038
  ##
7904
8039
 
7905
8040
 
@@ -7951,37 +8086,6 @@ class DeployAppConfSpec:
7951
8086
  seen.add(f.path)
7952
8087
 
7953
8088
 
7954
- ########################################
7955
- # ../deploy/deploy.py
7956
-
7957
-
7958
- DEPLOY_TAG_DATETIME_FMT = '%Y%m%dT%H%M%SZ'
7959
-
7960
-
7961
- DeployManagerUtcClock = ta.NewType('DeployManagerUtcClock', Func0[datetime.datetime])
7962
-
7963
-
7964
- class DeployManager:
7965
- def __init__(
7966
- self,
7967
- *,
7968
-
7969
- utc_clock: ta.Optional[DeployManagerUtcClock] = None,
7970
- ):
7971
- super().__init__()
7972
-
7973
- self._utc_clock = utc_clock
7974
-
7975
- def _utc_now(self) -> datetime.datetime:
7976
- if self._utc_clock is not None:
7977
- return self._utc_clock() # noqa
7978
- else:
7979
- return datetime.datetime.now(tz=datetime.timezone.utc) # noqa
7980
-
7981
- def make_deploy_time(self) -> DeployTime:
7982
- return DeployTime(self._utc_now().strftime(DEPLOY_TAG_DATETIME_FMT))
7983
-
7984
-
7985
8089
  ########################################
7986
8090
  # ../deploy/paths/paths.py
7987
8091
  """
@@ -8003,6 +8107,10 @@ class DeployPathError(Exception):
8003
8107
 
8004
8108
 
8005
8109
  class DeployPathRenderable(abc.ABC):
8110
+ @cached_nullary
8111
+ def __str__(self) -> str:
8112
+ return self.render(None)
8113
+
8006
8114
  @abc.abstractmethod
8007
8115
  def render(self, tags: ta.Optional[DeployTagMap] = None) -> str:
8008
8116
  raise NotImplementedError
@@ -8144,7 +8252,7 @@ class FileDeployPathPart(DeployPathPart):
8144
8252
 
8145
8253
 
8146
8254
  @dc.dataclass(frozen=True)
8147
- class DeployPath:
8255
+ class DeployPath(DeployPathRenderable):
8148
8256
  parts: ta.Sequence[DeployPathPart]
8149
8257
 
8150
8258
  @property
@@ -9171,6 +9279,10 @@ class DeployConfManager:
9171
9279
  elif isinstance(ac, IniDeployAppConfContent):
9172
9280
  return strip_with_newline(render_ini_config(ac.sections))
9173
9281
 
9282
+ elif isinstance(ac, NginxDeployAppConfContent):
9283
+ ni = NginxConfigItems.of(ac.items)
9284
+ return strip_with_newline(render_nginx_config_str(ni))
9285
+
9174
9286
  else:
9175
9287
  raise TypeError(ac)
9176
9288
 
@@ -9189,6 +9301,17 @@ class DeployConfManager:
9189
9301
  with open(conf_file, 'w') as f: # noqa
9190
9302
  f.write(body)
9191
9303
 
9304
+ async def write_app_conf(
9305
+ self,
9306
+ spec: DeployAppConfSpec,
9307
+ app_conf_dir: str,
9308
+ ) -> None:
9309
+ for acf in spec.files or []:
9310
+ await self._write_app_conf_file(
9311
+ acf,
9312
+ app_conf_dir,
9313
+ )
9314
+
9192
9315
  #
9193
9316
 
9194
9317
  class _ComputedConfLink(ta.NamedTuple):
@@ -9298,23 +9421,13 @@ class DeployConfManager:
9298
9421
  make_dirs=True,
9299
9422
  )
9300
9423
 
9301
- #
9302
-
9303
- async def write_app_conf(
9424
+ async def link_app_conf(
9304
9425
  self,
9305
9426
  spec: DeployAppConfSpec,
9306
9427
  tags: DeployTagMap,
9307
9428
  app_conf_dir: str,
9308
9429
  conf_link_dir: str,
9309
- ) -> None:
9310
- for acf in spec.files or []:
9311
- await self._write_app_conf_file(
9312
- acf,
9313
- app_conf_dir,
9314
- )
9315
-
9316
- #
9317
-
9430
+ ):
9318
9431
  for link in spec.links or []:
9319
9432
  await self._make_app_conf_link(
9320
9433
  link,
@@ -9442,11 +9555,22 @@ class DeployAppSpec(DeploySpecKeyed[DeployAppKey]):
9442
9555
  ##
9443
9556
 
9444
9557
 
9558
+ @dc.dataclass(frozen=True)
9559
+ class DeploySystemdSpec:
9560
+ # ~/.config/systemd/user/
9561
+ unit_dir: ta.Optional[str] = None
9562
+
9563
+
9564
+ ##
9565
+
9566
+
9445
9567
  @dc.dataclass(frozen=True)
9446
9568
  class DeploySpec(DeploySpecKeyed[DeployKey]):
9447
9569
  home: DeployHome
9448
9570
 
9449
- apps: ta.Sequence[DeployAppSpec]
9571
+ apps: ta.Sequence[DeployAppSpec] = ()
9572
+
9573
+ systemd: ta.Optional[DeploySystemdSpec] = None
9450
9574
 
9451
9575
  def __post_init__(self) -> None:
9452
9576
  check.non_empty_str(self.home)
@@ -10848,6 +10972,109 @@ def bind_deploy_paths() -> InjectorBindings:
10848
10972
  return inj.as_bindings(*lst)
10849
10973
 
10850
10974
 
10975
+ ########################################
10976
+ # ../deploy/systemd.py
10977
+ """
10978
+ ~/.config/systemd/user/
10979
+
10980
+ verify - systemd-analyze
10981
+
10982
+ sudo loginctl enable-linger "$USER"
10983
+
10984
+ cat ~/.config/systemd/user/sleep-infinity.service
10985
+ [Unit]
10986
+ Description=User-specific service to run 'sleep infinity'
10987
+ After=default.target
10988
+
10989
+ [Service]
10990
+ ExecStart=/bin/sleep infinity
10991
+ Restart=always
10992
+ RestartSec=5
10993
+
10994
+ [Install]
10995
+ WantedBy=default.target
10996
+
10997
+ systemctl --user daemon-reload
10998
+
10999
+ systemctl --user enable sleep-infinity.service
11000
+ systemctl --user start sleep-infinity.service
11001
+
11002
+ systemctl --user status sleep-infinity.service
11003
+ """
11004
+
11005
+
11006
+ class DeploySystemdManager:
11007
+ def __init__(
11008
+ self,
11009
+ *,
11010
+ atomics: DeployHomeAtomics,
11011
+ ) -> None:
11012
+ super().__init__()
11013
+
11014
+ self._atomics = atomics
11015
+
11016
+ def _scan_link_dir(
11017
+ self,
11018
+ d: str,
11019
+ *,
11020
+ strict: bool = False,
11021
+ ) -> ta.Dict[str, str]:
11022
+ o: ta.Dict[str, str] = {}
11023
+ for f in os.listdir(d):
11024
+ fp = os.path.join(d, f)
11025
+ if strict:
11026
+ check.state(os.path.islink(fp))
11027
+ o[f] = abs_real_path(fp)
11028
+ return o
11029
+
11030
+ async def sync_systemd(
11031
+ self,
11032
+ spec: ta.Optional[DeploySystemdSpec],
11033
+ home: DeployHome,
11034
+ conf_dir: str,
11035
+ ) -> None:
11036
+ check.non_empty_str(home)
11037
+
11038
+ if not spec:
11039
+ return
11040
+
11041
+ if not (ud := spec.unit_dir):
11042
+ return
11043
+
11044
+ ud = abs_real_path(os.path.expanduser(ud))
11045
+
11046
+ os.makedirs(ud, exist_ok=True)
11047
+
11048
+ uld = {
11049
+ n: p
11050
+ for n, p in self._scan_link_dir(ud).items()
11051
+ if is_path_in_dir(home, p)
11052
+ }
11053
+
11054
+ if os.path.exists(conf_dir):
11055
+ cld = self._scan_link_dir(conf_dir, strict=True)
11056
+ else:
11057
+ cld = {}
11058
+
11059
+ for n in sorted(set(uld) | set(cld)):
11060
+ ul = uld.get(n) # noqa
11061
+ cl = cld.get(n)
11062
+ if cl is None:
11063
+ os.unlink(os.path.join(ud, n))
11064
+ else:
11065
+ with self._atomics(home).begin_atomic_path_swap( # noqa
11066
+ 'file',
11067
+ os.path.join(ud, n),
11068
+ auto_commit=True,
11069
+ skip_root_dir_check=True,
11070
+ ) as dst_swap:
11071
+ os.unlink(dst_swap.tmp_path)
11072
+ os.symlink(
11073
+ os.path.relpath(cl, os.path.dirname(dst_swap.dst_path)),
11074
+ dst_swap.tmp_path,
11075
+ )
11076
+
11077
+
10851
11078
  ########################################
10852
11079
  # ../remote/inject.py
10853
11080
 
@@ -11202,7 +11429,6 @@ class DeployVenvManager:
11202
11429
  async def setup_venv(
11203
11430
  self,
11204
11431
  spec: DeployVenvSpec,
11205
- home: DeployHome,
11206
11432
  git_dir: str,
11207
11433
  venv_dir: str,
11208
11434
  ) -> None:
@@ -11245,39 +11471,31 @@ class DeployAppManager(DeployPathOwner):
11245
11471
  def __init__(
11246
11472
  self,
11247
11473
  *,
11248
- conf: DeployConfManager,
11249
11474
  git: DeployGitManager,
11250
11475
  venvs: DeployVenvManager,
11476
+ conf: DeployConfManager,
11477
+
11478
+ msh: ObjMarshalerManager,
11251
11479
  ) -> None:
11252
11480
  super().__init__()
11253
11481
 
11254
- self._conf = conf
11255
11482
  self._git = git
11256
11483
  self._venvs = venvs
11484
+ self._conf = conf
11257
11485
 
11258
- #
11259
-
11260
- _APP_DIR_STR = 'apps/@app/@time--@app-rev--@app-key/'
11261
- _APP_DIR = DeployPath.parse(_APP_DIR_STR)
11486
+ self._msh = msh
11262
11487
 
11263
- _DEPLOY_DIR_STR = 'deploys/@time--@deploy-key/'
11264
- _DEPLOY_DIR = DeployPath.parse(_DEPLOY_DIR_STR)
11488
+ #
11265
11489
 
11266
- _APP_DEPLOY_LINK = DeployPath.parse(f'{_DEPLOY_DIR_STR}apps/@app')
11267
- _CONF_DEPLOY_DIR = DeployPath.parse(f'{_DEPLOY_DIR_STR}conf/@conf/')
11490
+ APP_DIR = DeployPath.parse('apps/@app/@time--@app-rev--@app-key/')
11268
11491
 
11269
11492
  @cached_nullary
11270
11493
  def get_owned_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
11271
11494
  return {
11272
- self._APP_DIR,
11273
-
11274
- self._DEPLOY_DIR,
11275
-
11276
- self._APP_DEPLOY_LINK,
11277
- self._CONF_DEPLOY_DIR,
11495
+ self.APP_DIR,
11278
11496
 
11279
11497
  *[
11280
- DeployPath.parse(f'{self._APP_DIR_STR}{sfx}/')
11498
+ DeployPath.parse(f'{self.APP_DIR}{sfx}/')
11281
11499
  for sfx in [
11282
11500
  'conf',
11283
11501
  'git',
@@ -11288,122 +11506,148 @@ class DeployAppManager(DeployPathOwner):
11288
11506
 
11289
11507
  #
11290
11508
 
11509
+ @dc.dataclass(frozen=True)
11510
+ class PreparedApp:
11511
+ app_dir: str
11512
+
11513
+ git_dir: ta.Optional[str] = None
11514
+ venv_dir: ta.Optional[str] = None
11515
+ conf_dir: ta.Optional[str] = None
11516
+
11291
11517
  async def prepare_app(
11292
11518
  self,
11293
11519
  spec: DeployAppSpec,
11294
11520
  home: DeployHome,
11295
11521
  tags: DeployTagMap,
11296
- ) -> None:
11297
- check.non_empty_str(home)
11298
-
11299
- def build_path(pth: DeployPath) -> str:
11300
- return os.path.join(home, pth.render(tags))
11301
-
11302
- app_dir = build_path(self._APP_DIR)
11303
- deploy_dir = build_path(self._DEPLOY_DIR)
11304
- app_deploy_link = build_path(self._APP_DEPLOY_LINK)
11522
+ ) -> PreparedApp:
11523
+ spec_json = json_dumps_pretty(self._msh.marshal_obj(spec))
11305
11524
 
11306
11525
  #
11307
11526
 
11308
- os.makedirs(deploy_dir, exist_ok=True)
11527
+ check.non_empty_str(home)
11309
11528
 
11310
- deploying_link = os.path.join(home, 'deploys/deploying')
11311
- if os.path.exists(deploying_link):
11312
- os.unlink(deploying_link)
11313
- relative_symlink(
11314
- deploy_dir,
11315
- deploying_link,
11316
- target_is_directory=True,
11317
- make_dirs=True,
11318
- )
11529
+ app_dir = os.path.join(home, self.APP_DIR.render(tags))
11319
11530
 
11320
- #
11321
-
11322
- os.makedirs(app_dir)
11323
- relative_symlink(
11324
- app_dir,
11325
- app_deploy_link,
11326
- target_is_directory=True,
11327
- make_dirs=True,
11328
- )
11531
+ os.makedirs(app_dir, exist_ok=True)
11329
11532
 
11330
11533
  #
11331
11534
 
11332
- deploy_conf_dir = os.path.join(deploy_dir, 'conf')
11333
- os.makedirs(deploy_conf_dir, exist_ok=True)
11535
+ rkw: ta.Dict[str, ta.Any] = dict(
11536
+ app_dir=app_dir,
11537
+ )
11334
11538
 
11335
11539
  #
11336
11540
 
11337
- # def mirror_symlinks(src: str, dst: str) -> None:
11338
- # def mirror_link(lp: str) -> None:
11339
- # check.state(os.path.islink(lp))
11340
- # shutil.copy2(
11341
- # lp,
11342
- # os.path.join(dst, os.path.relpath(lp, src)),
11343
- # follow_symlinks=False,
11344
- # )
11345
- #
11346
- # for dp, dns, fns in os.walk(src, followlinks=False):
11347
- # for fn in fns:
11348
- # mirror_link(os.path.join(dp, fn))
11349
- #
11350
- # for dn in dns:
11351
- # dp2 = os.path.join(dp, dn)
11352
- # if os.path.islink(dp2):
11353
- # mirror_link(dp2)
11354
- # else:
11355
- # os.makedirs(os.path.join(dst, os.path.relpath(dp2, src)))
11356
-
11357
- current_link = os.path.join(home, 'deploys/current')
11358
-
11359
- # if os.path.exists(current_link):
11360
- # mirror_symlinks(
11361
- # os.path.join(current_link, 'conf'),
11362
- # conf_tag_dir,
11363
- # )
11364
- # mirror_symlinks(
11365
- # os.path.join(current_link, 'apps'),
11366
- # os.path.join(deploy_dir, 'apps'),
11367
- # )
11541
+ spec_file = os.path.join(app_dir, 'spec.json')
11542
+ with open(spec_file, 'w') as f: # noqa
11543
+ f.write(spec_json)
11368
11544
 
11369
11545
  #
11370
11546
 
11371
- app_git_dir = os.path.join(app_dir, 'git')
11547
+ git_dir = os.path.join(app_dir, 'git')
11548
+ rkw.update(git_dir=git_dir)
11372
11549
  await self._git.checkout(
11373
11550
  spec.git,
11374
11551
  home,
11375
- app_git_dir,
11552
+ git_dir,
11376
11553
  )
11377
11554
 
11378
11555
  #
11379
11556
 
11380
11557
  if spec.venv is not None:
11381
- app_venv_dir = os.path.join(app_dir, 'venv')
11558
+ venv_dir = os.path.join(app_dir, 'venv')
11559
+ rkw.update(venv_dir=venv_dir)
11382
11560
  await self._venvs.setup_venv(
11383
11561
  spec.venv,
11384
- home,
11385
- app_git_dir,
11386
- app_venv_dir,
11562
+ git_dir,
11563
+ venv_dir,
11387
11564
  )
11388
11565
 
11389
11566
  #
11390
11567
 
11391
11568
  if spec.conf is not None:
11392
- app_conf_dir = os.path.join(app_dir, 'conf')
11569
+ conf_dir = os.path.join(app_dir, 'conf')
11570
+ rkw.update(conf_dir=conf_dir)
11393
11571
  await self._conf.write_app_conf(
11394
11572
  spec.conf,
11395
- tags,
11396
- app_conf_dir,
11397
- deploy_conf_dir,
11573
+ conf_dir,
11398
11574
  )
11399
11575
 
11400
11576
  #
11401
11577
 
11402
- os.replace(deploying_link, current_link)
11578
+ return DeployAppManager.PreparedApp(**rkw)
11403
11579
 
11404
11580
 
11405
11581
  ########################################
11406
- # ../deploy/driver.py
11582
+ # ../deploy/deploy.py
11583
+
11584
+
11585
+ ##
11586
+
11587
+
11588
+ DEPLOY_TAG_DATETIME_FMT = '%Y%m%dT%H%M%SZ'
11589
+
11590
+
11591
+ DeployManagerUtcClock = ta.NewType('DeployManagerUtcClock', Func0[datetime.datetime])
11592
+
11593
+
11594
+ class DeployManager(DeployPathOwner):
11595
+ def __init__(
11596
+ self,
11597
+ *,
11598
+ utc_clock: ta.Optional[DeployManagerUtcClock] = None,
11599
+ ):
11600
+ super().__init__()
11601
+
11602
+ self._utc_clock = utc_clock
11603
+
11604
+ #
11605
+
11606
+ DEPLOYS_DIR = DeployPath.parse('deploys/')
11607
+
11608
+ CURRENT_DEPLOY_LINK = DeployPath.parse(f'{DEPLOYS_DIR}current')
11609
+ DEPLOYING_DEPLOY_LINK = DeployPath.parse(f'{DEPLOYS_DIR}deploying')
11610
+
11611
+ DEPLOY_DIR = DeployPath.parse(f'{DEPLOYS_DIR}@time--@deploy-key/')
11612
+ DEPLOY_SPEC_FILE = DeployPath.parse(f'{DEPLOY_DIR}spec.json')
11613
+
11614
+ APPS_DEPLOY_DIR = DeployPath.parse(f'{DEPLOY_DIR}apps/')
11615
+ APP_DEPLOY_LINK = DeployPath.parse(f'{APPS_DEPLOY_DIR}@app')
11616
+
11617
+ CONFS_DEPLOY_DIR = DeployPath.parse(f'{DEPLOY_DIR}conf/')
11618
+ CONF_DEPLOY_DIR = DeployPath.parse(f'{CONFS_DEPLOY_DIR}@conf/')
11619
+
11620
+ @cached_nullary
11621
+ def get_owned_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
11622
+ return {
11623
+ self.DEPLOYS_DIR,
11624
+
11625
+ self.CURRENT_DEPLOY_LINK,
11626
+ self.DEPLOYING_DEPLOY_LINK,
11627
+
11628
+ self.DEPLOY_DIR,
11629
+ self.DEPLOY_SPEC_FILE,
11630
+
11631
+ self.APPS_DEPLOY_DIR,
11632
+ self.APP_DEPLOY_LINK,
11633
+
11634
+ self.CONFS_DEPLOY_DIR,
11635
+ self.CONF_DEPLOY_DIR,
11636
+ }
11637
+
11638
+ #
11639
+
11640
+ def _utc_now(self) -> datetime.datetime:
11641
+ if self._utc_clock is not None:
11642
+ return self._utc_clock() # noqa
11643
+ else:
11644
+ return datetime.datetime.now(tz=datetime.timezone.utc) # noqa
11645
+
11646
+ def make_deploy_time(self) -> DeployTime:
11647
+ return DeployTime(self._utc_now().strftime(DEPLOY_TAG_DATETIME_FMT))
11648
+
11649
+
11650
+ ##
11407
11651
 
11408
11652
 
11409
11653
  class DeployDriverFactory(Func1[DeploySpec, ta.ContextManager['DeployDriver']]):
@@ -11418,8 +11662,13 @@ class DeployDriver:
11418
11662
  home: DeployHome,
11419
11663
  time: DeployTime,
11420
11664
 
11665
+ deploys: DeployManager,
11421
11666
  paths: DeployPathsManager,
11422
11667
  apps: DeployAppManager,
11668
+ conf: DeployConfManager,
11669
+ systemd: DeploySystemdManager,
11670
+
11671
+ msh: ObjMarshalerManager,
11423
11672
  ) -> None:
11424
11673
  super().__init__()
11425
11674
 
@@ -11427,32 +11676,123 @@ class DeployDriver:
11427
11676
  self._home = home
11428
11677
  self._time = time
11429
11678
 
11679
+ self._deploys = deploys
11430
11680
  self._paths = paths
11431
11681
  self._apps = apps
11682
+ self._conf = conf
11683
+ self._systemd = systemd
11684
+
11685
+ self._msh = msh
11686
+
11687
+ #
11688
+
11689
+ @property
11690
+ def deploy_tags(self) -> DeployTagMap:
11691
+ return DeployTagMap(
11692
+ self._time,
11693
+ self._spec.key(),
11694
+ )
11695
+
11696
+ def render_deploy_path(self, pth: DeployPath, tags: ta.Optional[DeployTagMap] = None) -> str:
11697
+ return os.path.join(self._home, pth.render(tags if tags is not None else self.deploy_tags))
11698
+
11699
+ @property
11700
+ def deploy_dir(self) -> str:
11701
+ return self.render_deploy_path(self._deploys.DEPLOY_DIR)
11702
+
11703
+ #
11432
11704
 
11433
11705
  async def drive_deploy(self) -> None:
11706
+ spec_json = json_dumps_pretty(self._msh.marshal_obj(self._spec))
11707
+
11708
+ #
11709
+
11434
11710
  self._paths.validate_deploy_paths()
11435
11711
 
11436
11712
  #
11437
11713
 
11438
- deploy_tags = DeployTagMap(
11439
- self._time,
11440
- self._spec.key(),
11714
+ os.makedirs(self.deploy_dir)
11715
+
11716
+ #
11717
+
11718
+ spec_file = self.render_deploy_path(self._deploys.DEPLOY_SPEC_FILE)
11719
+ with open(spec_file, 'w') as f: # noqa
11720
+ f.write(spec_json)
11721
+
11722
+ #
11723
+
11724
+ deploying_link = self.render_deploy_path(self._deploys.DEPLOYING_DEPLOY_LINK)
11725
+ if os.path.exists(deploying_link):
11726
+ os.unlink(deploying_link)
11727
+ relative_symlink(
11728
+ self.deploy_dir,
11729
+ deploying_link,
11730
+ target_is_directory=True,
11731
+ make_dirs=True,
11441
11732
  )
11442
11733
 
11443
11734
  #
11444
11735
 
11736
+ for md in [
11737
+ self._deploys.APPS_DEPLOY_DIR,
11738
+ self._deploys.CONFS_DEPLOY_DIR,
11739
+ ]:
11740
+ os.makedirs(self.render_deploy_path(md))
11741
+
11742
+ #
11743
+
11445
11744
  for app in self._spec.apps:
11446
- app_tags = deploy_tags.add(
11447
- app.app,
11448
- app.key(),
11449
- DeployAppRev(app.git.rev),
11450
- )
11745
+ await self.drive_app_deploy(app)
11746
+
11747
+ #
11748
+
11749
+ current_link = self.render_deploy_path(self._deploys.CURRENT_DEPLOY_LINK)
11750
+ os.replace(deploying_link, current_link)
11751
+
11752
+ #
11753
+
11754
+ await self._systemd.sync_systemd(
11755
+ self._spec.systemd,
11756
+ self._home,
11757
+ os.path.join(self.deploy_dir, 'conf', 'systemd'), # FIXME
11758
+ )
11759
+
11760
+ #
11451
11761
 
11452
- await self._apps.prepare_app(
11453
- app,
11454
- self._home,
11762
+ async def drive_app_deploy(self, app: DeployAppSpec) -> None:
11763
+ app_tags = self.deploy_tags.add(
11764
+ app.app,
11765
+ app.key(),
11766
+ DeployAppRev(app.git.rev),
11767
+ )
11768
+
11769
+ #
11770
+
11771
+ da = await self._apps.prepare_app(
11772
+ app,
11773
+ self._home,
11774
+ app_tags,
11775
+ )
11776
+
11777
+ #
11778
+
11779
+ app_link = self.render_deploy_path(self._deploys.APP_DEPLOY_LINK, app_tags)
11780
+ relative_symlink(
11781
+ da.app_dir,
11782
+ app_link,
11783
+ target_is_directory=True,
11784
+ make_dirs=True,
11785
+ )
11786
+
11787
+ #
11788
+
11789
+ deploy_conf_dir = self.render_deploy_path(self._deploys.CONFS_DEPLOY_DIR)
11790
+ if app.conf is not None:
11791
+ await self._conf.link_app_conf(
11792
+ app.conf,
11455
11793
  app_tags,
11794
+ check.non_empty_str(da.conf_dir),
11795
+ deploy_conf_dir,
11456
11796
  )
11457
11797
 
11458
11798
 
@@ -11558,13 +11898,10 @@ def bind_deploy(
11558
11898
 
11559
11899
  lst.extend([
11560
11900
  bind_deploy_manager(DeployAppManager),
11561
-
11562
11901
  bind_deploy_manager(DeployGitManager),
11563
-
11564
11902
  bind_deploy_manager(DeployManager),
11565
-
11903
+ bind_deploy_manager(DeploySystemdManager),
11566
11904
  bind_deploy_manager(DeployTmpManager),
11567
-
11568
11905
  bind_deploy_manager(DeployVenvManager),
11569
11906
  ])
11570
11907