ominfra 0.0.0.dev189__py3-none-any.whl → 0.0.0.dev191__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
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