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.
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
 
@@ -7035,7 +7089,7 @@ DEPLOY_TAG_ILLEGAL_STRS: ta.AbstractSet[str] = frozenset([
7035
7089
  ##
7036
7090
 
7037
7091
 
7038
- @dc.dataclass(frozen=True)
7092
+ @dc.dataclass(frozen=True, order=True)
7039
7093
  class DeployTag(abc.ABC): # noqa
7040
7094
  s: str
7041
7095
 
@@ -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
@@ -9160,16 +9268,63 @@ TODO:
9160
9268
  """
9161
9269
 
9162
9270
 
9271
+ ##
9272
+
9273
+
9163
9274
  class DeployConfManager:
9164
- def _render_app_conf_content(self, ac: DeployAppConfContent) -> str:
9275
+ def _process_conf_content(
9276
+ self,
9277
+ content: T,
9278
+ *,
9279
+ str_processor: ta.Optional[ta.Callable[[str], str]] = None,
9280
+ ) -> T:
9281
+ def rec(o):
9282
+ if isinstance(o, str):
9283
+ if str_processor is not None:
9284
+ return type(o)(str_processor(o))
9285
+
9286
+ elif isinstance(o, collections.abc.Mapping):
9287
+ return type(o)([ # type: ignore
9288
+ (rec(k), rec(v))
9289
+ for k, v in o.items()
9290
+ ])
9291
+
9292
+ elif isinstance(o, collections.abc.Iterable):
9293
+ return type(o)([ # type: ignore
9294
+ rec(e) for e in o
9295
+ ])
9296
+
9297
+ return o
9298
+
9299
+ return rec(content)
9300
+
9301
+ #
9302
+
9303
+ def _render_app_conf_content(
9304
+ self,
9305
+ ac: DeployAppConfContent,
9306
+ *,
9307
+ str_processor: ta.Optional[ta.Callable[[str], str]] = None,
9308
+ ) -> str:
9309
+ pcc = functools.partial(
9310
+ self._process_conf_content,
9311
+ str_processor=str_processor,
9312
+ )
9313
+
9165
9314
  if isinstance(ac, RawDeployAppConfContent):
9166
- return ac.body
9315
+ return pcc(ac.body)
9167
9316
 
9168
9317
  elif isinstance(ac, JsonDeployAppConfContent):
9169
- return strip_with_newline(json_dumps_pretty(ac.obj))
9318
+ json_obj = pcc(ac.obj)
9319
+ return strip_with_newline(json_dumps_pretty(json_obj))
9170
9320
 
9171
9321
  elif isinstance(ac, IniDeployAppConfContent):
9172
- return strip_with_newline(render_ini_config(ac.sections))
9322
+ ini_sections = pcc(ac.sections)
9323
+ return strip_with_newline(render_ini_config(ini_sections))
9324
+
9325
+ elif isinstance(ac, NginxDeployAppConfContent):
9326
+ nginx_items = NginxConfigItems.of(pcc(ac.items))
9327
+ return strip_with_newline(render_nginx_config_str(nginx_items))
9173
9328
 
9174
9329
  else:
9175
9330
  raise TypeError(ac)
@@ -9178,17 +9333,37 @@ class DeployConfManager:
9178
9333
  self,
9179
9334
  acf: DeployAppConfFile,
9180
9335
  app_conf_dir: str,
9336
+ *,
9337
+ str_processor: ta.Optional[ta.Callable[[str], str]] = None,
9181
9338
  ) -> None:
9182
9339
  conf_file = os.path.join(app_conf_dir, acf.path)
9183
9340
  check.arg(is_path_in_dir(app_conf_dir, conf_file))
9184
9341
 
9185
- body = self._render_app_conf_content(acf.content)
9342
+ body = self._render_app_conf_content(
9343
+ acf.content,
9344
+ str_processor=str_processor,
9345
+ )
9186
9346
 
9187
9347
  os.makedirs(os.path.dirname(conf_file), exist_ok=True)
9188
9348
 
9189
9349
  with open(conf_file, 'w') as f: # noqa
9190
9350
  f.write(body)
9191
9351
 
9352
+ async def write_app_conf(
9353
+ self,
9354
+ spec: DeployAppConfSpec,
9355
+ app_conf_dir: str,
9356
+ ) -> None:
9357
+ def process_str(s: str) -> str:
9358
+ return s
9359
+
9360
+ for acf in spec.files or []:
9361
+ await self._write_app_conf_file(
9362
+ acf,
9363
+ app_conf_dir,
9364
+ str_processor=process_str,
9365
+ )
9366
+
9192
9367
  #
9193
9368
 
9194
9369
  class _ComputedConfLink(ta.NamedTuple):
@@ -9298,23 +9473,13 @@ class DeployConfManager:
9298
9473
  make_dirs=True,
9299
9474
  )
9300
9475
 
9301
- #
9302
-
9303
- async def write_app_conf(
9476
+ async def link_app_conf(
9304
9477
  self,
9305
9478
  spec: DeployAppConfSpec,
9306
9479
  tags: DeployTagMap,
9307
9480
  app_conf_dir: str,
9308
9481
  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
-
9482
+ ):
9318
9483
  for link in spec.links or []:
9319
9484
  await self._make_app_conf_link(
9320
9485
  link,
@@ -9439,6 +9604,22 @@ class DeployAppSpec(DeploySpecKeyed[DeployAppKey]):
9439
9604
  return DeployAppKey(self._key_str())
9440
9605
 
9441
9606
 
9607
+ @dc.dataclass(frozen=True)
9608
+ class DeployAppLinksSpec:
9609
+ apps: ta.Sequence[DeployApp] = ()
9610
+
9611
+ exclude_unspecified: bool = False
9612
+
9613
+
9614
+ ##
9615
+
9616
+
9617
+ @dc.dataclass(frozen=True)
9618
+ class DeploySystemdSpec:
9619
+ # ~/.config/systemd/user/
9620
+ unit_dir: ta.Optional[str] = None
9621
+
9622
+
9442
9623
  ##
9443
9624
 
9444
9625
 
@@ -9446,7 +9627,11 @@ class DeployAppSpec(DeploySpecKeyed[DeployAppKey]):
9446
9627
  class DeploySpec(DeploySpecKeyed[DeployKey]):
9447
9628
  home: DeployHome
9448
9629
 
9449
- apps: ta.Sequence[DeployAppSpec]
9630
+ apps: ta.Sequence[DeployAppSpec] = ()
9631
+
9632
+ app_links: DeployAppLinksSpec = DeployAppLinksSpec()
9633
+
9634
+ systemd: ta.Optional[DeploySystemdSpec] = None
9450
9635
 
9451
9636
  def __post_init__(self) -> None:
9452
9637
  check.non_empty_str(self.home)
@@ -10848,6 +11033,128 @@ def bind_deploy_paths() -> InjectorBindings:
10848
11033
  return inj.as_bindings(*lst)
10849
11034
 
10850
11035
 
11036
+ ########################################
11037
+ # ../deploy/systemd.py
11038
+ """
11039
+ TODO:
11040
+ - verify - systemd-analyze
11041
+ - sudo loginctl enable-linger "$USER"
11042
+ - idemp kill services that shouldn't be running, start ones that should
11043
+ - ideally only those defined by links to deploy home
11044
+ - ominfra.systemd / x.sd_orphans
11045
+ """
11046
+
11047
+
11048
+ class DeploySystemdManager:
11049
+ def __init__(
11050
+ self,
11051
+ *,
11052
+ atomics: DeployHomeAtomics,
11053
+ ) -> None:
11054
+ super().__init__()
11055
+
11056
+ self._atomics = atomics
11057
+
11058
+ def _scan_link_dir(
11059
+ self,
11060
+ d: str,
11061
+ *,
11062
+ strict: bool = False,
11063
+ ) -> ta.Dict[str, str]:
11064
+ o: ta.Dict[str, str] = {}
11065
+ for f in os.listdir(d):
11066
+ fp = os.path.join(d, f)
11067
+ if strict:
11068
+ check.state(os.path.islink(fp))
11069
+ o[f] = abs_real_path(fp)
11070
+ return o
11071
+
11072
+ async def sync_systemd(
11073
+ self,
11074
+ spec: ta.Optional[DeploySystemdSpec],
11075
+ home: DeployHome,
11076
+ conf_dir: str,
11077
+ ) -> None:
11078
+ check.non_empty_str(home)
11079
+
11080
+ if not spec:
11081
+ return
11082
+
11083
+ #
11084
+
11085
+ if not (ud := spec.unit_dir):
11086
+ return
11087
+
11088
+ ud = abs_real_path(os.path.expanduser(ud))
11089
+
11090
+ os.makedirs(ud, exist_ok=True)
11091
+
11092
+ #
11093
+
11094
+ uld = {
11095
+ n: p
11096
+ for n, p in self._scan_link_dir(ud).items()
11097
+ if is_path_in_dir(home, p)
11098
+ }
11099
+
11100
+ if os.path.exists(conf_dir):
11101
+ cld = self._scan_link_dir(conf_dir, strict=True)
11102
+ else:
11103
+ cld = {}
11104
+
11105
+ #
11106
+
11107
+ ns = sorted(set(uld) | set(cld))
11108
+
11109
+ for n in ns:
11110
+ cl = cld.get(n)
11111
+ if cl is None:
11112
+ os.unlink(os.path.join(ud, n))
11113
+ else:
11114
+ with self._atomics(home).begin_atomic_path_swap( # noqa
11115
+ 'file',
11116
+ os.path.join(ud, n),
11117
+ auto_commit=True,
11118
+ skip_root_dir_check=True,
11119
+ ) as dst_swap:
11120
+ os.unlink(dst_swap.tmp_path)
11121
+ os.symlink(
11122
+ os.path.relpath(cl, os.path.dirname(dst_swap.dst_path)),
11123
+ dst_swap.tmp_path,
11124
+ )
11125
+
11126
+ #
11127
+
11128
+ if sys.platform == 'linux':
11129
+ async def reload() -> None:
11130
+ await asyncio_subprocesses.check_call('systemctl', '--user', 'daemon-reload')
11131
+
11132
+ await reload()
11133
+
11134
+ num_deleted = 0
11135
+ for n in ns:
11136
+ if n.endswith('.service'):
11137
+ cl = cld.get(n)
11138
+ ul = uld.get(n)
11139
+ if cl is not None:
11140
+ if ul is None:
11141
+ cs = ['enable', 'start']
11142
+ else:
11143
+ cs = ['restart']
11144
+ else: # noqa
11145
+ if ul is not None:
11146
+ cs = ['stop']
11147
+ num_deleted += 1
11148
+ else:
11149
+ cs = []
11150
+
11151
+ for c in cs:
11152
+ await asyncio_subprocesses.check_call('systemctl', '--user', c, n)
11153
+
11154
+ if num_deleted:
11155
+ await reload()
11156
+
11157
+
10851
11158
  ########################################
10852
11159
  # ../remote/inject.py
10853
11160
 
@@ -11202,7 +11509,6 @@ class DeployVenvManager:
11202
11509
  async def setup_venv(
11203
11510
  self,
11204
11511
  spec: DeployVenvSpec,
11205
- home: DeployHome,
11206
11512
  git_dir: str,
11207
11513
  venv_dir: str,
11208
11514
  ) -> None:
@@ -11229,12 +11535,15 @@ class DeployVenvManager:
11229
11535
 
11230
11536
  if os.path.isfile(reqs_txt):
11231
11537
  if spec.use_uv:
11232
- await asyncio_subprocesses.check_call(venv_exe, '-m', 'pip', 'install', 'uv')
11233
- pip_cmd = ['-m', 'uv', 'pip']
11538
+ if shutil.which('uv') is not None:
11539
+ pip_cmd = ['uv', 'pip']
11540
+ else:
11541
+ await asyncio_subprocesses.check_call(venv_exe, '-m', 'pip', 'install', 'uv')
11542
+ pip_cmd = [venv_exe, '-m', 'uv', 'pip']
11234
11543
  else:
11235
- pip_cmd = ['-m', 'pip']
11544
+ pip_cmd = [venv_exe, '-m', 'pip']
11236
11545
 
11237
- await asyncio_subprocesses.check_call(venv_exe, *pip_cmd,'install', '-r', reqs_txt)
11546
+ await asyncio_subprocesses.check_call(*pip_cmd, 'install', '-r', reqs_txt, cwd=venv_dir)
11238
11547
 
11239
11548
 
11240
11549
  ########################################
@@ -11245,39 +11554,31 @@ class DeployAppManager(DeployPathOwner):
11245
11554
  def __init__(
11246
11555
  self,
11247
11556
  *,
11248
- conf: DeployConfManager,
11249
11557
  git: DeployGitManager,
11250
11558
  venvs: DeployVenvManager,
11559
+ conf: DeployConfManager,
11560
+
11561
+ msh: ObjMarshalerManager,
11251
11562
  ) -> None:
11252
11563
  super().__init__()
11253
11564
 
11254
- self._conf = conf
11255
11565
  self._git = git
11256
11566
  self._venvs = venvs
11567
+ self._conf = conf
11257
11568
 
11258
- #
11259
-
11260
- _APP_DIR_STR = 'apps/@app/@time--@app-rev--@app-key/'
11261
- _APP_DIR = DeployPath.parse(_APP_DIR_STR)
11569
+ self._msh = msh
11262
11570
 
11263
- _DEPLOY_DIR_STR = 'deploys/@time--@deploy-key/'
11264
- _DEPLOY_DIR = DeployPath.parse(_DEPLOY_DIR_STR)
11571
+ #
11265
11572
 
11266
- _APP_DEPLOY_LINK = DeployPath.parse(f'{_DEPLOY_DIR_STR}apps/@app')
11267
- _CONF_DEPLOY_DIR = DeployPath.parse(f'{_DEPLOY_DIR_STR}conf/@conf/')
11573
+ APP_DIR = DeployPath.parse('apps/@app/@time--@app-rev--@app-key/')
11268
11574
 
11269
11575
  @cached_nullary
11270
11576
  def get_owned_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
11271
11577
  return {
11272
- self._APP_DIR,
11273
-
11274
- self._DEPLOY_DIR,
11275
-
11276
- self._APP_DEPLOY_LINK,
11277
- self._CONF_DEPLOY_DIR,
11578
+ self.APP_DIR,
11278
11579
 
11279
11580
  *[
11280
- DeployPath.parse(f'{self._APP_DIR_STR}{sfx}/')
11581
+ DeployPath.parse(f'{self.APP_DIR}{sfx}/')
11281
11582
  for sfx in [
11282
11583
  'conf',
11283
11584
  'git',
@@ -11288,122 +11589,244 @@ class DeployAppManager(DeployPathOwner):
11288
11589
 
11289
11590
  #
11290
11591
 
11592
+ def _make_tags(self, spec: DeployAppSpec) -> DeployTagMap:
11593
+ return DeployTagMap(
11594
+ spec.app,
11595
+ spec.key(),
11596
+ DeployAppRev(spec.git.rev),
11597
+ )
11598
+
11599
+ #
11600
+
11601
+ @dc.dataclass(frozen=True)
11602
+ class PreparedApp:
11603
+ spec: DeployAppSpec
11604
+ tags: DeployTagMap
11605
+
11606
+ dir: str
11607
+
11608
+ git_dir: ta.Optional[str] = None
11609
+ venv_dir: ta.Optional[str] = None
11610
+ conf_dir: ta.Optional[str] = None
11611
+
11291
11612
  async def prepare_app(
11292
11613
  self,
11293
11614
  spec: DeployAppSpec,
11294
11615
  home: DeployHome,
11295
11616
  tags: DeployTagMap,
11296
- ) -> None:
11297
- check.non_empty_str(home)
11617
+ ) -> PreparedApp:
11618
+ spec_json = json_dumps_pretty(self._msh.marshal_obj(spec))
11298
11619
 
11299
- def build_path(pth: DeployPath) -> str:
11300
- return os.path.join(home, pth.render(tags))
11620
+ #
11301
11621
 
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)
11622
+ app_tags = tags.add(*self._make_tags(spec))
11305
11623
 
11306
11624
  #
11307
11625
 
11308
- os.makedirs(deploy_dir, exist_ok=True)
11309
-
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
- )
11626
+ check.non_empty_str(home)
11319
11627
 
11320
- #
11628
+ app_dir = os.path.join(home, self.APP_DIR.render(app_tags))
11321
11629
 
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
- )
11630
+ os.makedirs(app_dir, exist_ok=True)
11329
11631
 
11330
11632
  #
11331
11633
 
11332
- deploy_conf_dir = os.path.join(deploy_dir, 'conf')
11333
- os.makedirs(deploy_conf_dir, exist_ok=True)
11634
+ rkw: ta.Dict[str, ta.Any] = dict(
11635
+ spec=spec,
11636
+ tags=app_tags,
11334
11637
 
11335
- #
11638
+ dir=app_dir,
11639
+ )
11336
11640
 
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
11641
  #
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
- # )
11642
+
11643
+ spec_file = os.path.join(app_dir, 'spec.json')
11644
+ with open(spec_file, 'w') as f: # noqa
11645
+ f.write(spec_json)
11368
11646
 
11369
11647
  #
11370
11648
 
11371
- app_git_dir = os.path.join(app_dir, 'git')
11649
+ git_dir = os.path.join(app_dir, 'git')
11650
+ rkw.update(git_dir=git_dir)
11372
11651
  await self._git.checkout(
11373
11652
  spec.git,
11374
11653
  home,
11375
- app_git_dir,
11654
+ git_dir,
11376
11655
  )
11377
11656
 
11378
11657
  #
11379
11658
 
11380
11659
  if spec.venv is not None:
11381
- app_venv_dir = os.path.join(app_dir, 'venv')
11660
+ venv_dir = os.path.join(app_dir, 'venv')
11661
+ rkw.update(venv_dir=venv_dir)
11382
11662
  await self._venvs.setup_venv(
11383
11663
  spec.venv,
11384
- home,
11385
- app_git_dir,
11386
- app_venv_dir,
11664
+ git_dir,
11665
+ venv_dir,
11387
11666
  )
11388
11667
 
11389
11668
  #
11390
11669
 
11391
11670
  if spec.conf is not None:
11392
- app_conf_dir = os.path.join(app_dir, 'conf')
11671
+ conf_dir = os.path.join(app_dir, 'conf')
11672
+ rkw.update(conf_dir=conf_dir)
11393
11673
  await self._conf.write_app_conf(
11394
11674
  spec.conf,
11395
- tags,
11396
- app_conf_dir,
11397
- deploy_conf_dir,
11675
+ conf_dir,
11398
11676
  )
11399
11677
 
11400
11678
  #
11401
11679
 
11402
- os.replace(deploying_link, current_link)
11680
+ return DeployAppManager.PreparedApp(**rkw)
11681
+
11682
+ async def prepare_app_link(
11683
+ self,
11684
+ tags: DeployTagMap,
11685
+ app_dir: str,
11686
+ ) -> PreparedApp:
11687
+ spec_file = os.path.join(app_dir, 'spec.json')
11688
+ with open(spec_file) as f: # noqa
11689
+ spec_json = f.read()
11690
+
11691
+ spec: DeployAppSpec = self._msh.unmarshal_obj(json.loads(spec_json), DeployAppSpec)
11692
+
11693
+ #
11694
+
11695
+ app_tags = tags.add(*self._make_tags(spec))
11696
+
11697
+ #
11698
+
11699
+ rkw: ta.Dict[str, ta.Any] = dict(
11700
+ spec=spec,
11701
+ tags=app_tags,
11702
+
11703
+ dir=app_dir,
11704
+ )
11705
+
11706
+ #
11707
+
11708
+ git_dir = os.path.join(app_dir, 'git')
11709
+ check.state(os.path.isdir(git_dir))
11710
+ rkw.update(git_dir=git_dir)
11711
+
11712
+ #
11713
+
11714
+ if spec.venv is not None:
11715
+ venv_dir = os.path.join(app_dir, 'venv')
11716
+ check.state(os.path.isdir(venv_dir))
11717
+ rkw.update(venv_dir=venv_dir)
11718
+
11719
+ #
11720
+
11721
+ if spec.conf is not None:
11722
+ conf_dir = os.path.join(app_dir, 'conf')
11723
+ check.state(os.path.isdir(conf_dir))
11724
+ rkw.update(conf_dir=conf_dir)
11725
+
11726
+ #
11727
+
11728
+ return DeployAppManager.PreparedApp(**rkw)
11403
11729
 
11404
11730
 
11405
11731
  ########################################
11406
- # ../deploy/driver.py
11732
+ # ../deploy/deploy.py
11733
+
11734
+
11735
+ ##
11736
+
11737
+
11738
+ DEPLOY_TAG_DATETIME_FMT = '%Y%m%dT%H%M%SZ'
11739
+
11740
+
11741
+ DeployManagerUtcClock = ta.NewType('DeployManagerUtcClock', Func0[datetime.datetime])
11742
+
11743
+
11744
+ class DeployManager(DeployPathOwner):
11745
+ def __init__(
11746
+ self,
11747
+ *,
11748
+ atomics: DeployHomeAtomics,
11749
+
11750
+ utc_clock: ta.Optional[DeployManagerUtcClock] = None,
11751
+ ):
11752
+ super().__init__()
11753
+
11754
+ self._atomics = atomics
11755
+
11756
+ self._utc_clock = utc_clock
11757
+
11758
+ #
11759
+
11760
+ # Home current link just points to CURRENT_DEPLOY_LINK, and is intended for user convenience.
11761
+ HOME_CURRENT_LINK = DeployPath.parse('current')
11762
+
11763
+ DEPLOYS_DIR = DeployPath.parse('deploys/')
11764
+
11765
+ # Authoritative current symlink is not in deploy-home, just to prevent accidental corruption.
11766
+ CURRENT_DEPLOY_LINK = DeployPath.parse(f'{DEPLOYS_DIR}current')
11767
+ DEPLOYING_DEPLOY_LINK = DeployPath.parse(f'{DEPLOYS_DIR}deploying')
11768
+
11769
+ DEPLOY_DIR = DeployPath.parse(f'{DEPLOYS_DIR}@time--@deploy-key/')
11770
+ DEPLOY_SPEC_FILE = DeployPath.parse(f'{DEPLOY_DIR}spec.json')
11771
+
11772
+ APPS_DEPLOY_DIR = DeployPath.parse(f'{DEPLOY_DIR}apps/')
11773
+ APP_DEPLOY_LINK = DeployPath.parse(f'{APPS_DEPLOY_DIR}@app')
11774
+
11775
+ CONFS_DEPLOY_DIR = DeployPath.parse(f'{DEPLOY_DIR}conf/')
11776
+ CONF_DEPLOY_DIR = DeployPath.parse(f'{CONFS_DEPLOY_DIR}@conf/')
11777
+
11778
+ @cached_nullary
11779
+ def get_owned_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
11780
+ return {
11781
+ self.DEPLOYS_DIR,
11782
+
11783
+ self.CURRENT_DEPLOY_LINK,
11784
+ self.DEPLOYING_DEPLOY_LINK,
11785
+
11786
+ self.DEPLOY_DIR,
11787
+ self.DEPLOY_SPEC_FILE,
11788
+
11789
+ self.APPS_DEPLOY_DIR,
11790
+ self.APP_DEPLOY_LINK,
11791
+
11792
+ self.CONFS_DEPLOY_DIR,
11793
+ self.CONF_DEPLOY_DIR,
11794
+ }
11795
+
11796
+ #
11797
+
11798
+ def render_path(self, home: DeployHome, pth: DeployPath, tags: ta.Optional[DeployTagMap] = None) -> str:
11799
+ return os.path.join(check.non_empty_str(home), pth.render(tags))
11800
+
11801
+ #
11802
+
11803
+ def _utc_now(self) -> datetime.datetime:
11804
+ if self._utc_clock is not None:
11805
+ return self._utc_clock() # noqa
11806
+ else:
11807
+ return datetime.datetime.now(tz=datetime.timezone.utc) # noqa
11808
+
11809
+ def make_deploy_time(self) -> DeployTime:
11810
+ return DeployTime(self._utc_now().strftime(DEPLOY_TAG_DATETIME_FMT))
11811
+
11812
+ #
11813
+
11814
+ def make_home_current_link(self, home: DeployHome) -> None:
11815
+ home_current_link = os.path.join(check.non_empty_str(home), self.HOME_CURRENT_LINK.render())
11816
+ current_deploy_link = os.path.join(check.non_empty_str(home), self.CURRENT_DEPLOY_LINK.render())
11817
+ with self._atomics(home).begin_atomic_path_swap( # noqa
11818
+ 'file',
11819
+ home_current_link,
11820
+ auto_commit=True,
11821
+ ) as dst_swap:
11822
+ os.unlink(dst_swap.tmp_path)
11823
+ os.symlink(
11824
+ os.path.relpath(current_deploy_link, os.path.dirname(dst_swap.dst_path)),
11825
+ dst_swap.tmp_path,
11826
+ )
11827
+
11828
+
11829
+ ##
11407
11830
 
11408
11831
 
11409
11832
  class DeployDriverFactory(Func1[DeploySpec, ta.ContextManager['DeployDriver']]):
@@ -11418,8 +11841,13 @@ class DeployDriver:
11418
11841
  home: DeployHome,
11419
11842
  time: DeployTime,
11420
11843
 
11844
+ deploys: DeployManager,
11421
11845
  paths: DeployPathsManager,
11422
11846
  apps: DeployAppManager,
11847
+ conf: DeployConfManager,
11848
+ systemd: DeploySystemdManager,
11849
+
11850
+ msh: ObjMarshalerManager,
11423
11851
  ) -> None:
11424
11852
  super().__init__()
11425
11853
 
@@ -11427,32 +11855,180 @@ class DeployDriver:
11427
11855
  self._home = home
11428
11856
  self._time = time
11429
11857
 
11858
+ self._deploys = deploys
11430
11859
  self._paths = paths
11431
11860
  self._apps = apps
11861
+ self._conf = conf
11862
+ self._systemd = systemd
11863
+
11864
+ self._msh = msh
11865
+
11866
+ #
11867
+
11868
+ @property
11869
+ def tags(self) -> DeployTagMap:
11870
+ return DeployTagMap(
11871
+ self._time,
11872
+ self._spec.key(),
11873
+ )
11874
+
11875
+ def render_path(self, pth: DeployPath, tags: ta.Optional[DeployTagMap] = None) -> str:
11876
+ return os.path.join(self._home, pth.render(tags if tags is not None else self.tags))
11877
+
11878
+ @property
11879
+ def dir(self) -> str:
11880
+ return self.render_path(self._deploys.DEPLOY_DIR)
11881
+
11882
+ #
11432
11883
 
11433
11884
  async def drive_deploy(self) -> None:
11885
+ spec_json = json_dumps_pretty(self._msh.marshal_obj(self._spec))
11886
+
11887
+ #
11888
+
11889
+ das: ta.Set[DeployApp] = {a.app for a in self._spec.apps}
11890
+ las: ta.Set[DeployApp] = set(self._spec.app_links.apps)
11891
+ if (ras := das & las):
11892
+ raise RuntimeError(f'Must not specify apps as both deploy and link: {sorted(a.s for a in ras)}')
11893
+
11894
+ #
11895
+
11434
11896
  self._paths.validate_deploy_paths()
11435
11897
 
11436
11898
  #
11437
11899
 
11438
- deploy_tags = DeployTagMap(
11439
- self._time,
11440
- self._spec.key(),
11900
+ os.makedirs(self.dir)
11901
+
11902
+ #
11903
+
11904
+ spec_file = self.render_path(self._deploys.DEPLOY_SPEC_FILE)
11905
+ with open(spec_file, 'w') as f: # noqa
11906
+ f.write(spec_json)
11907
+
11908
+ #
11909
+
11910
+ deploying_link = self.render_path(self._deploys.DEPLOYING_DEPLOY_LINK)
11911
+ current_link = self.render_path(self._deploys.CURRENT_DEPLOY_LINK)
11912
+
11913
+ #
11914
+
11915
+ if os.path.exists(deploying_link):
11916
+ os.unlink(deploying_link)
11917
+ relative_symlink(
11918
+ self.dir,
11919
+ deploying_link,
11920
+ target_is_directory=True,
11921
+ make_dirs=True,
11441
11922
  )
11442
11923
 
11443
11924
  #
11444
11925
 
11445
- for app in self._spec.apps:
11446
- app_tags = deploy_tags.add(
11447
- app.app,
11448
- app.key(),
11449
- DeployAppRev(app.git.rev),
11926
+ for md in [
11927
+ self._deploys.APPS_DEPLOY_DIR,
11928
+ self._deploys.CONFS_DEPLOY_DIR,
11929
+ ]:
11930
+ os.makedirs(self.render_path(md))
11931
+
11932
+ #
11933
+
11934
+ if not self._spec.app_links.exclude_unspecified:
11935
+ cad = abs_real_path(os.path.join(current_link, 'apps'))
11936
+ if os.path.exists(cad):
11937
+ for d in os.listdir(cad):
11938
+ if (da := DeployApp(d)) not in das:
11939
+ las.add(da)
11940
+
11941
+ for la in self._spec.app_links.apps:
11942
+ await self._drive_app_link(
11943
+ la,
11944
+ current_link,
11450
11945
  )
11451
11946
 
11452
- await self._apps.prepare_app(
11947
+ for app in self._spec.apps:
11948
+ await self._drive_app_deploy(
11453
11949
  app,
11454
- self._home,
11455
- app_tags,
11950
+ )
11951
+
11952
+ #
11953
+
11954
+ os.replace(deploying_link, current_link)
11955
+
11956
+ #
11957
+
11958
+ await self._systemd.sync_systemd(
11959
+ self._spec.systemd,
11960
+ self._home,
11961
+ os.path.join(self.dir, 'conf', 'systemd'), # FIXME
11962
+ )
11963
+
11964
+ #
11965
+
11966
+ self._deploys.make_home_current_link(self._home)
11967
+
11968
+ #
11969
+
11970
+ async def _drive_app_deploy(self, app: DeployAppSpec) -> None:
11971
+ pa = await self._apps.prepare_app(
11972
+ app,
11973
+ self._home,
11974
+ self.tags,
11975
+ )
11976
+
11977
+ #
11978
+
11979
+ app_link = self.render_path(self._deploys.APP_DEPLOY_LINK, pa.tags)
11980
+ relative_symlink(
11981
+ pa.dir,
11982
+ app_link,
11983
+ target_is_directory=True,
11984
+ make_dirs=True,
11985
+ )
11986
+
11987
+ #
11988
+
11989
+ await self._drive_app_configure(pa)
11990
+
11991
+ async def _drive_app_link(
11992
+ self,
11993
+ app: DeployApp,
11994
+ current_link: str,
11995
+ ) -> None:
11996
+ app_link = os.path.join(abs_real_path(current_link), 'apps', app.s)
11997
+ check.state(os.path.islink(app_link))
11998
+
11999
+ app_dir = abs_real_path(app_link)
12000
+ check.state(os.path.isdir(app_dir))
12001
+
12002
+ #
12003
+
12004
+ pa = await self._apps.prepare_app_link(
12005
+ self.tags,
12006
+ app_dir,
12007
+ )
12008
+
12009
+ #
12010
+
12011
+ relative_symlink(
12012
+ app_dir,
12013
+ os.path.join(self.dir, 'apps', app.s),
12014
+ target_is_directory=True,
12015
+ )
12016
+
12017
+ #
12018
+
12019
+ await self._drive_app_configure(pa)
12020
+
12021
+ async def _drive_app_configure(
12022
+ self,
12023
+ pa: DeployAppManager.PreparedApp,
12024
+ ) -> None:
12025
+ deploy_conf_dir = self.render_path(self._deploys.CONFS_DEPLOY_DIR)
12026
+ if pa.spec.conf is not None:
12027
+ await self._conf.link_app_conf(
12028
+ pa.spec.conf,
12029
+ pa.tags,
12030
+ check.non_empty_str(pa.conf_dir),
12031
+ deploy_conf_dir,
11456
12032
  )
11457
12033
 
11458
12034
 
@@ -11558,13 +12134,10 @@ def bind_deploy(
11558
12134
 
11559
12135
  lst.extend([
11560
12136
  bind_deploy_manager(DeployAppManager),
11561
-
11562
12137
  bind_deploy_manager(DeployGitManager),
11563
-
11564
12138
  bind_deploy_manager(DeployManager),
11565
-
12139
+ bind_deploy_manager(DeploySystemdManager),
11566
12140
  bind_deploy_manager(DeployTmpManager),
11567
-
11568
12141
  bind_deploy_manager(DeployVenvManager),
11569
12142
  ])
11570
12143