ominfra 0.0.0.dev175__py3-none-any.whl → 0.0.0.dev176__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/configs.py +4 -2
- ominfra/manage/commands/marshal.py +1 -1
- ominfra/manage/deploy/apps.py +2 -0
- ominfra/manage/deploy/tags.py +4 -0
- ominfra/manage/deploy/venvs.py +11 -1
- ominfra/manage/main.py +12 -3
- ominfra/scripts/journald2aws.py +40 -5
- ominfra/scripts/manage.py +1422 -1367
- ominfra/scripts/supervisor.py +40 -5
- {ominfra-0.0.0.dev175.dist-info → ominfra-0.0.0.dev176.dist-info}/METADATA +3 -3
- {ominfra-0.0.0.dev175.dist-info → ominfra-0.0.0.dev176.dist-info}/RECORD +15 -15
- {ominfra-0.0.0.dev175.dist-info → ominfra-0.0.0.dev176.dist-info}/LICENSE +0 -0
- {ominfra-0.0.0.dev175.dist-info → ominfra-0.0.0.dev176.dist-info}/WHEEL +0 -0
- {ominfra-0.0.0.dev175.dist-info → ominfra-0.0.0.dev176.dist-info}/entry_points.txt +0 -0
- {ominfra-0.0.0.dev175.dist-info → ominfra-0.0.0.dev176.dist-info}/top_level.txt +0 -0
ominfra/scripts/manage.py
CHANGED
@@ -129,12 +129,12 @@ AtomicPathSwapState = ta.Literal['open', 'committed', 'aborted'] # ta.TypeAlias
|
|
129
129
|
# ../configs.py
|
130
130
|
ConfigMapping = ta.Mapping[str, ta.Any]
|
131
131
|
|
132
|
-
# deploy/specs.py
|
133
|
-
KeyDeployTagT = ta.TypeVar('KeyDeployTagT', bound='KeyDeployTag')
|
134
|
-
|
135
132
|
# ../../omlish/subprocesses.py
|
136
133
|
SubprocessChannelOption = ta.Literal['pipe', 'stdout', 'devnull'] # ta.TypeAlias
|
137
134
|
|
135
|
+
# deploy/specs.py
|
136
|
+
KeyDeployTagT = ta.TypeVar('KeyDeployTagT', bound='KeyDeployTag')
|
137
|
+
|
138
138
|
# system/packages.py
|
139
139
|
SystemPackageOrStr = ta.Union['SystemPackage', str]
|
140
140
|
|
@@ -4251,229 +4251,6 @@ def build_command_name_map(crs: CommandRegistrations) -> CommandNameMap:
|
|
4251
4251
|
return CommandNameMap(dct)
|
4252
4252
|
|
4253
4253
|
|
4254
|
-
########################################
|
4255
|
-
# ../deploy/tags.py
|
4256
|
-
|
4257
|
-
|
4258
|
-
##
|
4259
|
-
|
4260
|
-
|
4261
|
-
DEPLOY_TAG_SIGIL = '@'
|
4262
|
-
|
4263
|
-
DEPLOY_TAG_SEPARATOR = '--'
|
4264
|
-
|
4265
|
-
DEPLOY_TAG_DELIMITERS: ta.AbstractSet[str] = frozenset([
|
4266
|
-
DEPLOY_TAG_SEPARATOR,
|
4267
|
-
'.',
|
4268
|
-
])
|
4269
|
-
|
4270
|
-
DEPLOY_TAG_ILLEGAL_STRS: ta.AbstractSet[str] = frozenset([
|
4271
|
-
DEPLOY_TAG_SIGIL,
|
4272
|
-
*DEPLOY_TAG_DELIMITERS,
|
4273
|
-
'/',
|
4274
|
-
])
|
4275
|
-
|
4276
|
-
|
4277
|
-
##
|
4278
|
-
|
4279
|
-
|
4280
|
-
@dc.dataclass(frozen=True)
|
4281
|
-
class DeployTag(abc.ABC): # noqa
|
4282
|
-
s: str
|
4283
|
-
|
4284
|
-
def __post_init__(self) -> None:
|
4285
|
-
check.not_in(abc.ABC, type(self).__bases__)
|
4286
|
-
check.non_empty_str(self.s)
|
4287
|
-
for ch in DEPLOY_TAG_ILLEGAL_STRS:
|
4288
|
-
check.state(ch not in self.s)
|
4289
|
-
|
4290
|
-
#
|
4291
|
-
|
4292
|
-
tag_name: ta.ClassVar[str]
|
4293
|
-
tag_kwarg: ta.ClassVar[str]
|
4294
|
-
|
4295
|
-
def __init_subclass__(cls, **kwargs: ta.Any) -> None:
|
4296
|
-
super().__init_subclass__(**kwargs)
|
4297
|
-
|
4298
|
-
if abc.ABC in cls.__bases__:
|
4299
|
-
return
|
4300
|
-
|
4301
|
-
for b in cls.__bases__:
|
4302
|
-
if issubclass(b, DeployTag):
|
4303
|
-
check.in_(abc.ABC, b.__bases__)
|
4304
|
-
|
4305
|
-
check.non_empty_str(tn := cls.tag_name)
|
4306
|
-
check.equal(tn, tn.lower().strip())
|
4307
|
-
check.not_in('_', tn)
|
4308
|
-
|
4309
|
-
check.state(not hasattr(cls, 'tag_kwarg'))
|
4310
|
-
cls.tag_kwarg = tn.replace('-', '_')
|
4311
|
-
|
4312
|
-
|
4313
|
-
##
|
4314
|
-
|
4315
|
-
|
4316
|
-
_DEPLOY_TAGS: ta.Set[ta.Type[DeployTag]] = set()
|
4317
|
-
DEPLOY_TAGS: ta.AbstractSet[ta.Type[DeployTag]] = _DEPLOY_TAGS
|
4318
|
-
|
4319
|
-
_DEPLOY_TAGS_BY_NAME: ta.Dict[str, ta.Type[DeployTag]] = {}
|
4320
|
-
DEPLOY_TAGS_BY_NAME: ta.Mapping[str, ta.Type[DeployTag]] = _DEPLOY_TAGS_BY_NAME
|
4321
|
-
|
4322
|
-
_DEPLOY_TAGS_BY_KWARG: ta.Dict[str, ta.Type[DeployTag]] = {}
|
4323
|
-
DEPLOY_TAGS_BY_KWARG: ta.Mapping[str, ta.Type[DeployTag]] = _DEPLOY_TAGS_BY_KWARG
|
4324
|
-
|
4325
|
-
|
4326
|
-
def _register_deploy_tag(cls):
|
4327
|
-
check.not_in(cls.tag_name, _DEPLOY_TAGS_BY_NAME)
|
4328
|
-
check.not_in(cls.tag_kwarg, _DEPLOY_TAGS_BY_KWARG)
|
4329
|
-
|
4330
|
-
_DEPLOY_TAGS.add(cls)
|
4331
|
-
_DEPLOY_TAGS_BY_NAME[cls.tag_name] = cls
|
4332
|
-
_DEPLOY_TAGS_BY_KWARG[cls.tag_kwarg] = cls
|
4333
|
-
|
4334
|
-
return cls
|
4335
|
-
|
4336
|
-
|
4337
|
-
##
|
4338
|
-
|
4339
|
-
|
4340
|
-
@_register_deploy_tag
|
4341
|
-
class DeployTime(DeployTag):
|
4342
|
-
tag_name: ta.ClassVar[str] = 'time'
|
4343
|
-
|
4344
|
-
|
4345
|
-
##
|
4346
|
-
|
4347
|
-
|
4348
|
-
class NameDeployTag(DeployTag, abc.ABC): # noqa
|
4349
|
-
pass
|
4350
|
-
|
4351
|
-
|
4352
|
-
@_register_deploy_tag
|
4353
|
-
class DeployApp(NameDeployTag):
|
4354
|
-
tag_name: ta.ClassVar[str] = 'app'
|
4355
|
-
|
4356
|
-
|
4357
|
-
@_register_deploy_tag
|
4358
|
-
class DeployConf(NameDeployTag):
|
4359
|
-
tag_name: ta.ClassVar[str] = 'conf'
|
4360
|
-
|
4361
|
-
|
4362
|
-
##
|
4363
|
-
|
4364
|
-
|
4365
|
-
class KeyDeployTag(DeployTag, abc.ABC): # noqa
|
4366
|
-
pass
|
4367
|
-
|
4368
|
-
|
4369
|
-
@_register_deploy_tag
|
4370
|
-
class DeployKey(KeyDeployTag):
|
4371
|
-
tag_name: ta.ClassVar[str] = 'deploy-key'
|
4372
|
-
|
4373
|
-
|
4374
|
-
@_register_deploy_tag
|
4375
|
-
class DeployAppKey(KeyDeployTag):
|
4376
|
-
tag_name: ta.ClassVar[str] = 'app-key'
|
4377
|
-
|
4378
|
-
|
4379
|
-
##
|
4380
|
-
|
4381
|
-
|
4382
|
-
class RevDeployTag(DeployTag, abc.ABC): # noqa
|
4383
|
-
pass
|
4384
|
-
|
4385
|
-
|
4386
|
-
@_register_deploy_tag
|
4387
|
-
class DeployAppRev(RevDeployTag):
|
4388
|
-
tag_name: ta.ClassVar[str] = 'app-rev'
|
4389
|
-
|
4390
|
-
|
4391
|
-
##
|
4392
|
-
|
4393
|
-
|
4394
|
-
class DeployTagMap:
|
4395
|
-
def __init__(
|
4396
|
-
self,
|
4397
|
-
*args: DeployTag,
|
4398
|
-
**kwargs: str,
|
4399
|
-
) -> None:
|
4400
|
-
super().__init__()
|
4401
|
-
|
4402
|
-
dct: ta.Dict[ta.Type[DeployTag], DeployTag] = {}
|
4403
|
-
|
4404
|
-
for a in args:
|
4405
|
-
c = type(check.isinstance(a, DeployTag))
|
4406
|
-
check.not_in(c, dct)
|
4407
|
-
dct[c] = a
|
4408
|
-
|
4409
|
-
for k, v in kwargs.items():
|
4410
|
-
c = DEPLOY_TAGS_BY_KWARG[k]
|
4411
|
-
check.not_in(c, dct)
|
4412
|
-
dct[c] = c(v)
|
4413
|
-
|
4414
|
-
self._dct = dct
|
4415
|
-
self._tup = tuple(sorted((type(t).tag_kwarg, t.s) for t in dct.values()))
|
4416
|
-
|
4417
|
-
#
|
4418
|
-
|
4419
|
-
def add(self, *args: ta.Any, **kwargs: ta.Any) -> 'DeployTagMap':
|
4420
|
-
return DeployTagMap(
|
4421
|
-
*self,
|
4422
|
-
*args,
|
4423
|
-
**kwargs,
|
4424
|
-
)
|
4425
|
-
|
4426
|
-
def remove(self, *tags_or_names: ta.Union[ta.Type[DeployTag], str]) -> 'DeployTagMap':
|
4427
|
-
dcs = {
|
4428
|
-
check.issubclass(a, DeployTag) if isinstance(a, type) else DEPLOY_TAGS_BY_NAME[a]
|
4429
|
-
for a in tags_or_names
|
4430
|
-
}
|
4431
|
-
|
4432
|
-
return DeployTagMap(*[
|
4433
|
-
t
|
4434
|
-
for t in self._dct.values()
|
4435
|
-
if t not in dcs
|
4436
|
-
])
|
4437
|
-
|
4438
|
-
#
|
4439
|
-
|
4440
|
-
def __repr__(self) -> str:
|
4441
|
-
return f'{self.__class__.__name__}({", ".join(f"{k}={v!r}" for k, v in self._tup)})'
|
4442
|
-
|
4443
|
-
def __hash__(self) -> int:
|
4444
|
-
return hash(self._tup)
|
4445
|
-
|
4446
|
-
def __eq__(self, other: object) -> bool:
|
4447
|
-
if isinstance(other, DeployTagMap):
|
4448
|
-
return self._tup == other._tup
|
4449
|
-
else:
|
4450
|
-
return NotImplemented
|
4451
|
-
|
4452
|
-
#
|
4453
|
-
|
4454
|
-
def __len__(self) -> int:
|
4455
|
-
return len(self._dct)
|
4456
|
-
|
4457
|
-
def __iter__(self) -> ta.Iterator[DeployTag]:
|
4458
|
-
return iter(self._dct.values())
|
4459
|
-
|
4460
|
-
def __getitem__(self, key: ta.Union[ta.Type[DeployTag], str]) -> DeployTag:
|
4461
|
-
if isinstance(key, str):
|
4462
|
-
return self._dct[DEPLOY_TAGS_BY_NAME[key]]
|
4463
|
-
elif isinstance(key, type):
|
4464
|
-
return self._dct[key]
|
4465
|
-
else:
|
4466
|
-
raise TypeError(key)
|
4467
|
-
|
4468
|
-
def __contains__(self, key: ta.Union[ta.Type[DeployTag], str]) -> bool:
|
4469
|
-
if isinstance(key, str):
|
4470
|
-
return DEPLOY_TAGS_BY_NAME[key] in self._dct
|
4471
|
-
elif isinstance(key, type):
|
4472
|
-
return key in self._dct
|
4473
|
-
else:
|
4474
|
-
raise TypeError(key)
|
4475
|
-
|
4476
|
-
|
4477
4254
|
########################################
|
4478
4255
|
# ../remote/config.py
|
4479
4256
|
|
@@ -6135,6 +5912,18 @@ class FieldsObjMarshaler(ObjMarshaler):
|
|
6135
5912
|
})
|
6136
5913
|
|
6137
5914
|
|
5915
|
+
@dc.dataclass(frozen=True)
|
5916
|
+
class SingleFieldObjMarshaler(ObjMarshaler):
|
5917
|
+
ty: type
|
5918
|
+
fld: str
|
5919
|
+
|
5920
|
+
def marshal(self, o: ta.Any, ctx: 'ObjMarshalContext') -> ta.Any:
|
5921
|
+
return getattr(o, self.fld)
|
5922
|
+
|
5923
|
+
def unmarshal(self, o: ta.Any, ctx: 'ObjMarshalContext') -> ta.Any:
|
5924
|
+
return self.ty(**{self.fld: o})
|
5925
|
+
|
5926
|
+
|
6138
5927
|
@dc.dataclass(frozen=True)
|
6139
5928
|
class PolymorphicObjMarshaler(ObjMarshaler):
|
6140
5929
|
class Impl(ta.NamedTuple):
|
@@ -6209,7 +5998,7 @@ _DEFAULT_OBJ_MARSHALERS: ta.Dict[ta.Any, ObjMarshaler] = {
|
|
6209
5998
|
**{t: IterableObjMarshaler(t, DynamicObjMarshaler()) for t in (list, tuple, set, frozenset)},
|
6210
5999
|
**{t: MappingObjMarshaler(t, DynamicObjMarshaler(), DynamicObjMarshaler()) for t in (dict,)},
|
6211
6000
|
|
6212
|
-
|
6001
|
+
**{t: DynamicObjMarshaler() for t in (ta.Any, object)},
|
6213
6002
|
|
6214
6003
|
**{t: DatetimeObjMarshaler(t) for t in (datetime.date, datetime.time, datetime.datetime)},
|
6215
6004
|
decimal.Decimal: DecimalObjMarshaler(),
|
@@ -6234,6 +6023,16 @@ _OBJ_MARSHALER_GENERIC_ITERABLE_TYPES: ta.Dict[ta.Any, type] = {
|
|
6234
6023
|
##
|
6235
6024
|
|
6236
6025
|
|
6026
|
+
_REGISTERED_OBJ_MARSHALERS_BY_TYPE: ta.MutableMapping[type, ObjMarshaler] = weakref.WeakKeyDictionary()
|
6027
|
+
|
6028
|
+
|
6029
|
+
def register_type_obj_marshaler(ty: type, om: ObjMarshaler) -> None:
|
6030
|
+
_REGISTERED_OBJ_MARSHALERS_BY_TYPE[ty] = om
|
6031
|
+
|
6032
|
+
|
6033
|
+
##
|
6034
|
+
|
6035
|
+
|
6237
6036
|
class ObjMarshalerManager:
|
6238
6037
|
def __init__(
|
6239
6038
|
self,
|
@@ -6243,6 +6042,8 @@ class ObjMarshalerManager:
|
|
6243
6042
|
default_obj_marshalers: ta.Dict[ta.Any, ObjMarshaler] = _DEFAULT_OBJ_MARSHALERS, # noqa
|
6244
6043
|
generic_mapping_types: ta.Dict[ta.Any, type] = _OBJ_MARSHALER_GENERIC_MAPPING_TYPES, # noqa
|
6245
6044
|
generic_iterable_types: ta.Dict[ta.Any, type] = _OBJ_MARSHALER_GENERIC_ITERABLE_TYPES, # noqa
|
6045
|
+
|
6046
|
+
registered_obj_marshalers: ta.Mapping[type, ObjMarshaler] = _REGISTERED_OBJ_MARSHALERS_BY_TYPE,
|
6246
6047
|
) -> None:
|
6247
6048
|
super().__init__()
|
6248
6049
|
|
@@ -6251,6 +6052,7 @@ class ObjMarshalerManager:
|
|
6251
6052
|
self._obj_marshalers = dict(default_obj_marshalers)
|
6252
6053
|
self._generic_mapping_types = generic_mapping_types
|
6253
6054
|
self._generic_iterable_types = generic_iterable_types
|
6055
|
+
self._registered_obj_marshalers = registered_obj_marshalers
|
6254
6056
|
|
6255
6057
|
self._lock = threading.RLock()
|
6256
6058
|
self._marshalers: ta.Dict[ta.Any, ObjMarshaler] = dict(_DEFAULT_OBJ_MARSHALERS)
|
@@ -6266,6 +6068,9 @@ class ObjMarshalerManager:
|
|
6266
6068
|
non_strict_fields: bool = False,
|
6267
6069
|
) -> ObjMarshaler:
|
6268
6070
|
if isinstance(ty, type):
|
6071
|
+
if (reg := self._registered_obj_marshalers.get(ty)) is not None:
|
6072
|
+
return reg
|
6073
|
+
|
6269
6074
|
if abc.ABC in ty.__bases__:
|
6270
6075
|
impls = [ity for ity in deep_subclasses(ty) if abc.ABC not in ity.__bases__] # type: ignore
|
6271
6076
|
if all(ity.__qualname__.endswith(ty.__name__) for ity in impls):
|
@@ -6330,9 +6135,15 @@ class ObjMarshalerManager:
|
|
6330
6135
|
|
6331
6136
|
#
|
6332
6137
|
|
6333
|
-
def
|
6138
|
+
def set_obj_marshaler(
|
6139
|
+
self,
|
6140
|
+
ty: ta.Any,
|
6141
|
+
m: ObjMarshaler,
|
6142
|
+
*,
|
6143
|
+
override: bool = False,
|
6144
|
+
) -> None:
|
6334
6145
|
with self._lock:
|
6335
|
-
if ty in self._obj_marshalers:
|
6146
|
+
if not override and ty in self._obj_marshalers:
|
6336
6147
|
raise KeyError(ty)
|
6337
6148
|
self._obj_marshalers[ty] = m
|
6338
6149
|
|
@@ -6423,7 +6234,7 @@ class ObjMarshalContext:
|
|
6423
6234
|
|
6424
6235
|
OBJ_MARSHALER_MANAGER = ObjMarshalerManager()
|
6425
6236
|
|
6426
|
-
|
6237
|
+
set_obj_marshaler = OBJ_MARSHALER_MANAGER.set_obj_marshaler
|
6427
6238
|
get_obj_marshaler = OBJ_MARSHALER_MANAGER.get_obj_marshaler
|
6428
6239
|
|
6429
6240
|
marshal_obj = OBJ_MARSHALER_MANAGER.marshal_obj
|
@@ -6825,6 +6636,7 @@ def read_config_file(
|
|
6825
6636
|
cls: ta.Type[T],
|
6826
6637
|
*,
|
6827
6638
|
prepare: ta.Optional[ta.Callable[[ConfigMapping], ConfigMapping]] = None,
|
6639
|
+
msh: ObjMarshalerManager = OBJ_MARSHALER_MANAGER,
|
6828
6640
|
) -> T:
|
6829
6641
|
with open(path) as cf:
|
6830
6642
|
config_dct = parse_config_file(os.path.basename(path), cf)
|
@@ -6832,7 +6644,7 @@ def read_config_file(
|
|
6832
6644
|
if prepare is not None:
|
6833
6645
|
config_dct = prepare(config_dct)
|
6834
6646
|
|
6835
|
-
return unmarshal_obj(config_dct, cls)
|
6647
|
+
return msh.unmarshal_obj(config_dct, cls)
|
6836
6648
|
|
6837
6649
|
|
6838
6650
|
def build_config_named_children(
|
@@ -6885,7 +6697,7 @@ def install_command_marshaling(
|
|
6885
6697
|
lambda c: c,
|
6886
6698
|
lambda c: c.Output,
|
6887
6699
|
]:
|
6888
|
-
msh.
|
6700
|
+
msh.set_obj_marshaler(
|
6889
6701
|
fn(Command),
|
6890
6702
|
PolymorphicObjMarshaler.of([
|
6891
6703
|
PolymorphicObjMarshaler.Impl(
|
@@ -6927,355 +6739,228 @@ CommandExecutorMap = ta.NewType('CommandExecutorMap', ta.Mapping[ta.Type[Command
|
|
6927
6739
|
|
6928
6740
|
|
6929
6741
|
########################################
|
6930
|
-
# ../deploy/
|
6931
|
-
"""
|
6932
|
-
TODO:
|
6933
|
-
- run/{.pid,.sock}
|
6934
|
-
- logs/...
|
6935
|
-
- current symlink
|
6936
|
-
- conf/{nginx,supervisor}
|
6937
|
-
- env/?
|
6938
|
-
- apps/<app>/shared
|
6939
|
-
"""
|
6742
|
+
# ../deploy/tags.py
|
6940
6743
|
|
6941
6744
|
|
6942
6745
|
##
|
6943
6746
|
|
6944
6747
|
|
6945
|
-
|
6946
|
-
pass
|
6947
|
-
|
6748
|
+
DEPLOY_TAG_SIGIL = '@'
|
6948
6749
|
|
6949
|
-
|
6950
|
-
@abc.abstractmethod
|
6951
|
-
def render(self, tags: ta.Optional[DeployTagMap] = None) -> str:
|
6952
|
-
raise NotImplementedError
|
6750
|
+
DEPLOY_TAG_SEPARATOR = '--'
|
6953
6751
|
|
6752
|
+
DEPLOY_TAG_DELIMITERS: ta.AbstractSet[str] = frozenset([
|
6753
|
+
DEPLOY_TAG_SEPARATOR,
|
6754
|
+
'.',
|
6755
|
+
])
|
6954
6756
|
|
6955
|
-
|
6757
|
+
DEPLOY_TAG_ILLEGAL_STRS: ta.AbstractSet[str] = frozenset([
|
6758
|
+
DEPLOY_TAG_SIGIL,
|
6759
|
+
*DEPLOY_TAG_DELIMITERS,
|
6760
|
+
'/',
|
6761
|
+
])
|
6956
6762
|
|
6957
6763
|
|
6958
|
-
|
6959
|
-
@classmethod
|
6960
|
-
def parse(cls, s: str) -> 'DeployPathNamePart':
|
6961
|
-
check.non_empty_str(s)
|
6962
|
-
if s.startswith(DEPLOY_TAG_SIGIL):
|
6963
|
-
return TagDeployPathNamePart(s[1:])
|
6964
|
-
elif s in DEPLOY_TAG_DELIMITERS:
|
6965
|
-
return DelimiterDeployPathNamePart(s)
|
6966
|
-
else:
|
6967
|
-
return ConstDeployPathNamePart(s)
|
6764
|
+
##
|
6968
6765
|
|
6969
6766
|
|
6970
6767
|
@dc.dataclass(frozen=True)
|
6971
|
-
class
|
6972
|
-
|
6768
|
+
class DeployTag(abc.ABC): # noqa
|
6769
|
+
s: str
|
6973
6770
|
|
6974
6771
|
def __post_init__(self) -> None:
|
6975
|
-
check.
|
6772
|
+
check.not_in(abc.ABC, type(self).__bases__)
|
6773
|
+
check.non_empty_str(self.s)
|
6774
|
+
for ch in DEPLOY_TAG_ILLEGAL_STRS:
|
6775
|
+
check.state(ch not in self.s)
|
6976
6776
|
|
6977
|
-
|
6978
|
-
def tag(self) -> ta.Type[DeployTag]:
|
6979
|
-
return DEPLOY_TAGS_BY_NAME[self.name]
|
6777
|
+
#
|
6980
6778
|
|
6981
|
-
|
6982
|
-
|
6983
|
-
return tags[self.tag].s
|
6984
|
-
else:
|
6985
|
-
return DEPLOY_TAG_SIGIL + self.name
|
6779
|
+
tag_name: ta.ClassVar[str]
|
6780
|
+
tag_kwarg: ta.ClassVar[str]
|
6986
6781
|
|
6782
|
+
def __init_subclass__(cls, **kwargs: ta.Any) -> None:
|
6783
|
+
super().__init_subclass__(**kwargs)
|
6987
6784
|
|
6988
|
-
|
6989
|
-
|
6990
|
-
delimiter: str
|
6785
|
+
if abc.ABC in cls.__bases__:
|
6786
|
+
return
|
6991
6787
|
|
6992
|
-
|
6993
|
-
|
6788
|
+
for b in cls.__bases__:
|
6789
|
+
if issubclass(b, DeployTag):
|
6790
|
+
check.in_(abc.ABC, b.__bases__)
|
6994
6791
|
|
6995
|
-
|
6996
|
-
|
6792
|
+
check.non_empty_str(tn := cls.tag_name)
|
6793
|
+
check.equal(tn, tn.lower().strip())
|
6794
|
+
check.not_in('_', tn)
|
6997
6795
|
|
6796
|
+
check.state(not hasattr(cls, 'tag_kwarg'))
|
6797
|
+
cls.tag_kwarg = tn.replace('-', '_')
|
6998
6798
|
|
6999
|
-
@dc.dataclass(frozen=True)
|
7000
|
-
class ConstDeployPathNamePart(DeployPathNamePart):
|
7001
|
-
const: str
|
7002
6799
|
|
7003
|
-
|
7004
|
-
check.non_empty_str(self.const)
|
7005
|
-
for c in [*DEPLOY_TAG_DELIMITERS, DEPLOY_TAG_SIGIL, '/']:
|
7006
|
-
check.not_in(c, self.const)
|
6800
|
+
##
|
7007
6801
|
|
7008
|
-
def render(self, tags: ta.Optional[DeployTagMap] = None) -> str:
|
7009
|
-
return self.const
|
7010
6802
|
|
6803
|
+
_DEPLOY_TAGS: ta.Set[ta.Type[DeployTag]] = set()
|
6804
|
+
DEPLOY_TAGS: ta.AbstractSet[ta.Type[DeployTag]] = _DEPLOY_TAGS
|
7011
6805
|
|
7012
|
-
|
7013
|
-
|
7014
|
-
parts: ta.Sequence[DeployPathNamePart]
|
6806
|
+
_DEPLOY_TAGS_BY_NAME: ta.Dict[str, ta.Type[DeployTag]] = {}
|
6807
|
+
DEPLOY_TAGS_BY_NAME: ta.Mapping[str, ta.Type[DeployTag]] = _DEPLOY_TAGS_BY_NAME
|
7015
6808
|
|
7016
|
-
|
7017
|
-
|
7018
|
-
check.not_empty(self.parts)
|
7019
|
-
for k, g in itertools.groupby(self.parts, type):
|
7020
|
-
if len(gl := list(g)) > 1:
|
7021
|
-
raise DeployPathError(f'May not have consecutive path name part types: {k} {gl}')
|
6809
|
+
_DEPLOY_TAGS_BY_KWARG: ta.Dict[str, ta.Type[DeployTag]] = {}
|
6810
|
+
DEPLOY_TAGS_BY_KWARG: ta.Mapping[str, ta.Type[DeployTag]] = _DEPLOY_TAGS_BY_KWARG
|
7022
6811
|
|
7023
|
-
def render(self, tags: ta.Optional[DeployTagMap] = None) -> str:
|
7024
|
-
return ''.join(p.render(tags) for p in self.parts)
|
7025
6812
|
|
7026
|
-
|
7027
|
-
|
7028
|
-
|
7029
|
-
check.not_in('/', s)
|
6813
|
+
def _register_deploy_tag(cls):
|
6814
|
+
check.not_in(cls.tag_name, _DEPLOY_TAGS_BY_NAME)
|
6815
|
+
check.not_in(cls.tag_kwarg, _DEPLOY_TAGS_BY_KWARG)
|
7030
6816
|
|
7031
|
-
|
7032
|
-
|
7033
|
-
|
7034
|
-
ns = [(n, d) for d in DEPLOY_TAG_DELIMITERS if (n := s.find(d, i)) >= 0]
|
7035
|
-
if not ns:
|
7036
|
-
ps.append(s[i:])
|
7037
|
-
break
|
7038
|
-
n, d = min(ns)
|
7039
|
-
ps.append(check.non_empty_str(s[i:n]))
|
7040
|
-
ps.append(s[n:n + len(d)])
|
7041
|
-
i = n + len(d)
|
6817
|
+
_DEPLOY_TAGS.add(cls)
|
6818
|
+
_DEPLOY_TAGS_BY_NAME[cls.tag_name] = cls
|
6819
|
+
_DEPLOY_TAGS_BY_KWARG[cls.tag_kwarg] = cls
|
7042
6820
|
|
7043
|
-
|
6821
|
+
register_type_obj_marshaler(cls, SingleFieldObjMarshaler(cls, 's'))
|
6822
|
+
|
6823
|
+
return cls
|
7044
6824
|
|
7045
6825
|
|
7046
6826
|
##
|
7047
6827
|
|
7048
6828
|
|
7049
|
-
@
|
7050
|
-
class
|
7051
|
-
|
6829
|
+
@_register_deploy_tag
|
6830
|
+
class DeployTime(DeployTag):
|
6831
|
+
tag_name: ta.ClassVar[str] = 'time'
|
7052
6832
|
|
7053
|
-
@property
|
7054
|
-
@abc.abstractmethod
|
7055
|
-
def kind(self) -> DeployPathKind:
|
7056
|
-
raise NotImplementedError
|
7057
6833
|
|
7058
|
-
|
7059
|
-
return self.name.render(tags) + ('/' if self.kind == 'dir' else '')
|
6834
|
+
##
|
7060
6835
|
|
7061
|
-
@classmethod
|
7062
|
-
def parse(cls, s: str) -> 'DeployPathPart':
|
7063
|
-
if (is_dir := s.endswith('/')):
|
7064
|
-
s = s[:-1]
|
7065
|
-
check.non_empty_str(s)
|
7066
|
-
check.not_in('/', s)
|
7067
6836
|
|
7068
|
-
|
7069
|
-
|
7070
|
-
return DirDeployPathPart(n)
|
7071
|
-
else:
|
7072
|
-
return FileDeployPathPart(n)
|
6837
|
+
class NameDeployTag(DeployTag, abc.ABC): # noqa
|
6838
|
+
pass
|
7073
6839
|
|
7074
6840
|
|
7075
|
-
|
7076
|
-
|
7077
|
-
|
7078
|
-
return 'dir'
|
6841
|
+
@_register_deploy_tag
|
6842
|
+
class DeployApp(NameDeployTag):
|
6843
|
+
tag_name: ta.ClassVar[str] = 'app'
|
7079
6844
|
|
7080
6845
|
|
7081
|
-
|
7082
|
-
|
7083
|
-
|
7084
|
-
return 'file'
|
6846
|
+
@_register_deploy_tag
|
6847
|
+
class DeployConf(NameDeployTag):
|
6848
|
+
tag_name: ta.ClassVar[str] = 'conf'
|
7085
6849
|
|
7086
6850
|
|
7087
6851
|
##
|
7088
6852
|
|
7089
6853
|
|
7090
|
-
|
7091
|
-
|
7092
|
-
parts: ta.Sequence[DeployPathPart]
|
7093
|
-
|
7094
|
-
@property
|
7095
|
-
def name_parts(self) -> ta.Iterator[DeployPathNamePart]:
|
7096
|
-
for p in self.parts:
|
7097
|
-
yield from p.name.parts
|
7098
|
-
|
7099
|
-
def __post_init__(self) -> None:
|
7100
|
-
hash(self)
|
7101
|
-
check.not_empty(self.parts)
|
7102
|
-
for p in self.parts[:-1]:
|
7103
|
-
check.equal(p.kind, 'dir')
|
7104
|
-
|
7105
|
-
@cached_nullary
|
7106
|
-
def tag_indices(self) -> ta.Mapping[ta.Type[DeployTag], ta.Sequence[int]]:
|
7107
|
-
pd: ta.Dict[ta.Type[DeployTag], ta.List[int]] = {}
|
7108
|
-
for i, np in enumerate(self.name_parts):
|
7109
|
-
if isinstance(np, TagDeployPathNamePart):
|
7110
|
-
pd.setdefault(np.tag, []).append(i)
|
7111
|
-
return pd
|
7112
|
-
|
7113
|
-
@property
|
7114
|
-
def kind(self) -> DeployPathKind:
|
7115
|
-
return self.parts[-1].kind
|
6854
|
+
class KeyDeployTag(DeployTag, abc.ABC): # noqa
|
6855
|
+
pass
|
7116
6856
|
|
7117
|
-
def render(self, tags: ta.Optional[DeployTagMap] = None) -> str:
|
7118
|
-
return ''.join([p.render(tags) for p in self.parts])
|
7119
6857
|
|
7120
|
-
|
7121
|
-
|
7122
|
-
|
7123
|
-
ps = split_keep_delimiter(s, '/')
|
7124
|
-
return cls(tuple(DeployPathPart.parse(p) for p in ps))
|
6858
|
+
@_register_deploy_tag
|
6859
|
+
class DeployKey(KeyDeployTag):
|
6860
|
+
tag_name: ta.ClassVar[str] = 'deploy-key'
|
7125
6861
|
|
7126
6862
|
|
7127
|
-
|
7128
|
-
|
6863
|
+
@_register_deploy_tag
|
6864
|
+
class DeployAppKey(KeyDeployTag):
|
6865
|
+
tag_name: ta.ClassVar[str] = 'app-key'
|
7129
6866
|
|
7130
6867
|
|
7131
6868
|
##
|
7132
6869
|
|
7133
6870
|
|
7134
|
-
|
7135
|
-
|
7136
|
-
for c in ['..', '//']:
|
7137
|
-
check.not_in(c, s)
|
7138
|
-
check.arg(not s.startswith('/'))
|
7139
|
-
return s
|
7140
|
-
|
6871
|
+
class RevDeployTag(DeployTag, abc.ABC): # noqa
|
6872
|
+
pass
|
7141
6873
|
|
7142
|
-
class DeploySpecKeyed(ta.Generic[KeyDeployTagT]):
|
7143
|
-
@cached_nullary
|
7144
|
-
def _key_str(self) -> str:
|
7145
|
-
return hashlib.sha256(repr(self).encode('utf-8')).hexdigest()[:8]
|
7146
6874
|
|
7147
|
-
|
7148
|
-
|
7149
|
-
|
6875
|
+
@_register_deploy_tag
|
6876
|
+
class DeployAppRev(RevDeployTag):
|
6877
|
+
tag_name: ta.ClassVar[str] = 'app-rev'
|
7150
6878
|
|
7151
6879
|
|
7152
6880
|
##
|
7153
6881
|
|
7154
6882
|
|
7155
|
-
|
7156
|
-
|
7157
|
-
|
7158
|
-
|
7159
|
-
|
6883
|
+
class DeployTagMap:
|
6884
|
+
def __init__(
|
6885
|
+
self,
|
6886
|
+
*args: DeployTag,
|
6887
|
+
**kwargs: str,
|
6888
|
+
) -> None:
|
6889
|
+
super().__init__()
|
7160
6890
|
|
7161
|
-
|
7162
|
-
check.not_in('..', check.non_empty_str(self.host))
|
7163
|
-
check.not_in('.', check.non_empty_str(self.path))
|
6891
|
+
dct: ta.Dict[ta.Type[DeployTag], DeployTag] = {}
|
7164
6892
|
|
6893
|
+
for a in args:
|
6894
|
+
c = type(check.isinstance(a, DeployTag))
|
6895
|
+
check.not_in(c, dct)
|
6896
|
+
dct[c] = a
|
7165
6897
|
|
7166
|
-
|
7167
|
-
|
7168
|
-
|
7169
|
-
|
7170
|
-
|
7171
|
-
subtrees: ta.Optional[ta.Sequence[str]] = None
|
7172
|
-
|
7173
|
-
def __post_init__(self) -> None:
|
7174
|
-
check.non_empty_str(self.rev)
|
7175
|
-
if self.subtrees is not None:
|
7176
|
-
for st in self.subtrees:
|
7177
|
-
check.non_empty_str(st)
|
7178
|
-
|
7179
|
-
|
7180
|
-
##
|
7181
|
-
|
7182
|
-
|
7183
|
-
@dc.dataclass(frozen=True)
|
7184
|
-
class DeployVenvSpec:
|
7185
|
-
interp: ta.Optional[str] = None
|
7186
|
-
|
7187
|
-
requirements_files: ta.Optional[ta.Sequence[str]] = None
|
7188
|
-
extra_dependencies: ta.Optional[ta.Sequence[str]] = None
|
7189
|
-
|
7190
|
-
use_uv: bool = False
|
7191
|
-
|
7192
|
-
|
7193
|
-
##
|
7194
|
-
|
7195
|
-
|
7196
|
-
@dc.dataclass(frozen=True)
|
7197
|
-
class DeployAppConfFile:
|
7198
|
-
path: str
|
7199
|
-
body: str
|
7200
|
-
|
7201
|
-
def __post_init__(self) -> None:
|
7202
|
-
check_valid_deploy_spec_path(self.path)
|
7203
|
-
|
7204
|
-
|
7205
|
-
#
|
7206
|
-
|
7207
|
-
|
7208
|
-
@dc.dataclass(frozen=True)
|
7209
|
-
class DeployAppConfLink: # noqa
|
7210
|
-
"""
|
7211
|
-
May be either:
|
7212
|
-
- @conf(.ext)* - links a single file in root of app conf dir to conf/@conf/@dst(.ext)*
|
7213
|
-
- @conf/file - links a single file in a single subdir to conf/@conf/@dst--file
|
7214
|
-
- @conf/ - links a directory in root of app conf dir to conf/@conf/@dst/
|
7215
|
-
"""
|
7216
|
-
|
7217
|
-
src: str
|
7218
|
-
|
7219
|
-
kind: ta.Literal['current_only', 'all_active'] = 'current_only'
|
7220
|
-
|
7221
|
-
def __post_init__(self) -> None:
|
7222
|
-
check_valid_deploy_spec_path(self.src)
|
7223
|
-
if '/' in self.src:
|
7224
|
-
check.equal(self.src.count('/'), 1)
|
7225
|
-
|
7226
|
-
|
7227
|
-
#
|
7228
|
-
|
7229
|
-
|
7230
|
-
@dc.dataclass(frozen=True)
|
7231
|
-
class DeployAppConfSpec:
|
7232
|
-
files: ta.Optional[ta.Sequence[DeployAppConfFile]] = None
|
7233
|
-
|
7234
|
-
links: ta.Optional[ta.Sequence[DeployAppConfLink]] = None
|
7235
|
-
|
7236
|
-
def __post_init__(self) -> None:
|
7237
|
-
if self.files:
|
7238
|
-
seen: ta.Set[str] = set()
|
7239
|
-
for f in self.files:
|
7240
|
-
check.not_in(f.path, seen)
|
7241
|
-
seen.add(f.path)
|
6898
|
+
for k, v in kwargs.items():
|
6899
|
+
c = DEPLOY_TAGS_BY_KWARG[k]
|
6900
|
+
check.not_in(c, dct)
|
6901
|
+
dct[c] = c(v)
|
7242
6902
|
|
6903
|
+
self._dct = dct
|
6904
|
+
self._tup = tuple(sorted((type(t).tag_kwarg, t.s) for t in dct.values()))
|
7243
6905
|
|
7244
|
-
|
6906
|
+
#
|
7245
6907
|
|
6908
|
+
def add(self, *args: ta.Any, **kwargs: ta.Any) -> 'DeployTagMap':
|
6909
|
+
return DeployTagMap(
|
6910
|
+
*self,
|
6911
|
+
*args,
|
6912
|
+
**kwargs,
|
6913
|
+
)
|
7246
6914
|
|
7247
|
-
|
7248
|
-
|
7249
|
-
|
6915
|
+
def remove(self, *tags_or_names: ta.Union[ta.Type[DeployTag], str]) -> 'DeployTagMap':
|
6916
|
+
dcs = {
|
6917
|
+
check.issubclass(a, DeployTag) if isinstance(a, type) else DEPLOY_TAGS_BY_NAME[a]
|
6918
|
+
for a in tags_or_names
|
6919
|
+
}
|
7250
6920
|
|
7251
|
-
|
6921
|
+
return DeployTagMap(*[
|
6922
|
+
t
|
6923
|
+
for t in self._dct.values()
|
6924
|
+
if t not in dcs
|
6925
|
+
])
|
7252
6926
|
|
7253
|
-
|
6927
|
+
#
|
7254
6928
|
|
7255
|
-
|
6929
|
+
def __repr__(self) -> str:
|
6930
|
+
return f'{self.__class__.__name__}({", ".join(f"{k}={v!r}" for k, v in self._tup)})'
|
7256
6931
|
|
7257
|
-
|
7258
|
-
|
7259
|
-
return DeployAppKey(self._key_str())
|
6932
|
+
def __hash__(self) -> int:
|
6933
|
+
return hash(self._tup)
|
7260
6934
|
|
6935
|
+
def __eq__(self, other: object) -> bool:
|
6936
|
+
if isinstance(other, DeployTagMap):
|
6937
|
+
return self._tup == other._tup
|
6938
|
+
else:
|
6939
|
+
return NotImplemented
|
7261
6940
|
|
7262
|
-
|
6941
|
+
#
|
7263
6942
|
|
6943
|
+
def __len__(self) -> int:
|
6944
|
+
return len(self._dct)
|
7264
6945
|
|
7265
|
-
|
7266
|
-
|
7267
|
-
apps: ta.Sequence[DeployAppSpec]
|
6946
|
+
def __iter__(self) -> ta.Iterator[DeployTag]:
|
6947
|
+
return iter(self._dct.values())
|
7268
6948
|
|
7269
|
-
def
|
7270
|
-
|
7271
|
-
|
7272
|
-
|
7273
|
-
|
7274
|
-
|
6949
|
+
def __getitem__(self, key: ta.Union[ta.Type[DeployTag], str]) -> DeployTag:
|
6950
|
+
if isinstance(key, str):
|
6951
|
+
return self._dct[DEPLOY_TAGS_BY_NAME[key]]
|
6952
|
+
elif isinstance(key, type):
|
6953
|
+
return self._dct[key]
|
6954
|
+
else:
|
6955
|
+
raise TypeError(key)
|
7275
6956
|
|
7276
|
-
|
7277
|
-
|
7278
|
-
|
6957
|
+
def __contains__(self, key: ta.Union[ta.Type[DeployTag], str]) -> bool:
|
6958
|
+
if isinstance(key, str):
|
6959
|
+
return DEPLOY_TAGS_BY_NAME[key] in self._dct
|
6960
|
+
elif isinstance(key, type):
|
6961
|
+
return key in self._dct
|
6962
|
+
else:
|
6963
|
+
raise TypeError(key)
|
7279
6964
|
|
7280
6965
|
|
7281
6966
|
########################################
|
@@ -7863,291 +7548,418 @@ class LocalCommandExecutor(CommandExecutor):
|
|
7863
7548
|
|
7864
7549
|
|
7865
7550
|
########################################
|
7866
|
-
# ../deploy/
|
7551
|
+
# ../deploy/paths/paths.py
|
7867
7552
|
"""
|
7868
7553
|
TODO:
|
7869
|
-
-
|
7870
|
-
-
|
7871
|
-
|
7872
|
-
|
7873
|
-
-
|
7874
|
-
|
7875
|
-
- 2) for each subdir not needing modification, hardlink into temp dir
|
7876
|
-
- 3) for each subdir needing modification, new subdir, hardlink all files not needing modification
|
7877
|
-
- 4) write (or if deleting, omit) new files
|
7878
|
-
- 5) swap top level
|
7879
|
-
- ** whole deploy can be atomic(-ish) - do this for everything **
|
7880
|
-
- just a '/deploy/current' dir
|
7881
|
-
- some things (venvs) cannot be moved, thus the /deploy/venvs dir
|
7882
|
-
- ** ensure (enforce) equivalent relpath nesting
|
7554
|
+
- run/{.pid,.sock}
|
7555
|
+
- logs/...
|
7556
|
+
- current symlink
|
7557
|
+
- conf/{nginx,supervisor}
|
7558
|
+
- env/?
|
7559
|
+
- apps/<app>/shared
|
7883
7560
|
"""
|
7884
7561
|
|
7885
7562
|
|
7886
|
-
|
7887
|
-
def __init__(
|
7888
|
-
self,
|
7889
|
-
*,
|
7890
|
-
deploy_home: ta.Optional[DeployHome] = None,
|
7891
|
-
) -> None:
|
7892
|
-
super().__init__()
|
7893
|
-
|
7894
|
-
self._deploy_home = deploy_home
|
7563
|
+
##
|
7895
7564
|
|
7896
|
-
#
|
7897
7565
|
|
7898
|
-
|
7899
|
-
|
7900
|
-
acf: DeployAppConfFile,
|
7901
|
-
app_conf_dir: str,
|
7902
|
-
) -> None:
|
7903
|
-
conf_file = os.path.join(app_conf_dir, acf.path)
|
7904
|
-
check.arg(is_path_in_dir(app_conf_dir, conf_file))
|
7566
|
+
class DeployPathError(Exception):
|
7567
|
+
pass
|
7905
7568
|
|
7906
|
-
os.makedirs(os.path.dirname(conf_file), exist_ok=True)
|
7907
7569
|
|
7908
|
-
|
7909
|
-
|
7570
|
+
class DeployPathRenderable(abc.ABC):
|
7571
|
+
@abc.abstractmethod
|
7572
|
+
def render(self, tags: ta.Optional[DeployTagMap] = None) -> str:
|
7573
|
+
raise NotImplementedError
|
7910
7574
|
|
7911
|
-
#
|
7912
7575
|
|
7913
|
-
|
7914
|
-
conf: DeployConf
|
7915
|
-
is_dir: bool
|
7916
|
-
link_src: str
|
7917
|
-
link_dst: str
|
7576
|
+
##
|
7918
7577
|
|
7919
|
-
_UNIQUE_LINK_NAME_STR = '@app--@time--@app-key'
|
7920
|
-
_UNIQUE_LINK_NAME = DeployPath.parse(_UNIQUE_LINK_NAME_STR)
|
7921
7578
|
|
7579
|
+
class DeployPathNamePart(DeployPathRenderable, abc.ABC):
|
7922
7580
|
@classmethod
|
7923
|
-
def
|
7924
|
-
|
7925
|
-
|
7926
|
-
|
7927
|
-
|
7928
|
-
|
7929
|
-
|
7930
|
-
|
7931
|
-
check.arg(is_path_in_dir(app_conf_dir, link_src))
|
7932
|
-
|
7933
|
-
#
|
7581
|
+
def parse(cls, s: str) -> 'DeployPathNamePart':
|
7582
|
+
check.non_empty_str(s)
|
7583
|
+
if s.startswith(DEPLOY_TAG_SIGIL):
|
7584
|
+
return TagDeployPathNamePart(s[1:])
|
7585
|
+
elif s in DEPLOY_TAG_DELIMITERS:
|
7586
|
+
return DelimiterDeployPathNamePart(s)
|
7587
|
+
else:
|
7588
|
+
return ConstDeployPathNamePart(s)
|
7934
7589
|
|
7935
|
-
if (is_dir := link.src.endswith('/')):
|
7936
|
-
# @conf/ - links a directory in root of app conf dir to conf/@conf/@dst/
|
7937
|
-
check.arg(link.src.count('/') == 1)
|
7938
|
-
conf = DeployConf(link.src.split('/')[0])
|
7939
|
-
link_dst_pfx = link.src
|
7940
|
-
link_dst_sfx = ''
|
7941
7590
|
|
7942
|
-
|
7943
|
-
|
7944
|
-
|
7945
|
-
# TODO: check filename :|
|
7946
|
-
conf = DeployConf(d)
|
7947
|
-
link_dst_pfx = d + '/'
|
7948
|
-
link_dst_sfx = DEPLOY_TAG_SEPARATOR + f
|
7591
|
+
@dc.dataclass(frozen=True)
|
7592
|
+
class TagDeployPathNamePart(DeployPathNamePart):
|
7593
|
+
name: str
|
7949
7594
|
|
7950
|
-
|
7951
|
-
|
7952
|
-
if '.' in link.src:
|
7953
|
-
l, _, r = link.src.partition('.')
|
7954
|
-
conf = DeployConf(l)
|
7955
|
-
link_dst_pfx = l + '/'
|
7956
|
-
link_dst_sfx = '.' + r
|
7957
|
-
else:
|
7958
|
-
conf = DeployConf(link.src)
|
7959
|
-
link_dst_pfx = link.src + '/'
|
7960
|
-
link_dst_sfx = ''
|
7595
|
+
def __post_init__(self) -> None:
|
7596
|
+
check.in_(self.name, DEPLOY_TAGS_BY_NAME)
|
7961
7597
|
|
7962
|
-
|
7598
|
+
@property
|
7599
|
+
def tag(self) -> ta.Type[DeployTag]:
|
7600
|
+
return DEPLOY_TAGS_BY_NAME[self.name]
|
7963
7601
|
|
7964
|
-
|
7965
|
-
|
7966
|
-
|
7967
|
-
link_dst_mid = cls._UNIQUE_LINK_NAME.render(tags)
|
7602
|
+
def render(self, tags: ta.Optional[DeployTagMap] = None) -> str:
|
7603
|
+
if tags is not None:
|
7604
|
+
return tags[self.tag].s
|
7968
7605
|
else:
|
7969
|
-
|
7606
|
+
return DEPLOY_TAG_SIGIL + self.name
|
7970
7607
|
|
7971
|
-
#
|
7972
7608
|
|
7973
|
-
|
7974
|
-
|
7975
|
-
|
7976
|
-
link_dst_sfx,
|
7977
|
-
])
|
7978
|
-
link_dst = os.path.join(conf_link_dir, link_dst_name)
|
7609
|
+
@dc.dataclass(frozen=True)
|
7610
|
+
class DelimiterDeployPathNamePart(DeployPathNamePart):
|
7611
|
+
delimiter: str
|
7979
7612
|
|
7980
|
-
|
7981
|
-
|
7982
|
-
is_dir=is_dir,
|
7983
|
-
link_src=link_src,
|
7984
|
-
link_dst=link_dst,
|
7985
|
-
)
|
7613
|
+
def __post_init__(self) -> None:
|
7614
|
+
check.in_(self.delimiter, DEPLOY_TAG_DELIMITERS)
|
7986
7615
|
|
7987
|
-
|
7988
|
-
|
7989
|
-
link: DeployAppConfLink,
|
7990
|
-
tags: DeployTagMap,
|
7991
|
-
app_conf_dir: str,
|
7992
|
-
conf_link_dir: str,
|
7993
|
-
) -> None:
|
7994
|
-
comp = self._compute_app_conf_link_dst(
|
7995
|
-
link,
|
7996
|
-
tags,
|
7997
|
-
app_conf_dir,
|
7998
|
-
conf_link_dir,
|
7999
|
-
)
|
7616
|
+
def render(self, tags: ta.Optional[DeployTagMap] = None) -> str:
|
7617
|
+
return self.delimiter
|
8000
7618
|
|
8001
|
-
#
|
8002
7619
|
|
8003
|
-
|
8004
|
-
|
7620
|
+
@dc.dataclass(frozen=True)
|
7621
|
+
class ConstDeployPathNamePart(DeployPathNamePart):
|
7622
|
+
const: str
|
8005
7623
|
|
8006
|
-
|
8007
|
-
|
8008
|
-
|
8009
|
-
check.
|
7624
|
+
def __post_init__(self) -> None:
|
7625
|
+
check.non_empty_str(self.const)
|
7626
|
+
for c in [*DEPLOY_TAG_DELIMITERS, DEPLOY_TAG_SIGIL, '/']:
|
7627
|
+
check.not_in(c, self.const)
|
8010
7628
|
|
8011
|
-
|
7629
|
+
def render(self, tags: ta.Optional[DeployTagMap] = None) -> str:
|
7630
|
+
return self.const
|
8012
7631
|
|
8013
|
-
relative_symlink( # noqa
|
8014
|
-
comp.link_src,
|
8015
|
-
comp.link_dst,
|
8016
|
-
target_is_directory=comp.is_dir,
|
8017
|
-
make_dirs=True,
|
8018
|
-
)
|
8019
7632
|
|
8020
|
-
|
7633
|
+
@dc.dataclass(frozen=True)
|
7634
|
+
class DeployPathName(DeployPathRenderable):
|
7635
|
+
parts: ta.Sequence[DeployPathNamePart]
|
8021
7636
|
|
8022
|
-
|
8023
|
-
|
8024
|
-
|
8025
|
-
|
8026
|
-
|
8027
|
-
|
8028
|
-
) -> None:
|
8029
|
-
for acf in spec.files or []:
|
8030
|
-
await self._write_app_conf_file(
|
8031
|
-
acf,
|
8032
|
-
app_conf_dir,
|
8033
|
-
)
|
7637
|
+
def __post_init__(self) -> None:
|
7638
|
+
hash(self)
|
7639
|
+
check.not_empty(self.parts)
|
7640
|
+
for k, g in itertools.groupby(self.parts, type):
|
7641
|
+
if len(gl := list(g)) > 1:
|
7642
|
+
raise DeployPathError(f'May not have consecutive path name part types: {k} {gl}')
|
8034
7643
|
|
8035
|
-
|
7644
|
+
def render(self, tags: ta.Optional[DeployTagMap] = None) -> str:
|
7645
|
+
return ''.join(p.render(tags) for p in self.parts)
|
8036
7646
|
|
8037
|
-
|
8038
|
-
|
8039
|
-
|
8040
|
-
|
8041
|
-
app_conf_dir,
|
8042
|
-
conf_link_dir,
|
8043
|
-
)
|
7647
|
+
@classmethod
|
7648
|
+
def parse(cls, s: str) -> 'DeployPathName':
|
7649
|
+
check.non_empty_str(s)
|
7650
|
+
check.not_in('/', s)
|
8044
7651
|
|
7652
|
+
i = 0
|
7653
|
+
ps = []
|
7654
|
+
while i < len(s):
|
7655
|
+
ns = [(n, d) for d in DEPLOY_TAG_DELIMITERS if (n := s.find(d, i)) >= 0]
|
7656
|
+
if not ns:
|
7657
|
+
ps.append(s[i:])
|
7658
|
+
break
|
7659
|
+
n, d = min(ns)
|
7660
|
+
ps.append(check.non_empty_str(s[i:n]))
|
7661
|
+
ps.append(s[n:n + len(d)])
|
7662
|
+
i = n + len(d)
|
8045
7663
|
|
8046
|
-
|
8047
|
-
# ../deploy/paths/owners.py
|
7664
|
+
return cls(tuple(DeployPathNamePart.parse(p) for p in ps))
|
8048
7665
|
|
8049
7666
|
|
8050
|
-
|
7667
|
+
##
|
7668
|
+
|
7669
|
+
|
7670
|
+
@dc.dataclass(frozen=True)
|
7671
|
+
class DeployPathPart(DeployPathRenderable, abc.ABC): # noqa
|
7672
|
+
name: DeployPathName
|
7673
|
+
|
7674
|
+
@property
|
8051
7675
|
@abc.abstractmethod
|
8052
|
-
def
|
7676
|
+
def kind(self) -> DeployPathKind:
|
8053
7677
|
raise NotImplementedError
|
8054
7678
|
|
7679
|
+
def render(self, tags: ta.Optional[DeployTagMap] = None) -> str:
|
7680
|
+
return self.name.render(tags) + ('/' if self.kind == 'dir' else '')
|
8055
7681
|
|
8056
|
-
|
7682
|
+
@classmethod
|
7683
|
+
def parse(cls, s: str) -> 'DeployPathPart':
|
7684
|
+
if (is_dir := s.endswith('/')):
|
7685
|
+
s = s[:-1]
|
7686
|
+
check.non_empty_str(s)
|
7687
|
+
check.not_in('/', s)
|
8057
7688
|
|
7689
|
+
n = DeployPathName.parse(s)
|
7690
|
+
if is_dir:
|
7691
|
+
return DirDeployPathPart(n)
|
7692
|
+
else:
|
7693
|
+
return FileDeployPathPart(n)
|
8058
7694
|
|
8059
|
-
class SingleDirDeployPathOwner(DeployPathOwner, abc.ABC):
|
8060
|
-
def __init__(
|
8061
|
-
self,
|
8062
|
-
*args: ta.Any,
|
8063
|
-
owned_dir: str,
|
8064
|
-
deploy_home: ta.Optional[DeployHome],
|
8065
|
-
**kwargs: ta.Any,
|
8066
|
-
) -> None:
|
8067
|
-
super().__init__(*args, **kwargs)
|
8068
7695
|
|
8069
|
-
|
8070
|
-
|
7696
|
+
class DirDeployPathPart(DeployPathPart):
|
7697
|
+
@property
|
7698
|
+
def kind(self) -> DeployPathKind:
|
7699
|
+
return 'dir'
|
8071
7700
|
|
8072
|
-
self._deploy_home = deploy_home
|
8073
7701
|
|
8074
|
-
|
7702
|
+
class FileDeployPathPart(DeployPathPart):
|
7703
|
+
@property
|
7704
|
+
def kind(self) -> DeployPathKind:
|
7705
|
+
return 'file'
|
8075
7706
|
|
8076
|
-
@cached_nullary
|
8077
|
-
def _dir(self) -> str:
|
8078
|
-
return os.path.join(check.non_empty_str(self._deploy_home), self._owned_dir)
|
8079
7707
|
|
8080
|
-
|
8081
|
-
def _make_dir(self) -> str:
|
8082
|
-
if not os.path.isdir(d := self._dir()):
|
8083
|
-
os.makedirs(d, exist_ok=True)
|
8084
|
-
return d
|
7708
|
+
##
|
8085
7709
|
|
8086
|
-
def get_owned_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
|
8087
|
-
return self._owned_deploy_paths
|
8088
7710
|
|
7711
|
+
@dc.dataclass(frozen=True)
|
7712
|
+
class DeployPath:
|
7713
|
+
parts: ta.Sequence[DeployPathPart]
|
8089
7714
|
|
8090
|
-
|
8091
|
-
|
8092
|
-
|
8093
|
-
|
8094
|
-
- sequence all messages
|
8095
|
-
"""
|
7715
|
+
@property
|
7716
|
+
def name_parts(self) -> ta.Iterator[DeployPathNamePart]:
|
7717
|
+
for p in self.parts:
|
7718
|
+
yield from p.name.parts
|
8096
7719
|
|
7720
|
+
def __post_init__(self) -> None:
|
7721
|
+
hash(self)
|
7722
|
+
check.not_empty(self.parts)
|
7723
|
+
for p in self.parts[:-1]:
|
7724
|
+
check.equal(p.kind, 'dir')
|
8097
7725
|
|
8098
|
-
|
7726
|
+
@cached_nullary
|
7727
|
+
def tag_indices(self) -> ta.Mapping[ta.Type[DeployTag], ta.Sequence[int]]:
|
7728
|
+
pd: ta.Dict[ta.Type[DeployTag], ta.List[int]] = {}
|
7729
|
+
for i, np in enumerate(self.name_parts):
|
7730
|
+
if isinstance(np, TagDeployPathNamePart):
|
7731
|
+
pd.setdefault(np.tag, []).append(i)
|
7732
|
+
return pd
|
8099
7733
|
|
7734
|
+
@property
|
7735
|
+
def kind(self) -> DeployPathKind:
|
7736
|
+
return self.parts[-1].kind
|
8100
7737
|
|
8101
|
-
|
8102
|
-
|
8103
|
-
async def send(self, chan: RemoteChannel) -> None:
|
8104
|
-
await chan.send_obj(self, _RemoteProtocol.Message)
|
7738
|
+
def render(self, tags: ta.Optional[DeployTagMap] = None) -> str:
|
7739
|
+
return ''.join([p.render(tags) for p in self.parts])
|
8105
7740
|
|
8106
|
-
|
8107
|
-
|
8108
|
-
|
7741
|
+
@classmethod
|
7742
|
+
def parse(cls, s: str) -> 'DeployPath':
|
7743
|
+
check.non_empty_str(s)
|
7744
|
+
ps = split_keep_delimiter(s, '/')
|
7745
|
+
return cls(tuple(DeployPathPart.parse(p) for p in ps))
|
8109
7746
|
|
8110
|
-
#
|
8111
7747
|
|
8112
|
-
|
8113
|
-
|
7748
|
+
########################################
|
7749
|
+
# ../deploy/specs.py
|
8114
7750
|
|
8115
|
-
@dc.dataclass(frozen=True)
|
8116
|
-
class CommandRequest(Request):
|
8117
|
-
seq: int
|
8118
|
-
cmd: Command
|
8119
7751
|
|
8120
|
-
|
8121
|
-
class PingRequest(Request):
|
8122
|
-
time: float
|
7752
|
+
##
|
8123
7753
|
|
8124
|
-
#
|
8125
7754
|
|
8126
|
-
|
8127
|
-
|
7755
|
+
def check_valid_deploy_spec_path(s: str) -> str:
|
7756
|
+
check.non_empty_str(s)
|
7757
|
+
for c in ['..', '//']:
|
7758
|
+
check.not_in(c, s)
|
7759
|
+
check.arg(not s.startswith('/'))
|
7760
|
+
return s
|
8128
7761
|
|
8129
|
-
@dc.dataclass(frozen=True)
|
8130
|
-
class LogResponse(Response):
|
8131
|
-
s: str
|
8132
7762
|
|
8133
|
-
|
8134
|
-
|
8135
|
-
|
8136
|
-
|
7763
|
+
class DeploySpecKeyed(ta.Generic[KeyDeployTagT]):
|
7764
|
+
@cached_nullary
|
7765
|
+
def _key_str(self) -> str:
|
7766
|
+
return hashlib.sha256(repr(self).encode('utf-8')).hexdigest()[:8]
|
8137
7767
|
|
8138
|
-
@
|
8139
|
-
|
8140
|
-
|
7768
|
+
@abc.abstractmethod
|
7769
|
+
def key(self) -> KeyDeployTagT:
|
7770
|
+
raise NotImplementedError
|
8141
7771
|
|
8142
7772
|
|
8143
7773
|
##
|
8144
7774
|
|
8145
7775
|
|
8146
|
-
|
8147
|
-
|
8148
|
-
|
8149
|
-
|
8150
|
-
|
7776
|
+
@dc.dataclass(frozen=True)
|
7777
|
+
class DeployGitRepo:
|
7778
|
+
host: ta.Optional[str] = None
|
7779
|
+
username: ta.Optional[str] = None
|
7780
|
+
path: ta.Optional[str] = None
|
7781
|
+
|
7782
|
+
def __post_init__(self) -> None:
|
7783
|
+
check.not_in('..', check.non_empty_str(self.host))
|
7784
|
+
check.not_in('.', check.non_empty_str(self.path))
|
7785
|
+
|
7786
|
+
|
7787
|
+
@dc.dataclass(frozen=True)
|
7788
|
+
class DeployGitSpec:
|
7789
|
+
repo: DeployGitRepo
|
7790
|
+
rev: DeployRev
|
7791
|
+
|
7792
|
+
subtrees: ta.Optional[ta.Sequence[str]] = None
|
7793
|
+
|
7794
|
+
def __post_init__(self) -> None:
|
7795
|
+
check.non_empty_str(self.rev)
|
7796
|
+
if self.subtrees is not None:
|
7797
|
+
for st in self.subtrees:
|
7798
|
+
check.non_empty_str(st)
|
7799
|
+
|
7800
|
+
|
7801
|
+
##
|
7802
|
+
|
7803
|
+
|
7804
|
+
@dc.dataclass(frozen=True)
|
7805
|
+
class DeployVenvSpec:
|
7806
|
+
interp: ta.Optional[str] = None
|
7807
|
+
|
7808
|
+
requirements_files: ta.Optional[ta.Sequence[str]] = None
|
7809
|
+
extra_dependencies: ta.Optional[ta.Sequence[str]] = None
|
7810
|
+
|
7811
|
+
use_uv: bool = False
|
7812
|
+
|
7813
|
+
|
7814
|
+
##
|
7815
|
+
|
7816
|
+
|
7817
|
+
@dc.dataclass(frozen=True)
|
7818
|
+
class DeployAppConfFile:
|
7819
|
+
path: str
|
7820
|
+
body: str
|
7821
|
+
|
7822
|
+
def __post_init__(self) -> None:
|
7823
|
+
check_valid_deploy_spec_path(self.path)
|
7824
|
+
|
7825
|
+
|
7826
|
+
#
|
7827
|
+
|
7828
|
+
|
7829
|
+
@dc.dataclass(frozen=True)
|
7830
|
+
class DeployAppConfLink: # noqa
|
7831
|
+
"""
|
7832
|
+
May be either:
|
7833
|
+
- @conf(.ext)* - links a single file in root of app conf dir to conf/@conf/@dst(.ext)*
|
7834
|
+
- @conf/file - links a single file in a single subdir to conf/@conf/@dst--file
|
7835
|
+
- @conf/ - links a directory in root of app conf dir to conf/@conf/@dst/
|
7836
|
+
"""
|
7837
|
+
|
7838
|
+
src: str
|
7839
|
+
|
7840
|
+
kind: ta.Literal['current_only', 'all_active'] = 'current_only'
|
7841
|
+
|
7842
|
+
def __post_init__(self) -> None:
|
7843
|
+
check_valid_deploy_spec_path(self.src)
|
7844
|
+
if '/' in self.src:
|
7845
|
+
check.equal(self.src.count('/'), 1)
|
7846
|
+
|
7847
|
+
|
7848
|
+
#
|
7849
|
+
|
7850
|
+
|
7851
|
+
@dc.dataclass(frozen=True)
|
7852
|
+
class DeployAppConfSpec:
|
7853
|
+
files: ta.Optional[ta.Sequence[DeployAppConfFile]] = None
|
7854
|
+
|
7855
|
+
links: ta.Optional[ta.Sequence[DeployAppConfLink]] = None
|
7856
|
+
|
7857
|
+
def __post_init__(self) -> None:
|
7858
|
+
if self.files:
|
7859
|
+
seen: ta.Set[str] = set()
|
7860
|
+
for f in self.files:
|
7861
|
+
check.not_in(f.path, seen)
|
7862
|
+
seen.add(f.path)
|
7863
|
+
|
7864
|
+
|
7865
|
+
##
|
7866
|
+
|
7867
|
+
|
7868
|
+
@dc.dataclass(frozen=True)
|
7869
|
+
class DeployAppSpec(DeploySpecKeyed[DeployAppKey]):
|
7870
|
+
app: DeployApp
|
7871
|
+
|
7872
|
+
git: DeployGitSpec
|
7873
|
+
|
7874
|
+
venv: ta.Optional[DeployVenvSpec] = None
|
7875
|
+
|
7876
|
+
conf: ta.Optional[DeployAppConfSpec] = None
|
7877
|
+
|
7878
|
+
# @ta.override
|
7879
|
+
def key(self) -> DeployAppKey:
|
7880
|
+
return DeployAppKey(self._key_str())
|
7881
|
+
|
7882
|
+
|
7883
|
+
##
|
7884
|
+
|
7885
|
+
|
7886
|
+
@dc.dataclass(frozen=True)
|
7887
|
+
class DeploySpec(DeploySpecKeyed[DeployKey]):
|
7888
|
+
apps: ta.Sequence[DeployAppSpec]
|
7889
|
+
|
7890
|
+
def __post_init__(self) -> None:
|
7891
|
+
seen: ta.Set[DeployApp] = set()
|
7892
|
+
for a in self.apps:
|
7893
|
+
if a.app in seen:
|
7894
|
+
raise KeyError(a.app)
|
7895
|
+
seen.add(a.app)
|
7896
|
+
|
7897
|
+
# @ta.override
|
7898
|
+
def key(self) -> DeployKey:
|
7899
|
+
return DeployKey(self._key_str())
|
7900
|
+
|
7901
|
+
|
7902
|
+
########################################
|
7903
|
+
# ../remote/execution.py
|
7904
|
+
"""
|
7905
|
+
TODO:
|
7906
|
+
- sequence all messages
|
7907
|
+
"""
|
7908
|
+
|
7909
|
+
|
7910
|
+
##
|
7911
|
+
|
7912
|
+
|
7913
|
+
class _RemoteProtocol:
|
7914
|
+
class Message(abc.ABC): # noqa
|
7915
|
+
async def send(self, chan: RemoteChannel) -> None:
|
7916
|
+
await chan.send_obj(self, _RemoteProtocol.Message)
|
7917
|
+
|
7918
|
+
@classmethod
|
7919
|
+
async def recv(cls: ta.Type[T], chan: RemoteChannel) -> ta.Optional[T]:
|
7920
|
+
return await chan.recv_obj(cls)
|
7921
|
+
|
7922
|
+
#
|
7923
|
+
|
7924
|
+
class Request(Message, abc.ABC): # noqa
|
7925
|
+
pass
|
7926
|
+
|
7927
|
+
@dc.dataclass(frozen=True)
|
7928
|
+
class CommandRequest(Request):
|
7929
|
+
seq: int
|
7930
|
+
cmd: Command
|
7931
|
+
|
7932
|
+
@dc.dataclass(frozen=True)
|
7933
|
+
class PingRequest(Request):
|
7934
|
+
time: float
|
7935
|
+
|
7936
|
+
#
|
7937
|
+
|
7938
|
+
class Response(Message, abc.ABC): # noqa
|
7939
|
+
pass
|
7940
|
+
|
7941
|
+
@dc.dataclass(frozen=True)
|
7942
|
+
class LogResponse(Response):
|
7943
|
+
s: str
|
7944
|
+
|
7945
|
+
@dc.dataclass(frozen=True)
|
7946
|
+
class CommandResponse(Response):
|
7947
|
+
seq: int
|
7948
|
+
res: CommandOutputOrExceptionData
|
7949
|
+
|
7950
|
+
@dc.dataclass(frozen=True)
|
7951
|
+
class PingResponse(Response):
|
7952
|
+
time: float
|
7953
|
+
|
7954
|
+
|
7955
|
+
##
|
7956
|
+
|
7957
|
+
|
7958
|
+
class _RemoteLogHandler(logging.Handler):
|
7959
|
+
def __init__(
|
7960
|
+
self,
|
7961
|
+
chan: RemoteChannel,
|
7962
|
+
loop: ta.Any = None,
|
8151
7963
|
) -> None:
|
8152
7964
|
super().__init__()
|
8153
7965
|
|
@@ -8874,262 +8686,254 @@ class SubprocessCommandExecutor(CommandExecutor[SubprocessCommand, SubprocessCom
|
|
8874
8686
|
|
8875
8687
|
|
8876
8688
|
########################################
|
8877
|
-
# ../deploy/
|
8689
|
+
# ../deploy/conf.py
|
8878
8690
|
"""
|
8879
8691
|
TODO:
|
8880
|
-
-
|
8881
|
-
|
8882
|
-
|
8883
|
-
|
8884
|
-
|
8885
|
-
|
8692
|
+
- @conf DeployPathPlaceholder? :|
|
8693
|
+
- post-deploy: remove any dir_links not present in new spec
|
8694
|
+
- * only if succeeded * - otherwise, remove any dir_links present in new spec but not previously present?
|
8695
|
+
- no such thing as 'previously present'.. build a 'deploy state' and pass it back?
|
8696
|
+
- ** whole thing can be atomic **
|
8697
|
+
- 1) new atomic temp dir
|
8698
|
+
- 2) for each subdir not needing modification, hardlink into temp dir
|
8699
|
+
- 3) for each subdir needing modification, new subdir, hardlink all files not needing modification
|
8700
|
+
- 4) write (or if deleting, omit) new files
|
8701
|
+
- 5) swap top level
|
8702
|
+
- ** whole deploy can be atomic(-ish) - do this for everything **
|
8703
|
+
- just a '/deploy/current' dir
|
8704
|
+
- some things (venvs) cannot be moved, thus the /deploy/venvs dir
|
8705
|
+
- ** ensure (enforce) equivalent relpath nesting
|
8886
8706
|
"""
|
8887
8707
|
|
8888
8708
|
|
8889
|
-
|
8890
|
-
|
8891
|
-
|
8892
|
-
class DeployGitManager(SingleDirDeployPathOwner):
|
8709
|
+
class DeployConfManager:
|
8893
8710
|
def __init__(
|
8894
8711
|
self,
|
8895
8712
|
*,
|
8896
8713
|
deploy_home: ta.Optional[DeployHome] = None,
|
8897
|
-
atomics: AtomicPathSwapping,
|
8898
8714
|
) -> None:
|
8899
|
-
super().__init__(
|
8900
|
-
owned_dir='git',
|
8901
|
-
deploy_home=deploy_home,
|
8902
|
-
)
|
8715
|
+
super().__init__()
|
8903
8716
|
|
8904
|
-
self.
|
8717
|
+
self._deploy_home = deploy_home
|
8905
8718
|
|
8906
|
-
|
8719
|
+
#
|
8907
8720
|
|
8908
|
-
|
8909
|
-
|
8910
|
-
|
8911
|
-
|
8912
|
-
|
8913
|
-
|
8914
|
-
|
8721
|
+
async def _write_app_conf_file(
|
8722
|
+
self,
|
8723
|
+
acf: DeployAppConfFile,
|
8724
|
+
app_conf_dir: str,
|
8725
|
+
) -> None:
|
8726
|
+
conf_file = os.path.join(app_conf_dir, acf.path)
|
8727
|
+
check.arg(is_path_in_dir(app_conf_dir, conf_file))
|
8915
8728
|
|
8916
|
-
|
8917
|
-
self._repo = repo
|
8918
|
-
self._dir = os.path.join(
|
8919
|
-
self._git._make_dir(), # noqa
|
8920
|
-
check.non_empty_str(repo.host),
|
8921
|
-
check.non_empty_str(repo.path),
|
8922
|
-
)
|
8729
|
+
os.makedirs(os.path.dirname(conf_file), exist_ok=True)
|
8923
8730
|
|
8924
|
-
|
8925
|
-
|
8926
|
-
return self._repo
|
8731
|
+
with open(conf_file, 'w') as f: # noqa
|
8732
|
+
f.write(acf.body)
|
8927
8733
|
|
8928
|
-
|
8929
|
-
|
8930
|
-
|
8931
|
-
|
8932
|
-
|
8933
|
-
|
8734
|
+
#
|
8735
|
+
|
8736
|
+
class _ComputedConfLink(ta.NamedTuple):
|
8737
|
+
conf: DeployConf
|
8738
|
+
is_dir: bool
|
8739
|
+
link_src: str
|
8740
|
+
link_dst: str
|
8741
|
+
|
8742
|
+
_UNIQUE_LINK_NAME_STR = '@app--@time--@app-key'
|
8743
|
+
_UNIQUE_LINK_NAME = DeployPath.parse(_UNIQUE_LINK_NAME_STR)
|
8744
|
+
|
8745
|
+
@classmethod
|
8746
|
+
def _compute_app_conf_link_dst(
|
8747
|
+
cls,
|
8748
|
+
link: DeployAppConfLink,
|
8749
|
+
tags: DeployTagMap,
|
8750
|
+
app_conf_dir: str,
|
8751
|
+
conf_link_dir: str,
|
8752
|
+
) -> _ComputedConfLink:
|
8753
|
+
link_src = os.path.join(app_conf_dir, link.src)
|
8754
|
+
check.arg(is_path_in_dir(app_conf_dir, link_src))
|
8934
8755
|
|
8935
8756
|
#
|
8936
8757
|
|
8937
|
-
|
8938
|
-
|
8939
|
-
|
8940
|
-
|
8941
|
-
|
8758
|
+
if (is_dir := link.src.endswith('/')):
|
8759
|
+
# @conf/ - links a directory in root of app conf dir to conf/@conf/@dst/
|
8760
|
+
check.arg(link.src.count('/') == 1)
|
8761
|
+
conf = DeployConf(link.src.split('/')[0])
|
8762
|
+
link_dst_pfx = link.src
|
8763
|
+
link_dst_sfx = ''
|
8764
|
+
|
8765
|
+
elif '/' in link.src:
|
8766
|
+
# @conf/file - links a single file in a single subdir to conf/@conf/@dst--file
|
8767
|
+
d, f = os.path.split(link.src)
|
8768
|
+
# TODO: check filename :|
|
8769
|
+
conf = DeployConf(d)
|
8770
|
+
link_dst_pfx = d + '/'
|
8771
|
+
link_dst_sfx = DEPLOY_TAG_SEPARATOR + f
|
8772
|
+
|
8773
|
+
else: # noqa
|
8774
|
+
# @conf(.ext)* - links a single file in root of app conf dir to conf/@conf/@dst(.ext)*
|
8775
|
+
if '.' in link.src:
|
8776
|
+
l, _, r = link.src.partition('.')
|
8777
|
+
conf = DeployConf(l)
|
8778
|
+
link_dst_pfx = l + '/'
|
8779
|
+
link_dst_sfx = '.' + r
|
8780
|
+
else:
|
8781
|
+
conf = DeployConf(link.src)
|
8782
|
+
link_dst_pfx = link.src + '/'
|
8783
|
+
link_dst_sfx = ''
|
8942
8784
|
|
8943
8785
|
#
|
8944
8786
|
|
8945
|
-
|
8946
|
-
|
8947
|
-
|
8948
|
-
|
8949
|
-
|
8787
|
+
if link.kind == 'current_only':
|
8788
|
+
link_dst_mid = str(tags[DeployApp].s)
|
8789
|
+
elif link.kind == 'all_active':
|
8790
|
+
link_dst_mid = cls._UNIQUE_LINK_NAME.render(tags)
|
8791
|
+
else:
|
8792
|
+
raise TypeError(link)
|
8950
8793
|
|
8951
|
-
|
8952
|
-
await self._call('git', 'remote', 'add', 'origin', self.url)
|
8794
|
+
#
|
8953
8795
|
|
8954
|
-
|
8955
|
-
|
8956
|
-
|
8796
|
+
link_dst_name = ''.join([
|
8797
|
+
link_dst_pfx,
|
8798
|
+
link_dst_mid,
|
8799
|
+
link_dst_sfx,
|
8800
|
+
])
|
8801
|
+
link_dst = os.path.join(conf_link_dir, link_dst_name)
|
8802
|
+
|
8803
|
+
return DeployConfManager._ComputedConfLink(
|
8804
|
+
conf=conf,
|
8805
|
+
is_dir=is_dir,
|
8806
|
+
link_src=link_src,
|
8807
|
+
link_dst=link_dst,
|
8808
|
+
)
|
8809
|
+
|
8810
|
+
async def _make_app_conf_link(
|
8811
|
+
self,
|
8812
|
+
link: DeployAppConfLink,
|
8813
|
+
tags: DeployTagMap,
|
8814
|
+
app_conf_dir: str,
|
8815
|
+
conf_link_dir: str,
|
8816
|
+
) -> None:
|
8817
|
+
comp = self._compute_app_conf_link_dst(
|
8818
|
+
link,
|
8819
|
+
tags,
|
8820
|
+
app_conf_dir,
|
8821
|
+
conf_link_dir,
|
8822
|
+
)
|
8957
8823
|
|
8958
8824
|
#
|
8959
8825
|
|
8960
|
-
|
8961
|
-
|
8962
|
-
with self._git._atomics.begin_atomic_path_swap( # noqa
|
8963
|
-
'dir',
|
8964
|
-
dst_dir,
|
8965
|
-
auto_commit=True,
|
8966
|
-
make_dirs=True,
|
8967
|
-
) as dst_swap:
|
8968
|
-
await self.fetch(spec.rev)
|
8826
|
+
check.arg(is_path_in_dir(app_conf_dir, comp.link_src))
|
8827
|
+
check.arg(is_path_in_dir(conf_link_dir, comp.link_dst))
|
8969
8828
|
|
8970
|
-
|
8971
|
-
|
8829
|
+
if comp.is_dir:
|
8830
|
+
check.arg(os.path.isdir(comp.link_src))
|
8831
|
+
else:
|
8832
|
+
check.arg(os.path.isfile(comp.link_src))
|
8972
8833
|
|
8973
|
-
|
8974
|
-
await dst_call('git', 'fetch', '--depth=1', 'local', spec.rev)
|
8975
|
-
await dst_call('git', 'checkout', spec.rev, *(spec.subtrees or []))
|
8834
|
+
#
|
8976
8835
|
|
8977
|
-
|
8978
|
-
|
8979
|
-
|
8980
|
-
|
8981
|
-
|
8982
|
-
|
8836
|
+
relative_symlink( # noqa
|
8837
|
+
comp.link_src,
|
8838
|
+
comp.link_dst,
|
8839
|
+
target_is_directory=comp.is_dir,
|
8840
|
+
make_dirs=True,
|
8841
|
+
)
|
8983
8842
|
|
8984
|
-
|
8843
|
+
#
|
8844
|
+
|
8845
|
+
async def write_app_conf(
|
8985
8846
|
self,
|
8986
|
-
spec:
|
8987
|
-
|
8847
|
+
spec: DeployAppConfSpec,
|
8848
|
+
tags: DeployTagMap,
|
8849
|
+
app_conf_dir: str,
|
8850
|
+
conf_link_dir: str,
|
8988
8851
|
) -> None:
|
8989
|
-
|
8852
|
+
for acf in spec.files or []:
|
8853
|
+
await self._write_app_conf_file(
|
8854
|
+
acf,
|
8855
|
+
app_conf_dir,
|
8856
|
+
)
|
8857
|
+
|
8858
|
+
#
|
8859
|
+
|
8860
|
+
for link in spec.links or []:
|
8861
|
+
await self._make_app_conf_link(
|
8862
|
+
link,
|
8863
|
+
tags,
|
8864
|
+
app_conf_dir,
|
8865
|
+
conf_link_dir,
|
8866
|
+
)
|
8990
8867
|
|
8991
8868
|
|
8992
8869
|
########################################
|
8993
|
-
# ../deploy/paths/
|
8870
|
+
# ../deploy/paths/owners.py
|
8994
8871
|
|
8995
8872
|
|
8996
|
-
class
|
8873
|
+
class DeployPathOwner(abc.ABC):
|
8874
|
+
@abc.abstractmethod
|
8875
|
+
def get_owned_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
|
8876
|
+
raise NotImplementedError
|
8877
|
+
|
8878
|
+
|
8879
|
+
DeployPathOwners = ta.NewType('DeployPathOwners', ta.Sequence[DeployPathOwner])
|
8880
|
+
|
8881
|
+
|
8882
|
+
class SingleDirDeployPathOwner(DeployPathOwner, abc.ABC):
|
8997
8883
|
def __init__(
|
8998
8884
|
self,
|
8999
|
-
|
8885
|
+
*args: ta.Any,
|
8886
|
+
owned_dir: str,
|
9000
8887
|
deploy_home: ta.Optional[DeployHome],
|
9001
|
-
|
8888
|
+
**kwargs: ta.Any,
|
9002
8889
|
) -> None:
|
9003
|
-
super().__init__()
|
8890
|
+
super().__init__(*args, **kwargs)
|
8891
|
+
|
8892
|
+
check.not_in('/', owned_dir)
|
8893
|
+
self._owned_dir: str = check.non_empty_str(owned_dir)
|
9004
8894
|
|
9005
8895
|
self._deploy_home = deploy_home
|
9006
|
-
|
8896
|
+
|
8897
|
+
self._owned_deploy_paths = frozenset([DeployPath.parse(self._owned_dir + '/')])
|
9007
8898
|
|
9008
8899
|
@cached_nullary
|
9009
|
-
def
|
9010
|
-
|
9011
|
-
for o in self._deploy_path_owners:
|
9012
|
-
for p in o.get_owned_deploy_paths():
|
9013
|
-
if p in dct:
|
9014
|
-
raise DeployPathError(f'Duplicate deploy path owner: {p}')
|
9015
|
-
dct[p] = o
|
9016
|
-
return dct
|
8900
|
+
def _dir(self) -> str:
|
8901
|
+
return os.path.join(check.non_empty_str(self._deploy_home), self._owned_dir)
|
9017
8902
|
|
9018
|
-
|
9019
|
-
|
8903
|
+
@cached_nullary
|
8904
|
+
def _make_dir(self) -> str:
|
8905
|
+
if not os.path.isdir(d := self._dir()):
|
8906
|
+
os.makedirs(d, exist_ok=True)
|
8907
|
+
return d
|
8908
|
+
|
8909
|
+
def get_owned_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
|
8910
|
+
return self._owned_deploy_paths
|
9020
8911
|
|
9021
8912
|
|
9022
8913
|
########################################
|
9023
|
-
# ../
|
8914
|
+
# ../remote/_main.py
|
9024
8915
|
|
9025
8916
|
|
9026
|
-
|
9027
|
-
SingleDirDeployPathOwner,
|
9028
|
-
AtomicPathSwapping,
|
9029
|
-
):
|
9030
|
-
def __init__(
|
9031
|
-
self,
|
9032
|
-
*,
|
9033
|
-
deploy_home: ta.Optional[DeployHome] = None,
|
9034
|
-
) -> None:
|
9035
|
-
super().__init__(
|
9036
|
-
owned_dir='tmp',
|
9037
|
-
deploy_home=deploy_home,
|
9038
|
-
)
|
8917
|
+
##
|
9039
8918
|
|
9040
|
-
@cached_nullary
|
9041
|
-
def _swapping(self) -> AtomicPathSwapping:
|
9042
|
-
return TempDirAtomicPathSwapping(
|
9043
|
-
temp_dir=self._make_dir(),
|
9044
|
-
root_dir=check.non_empty_str(self._deploy_home),
|
9045
|
-
)
|
9046
8919
|
|
9047
|
-
|
9048
|
-
|
9049
|
-
|
9050
|
-
|
9051
|
-
|
9052
|
-
|
9053
|
-
|
9054
|
-
|
9055
|
-
dst_path,
|
9056
|
-
**kwargs,
|
9057
|
-
)
|
8920
|
+
class _RemoteExecutionLogHandler(logging.Handler):
|
8921
|
+
def __init__(self, fn: ta.Callable[[str], None]) -> None:
|
8922
|
+
super().__init__()
|
8923
|
+
self._fn = fn
|
8924
|
+
|
8925
|
+
def emit(self, record):
|
8926
|
+
msg = self.format(record)
|
8927
|
+
self._fn(msg)
|
9058
8928
|
|
9059
8929
|
|
9060
|
-
|
9061
|
-
# ../deploy/venvs.py
|
9062
|
-
"""
|
9063
|
-
TODO:
|
9064
|
-
- interp
|
9065
|
-
- share more code with pyproject?
|
9066
|
-
"""
|
8930
|
+
##
|
9067
8931
|
|
9068
8932
|
|
9069
|
-
class
|
8933
|
+
class _RemoteExecutionMain:
|
9070
8934
|
def __init__(
|
9071
8935
|
self,
|
9072
|
-
|
9073
|
-
atomics: AtomicPathSwapping,
|
9074
|
-
) -> None:
|
9075
|
-
super().__init__()
|
9076
|
-
|
9077
|
-
self._atomics = atomics
|
9078
|
-
|
9079
|
-
async def setup_venv(
|
9080
|
-
self,
|
9081
|
-
spec: DeployVenvSpec,
|
9082
|
-
git_dir: str,
|
9083
|
-
venv_dir: str,
|
9084
|
-
) -> None:
|
9085
|
-
sys_exe = 'python3'
|
9086
|
-
|
9087
|
-
# !! NOTE: (most) venvs cannot be relocated, so an atomic swap can't be used. it's up to the path manager to
|
9088
|
-
# garbage collect orphaned dirs.
|
9089
|
-
await asyncio_subprocesses.check_call(sys_exe, '-m', 'venv', venv_dir)
|
9090
|
-
|
9091
|
-
#
|
9092
|
-
|
9093
|
-
venv_exe = os.path.join(venv_dir, 'bin', 'python3')
|
9094
|
-
|
9095
|
-
#
|
9096
|
-
|
9097
|
-
reqs_txt = os.path.join(git_dir, 'requirements.txt')
|
9098
|
-
|
9099
|
-
if os.path.isfile(reqs_txt):
|
9100
|
-
if spec.use_uv:
|
9101
|
-
await asyncio_subprocesses.check_call(venv_exe, '-m', 'pip', 'install', 'uv')
|
9102
|
-
pip_cmd = ['-m', 'uv', 'pip']
|
9103
|
-
else:
|
9104
|
-
pip_cmd = ['-m', 'pip']
|
9105
|
-
|
9106
|
-
await asyncio_subprocesses.check_call(venv_exe, *pip_cmd,'install', '-r', reqs_txt)
|
9107
|
-
|
9108
|
-
|
9109
|
-
########################################
|
9110
|
-
# ../remote/_main.py
|
9111
|
-
|
9112
|
-
|
9113
|
-
##
|
9114
|
-
|
9115
|
-
|
9116
|
-
class _RemoteExecutionLogHandler(logging.Handler):
|
9117
|
-
def __init__(self, fn: ta.Callable[[str], None]) -> None:
|
9118
|
-
super().__init__()
|
9119
|
-
self._fn = fn
|
9120
|
-
|
9121
|
-
def emit(self, record):
|
9122
|
-
msg = self.format(record)
|
9123
|
-
self._fn(msg)
|
9124
|
-
|
9125
|
-
|
9126
|
-
##
|
9127
|
-
|
9128
|
-
|
9129
|
-
class _RemoteExecutionMain:
|
9130
|
-
def __init__(
|
9131
|
-
self,
|
9132
|
-
chan: RemoteChannel,
|
8936
|
+
chan: RemoteChannel,
|
9133
8937
|
) -> None:
|
9134
8938
|
super().__init__()
|
9135
8939
|
|
@@ -9639,182 +9443,187 @@ def bind_commands(
|
|
9639
9443
|
|
9640
9444
|
|
9641
9445
|
########################################
|
9642
|
-
# ../deploy/
|
9446
|
+
# ../deploy/git.py
|
9447
|
+
"""
|
9448
|
+
TODO:
|
9449
|
+
- 'repos'?
|
9450
|
+
|
9451
|
+
git/github.com/wrmsr/omlish <- bootstrap repo
|
9452
|
+
- shallow clone off bootstrap into /apps
|
9453
|
+
|
9454
|
+
github.com/wrmsr/omlish@rev
|
9455
|
+
"""
|
9643
9456
|
|
9644
9457
|
|
9645
|
-
|
9458
|
+
##
|
9459
|
+
|
9460
|
+
|
9461
|
+
class DeployGitManager(SingleDirDeployPathOwner):
|
9646
9462
|
def __init__(
|
9647
9463
|
self,
|
9648
9464
|
*,
|
9649
9465
|
deploy_home: ta.Optional[DeployHome] = None,
|
9650
|
-
|
9651
|
-
conf: DeployConfManager,
|
9652
|
-
git: DeployGitManager,
|
9653
|
-
venvs: DeployVenvManager,
|
9466
|
+
atomics: AtomicPathSwapping,
|
9654
9467
|
) -> None:
|
9655
|
-
super().__init__(
|
9656
|
-
|
9657
|
-
|
9658
|
-
|
9659
|
-
self._conf = conf
|
9660
|
-
self._git = git
|
9661
|
-
self._venvs = venvs
|
9468
|
+
super().__init__(
|
9469
|
+
owned_dir='git',
|
9470
|
+
deploy_home=deploy_home,
|
9471
|
+
)
|
9662
9472
|
|
9663
|
-
|
9473
|
+
self._atomics = atomics
|
9664
9474
|
|
9665
|
-
|
9666
|
-
_APP_DIR = DeployPath.parse(_APP_DIR_STR)
|
9475
|
+
self._repo_dirs: ta.Dict[DeployGitRepo, DeployGitManager.RepoDir] = {}
|
9667
9476
|
|
9668
|
-
|
9669
|
-
|
9477
|
+
class RepoDir:
|
9478
|
+
def __init__(
|
9479
|
+
self,
|
9480
|
+
git: 'DeployGitManager',
|
9481
|
+
repo: DeployGitRepo,
|
9482
|
+
) -> None:
|
9483
|
+
super().__init__()
|
9670
9484
|
|
9671
|
-
|
9672
|
-
|
9485
|
+
self._git = git
|
9486
|
+
self._repo = repo
|
9487
|
+
self._dir = os.path.join(
|
9488
|
+
self._git._make_dir(), # noqa
|
9489
|
+
check.non_empty_str(repo.host),
|
9490
|
+
check.non_empty_str(repo.path),
|
9491
|
+
)
|
9673
9492
|
|
9674
|
-
|
9675
|
-
|
9676
|
-
|
9677
|
-
self._APP_DIR,
|
9493
|
+
@property
|
9494
|
+
def repo(self) -> DeployGitRepo:
|
9495
|
+
return self._repo
|
9678
9496
|
|
9679
|
-
|
9497
|
+
@property
|
9498
|
+
def url(self) -> str:
|
9499
|
+
if self._repo.username is not None:
|
9500
|
+
return f'{self._repo.username}@{self._repo.host}:{self._repo.path}'
|
9501
|
+
else:
|
9502
|
+
return f'https://{self._repo.host}/{self._repo.path}'
|
9680
9503
|
|
9681
|
-
|
9682
|
-
self._CONF_DEPLOY_DIR,
|
9504
|
+
#
|
9683
9505
|
|
9684
|
-
|
9685
|
-
|
9686
|
-
|
9687
|
-
|
9688
|
-
|
9689
|
-
'venv',
|
9690
|
-
]
|
9691
|
-
],
|
9692
|
-
}
|
9506
|
+
async def _call(self, *cmd: str) -> None:
|
9507
|
+
await asyncio_subprocesses.check_call(
|
9508
|
+
*cmd,
|
9509
|
+
cwd=self._dir,
|
9510
|
+
)
|
9693
9511
|
|
9694
|
-
|
9512
|
+
#
|
9695
9513
|
|
9696
|
-
|
9697
|
-
|
9698
|
-
|
9699
|
-
|
9700
|
-
|
9701
|
-
deploy_home = check.non_empty_str(self._deploy_home)
|
9514
|
+
@async_cached_nullary
|
9515
|
+
async def init(self) -> None:
|
9516
|
+
os.makedirs(self._dir, exist_ok=True)
|
9517
|
+
if os.path.exists(os.path.join(self._dir, '.git')):
|
9518
|
+
return
|
9702
9519
|
|
9703
|
-
|
9704
|
-
|
9520
|
+
await self._call('git', 'init')
|
9521
|
+
await self._call('git', 'remote', 'add', 'origin', self.url)
|
9705
9522
|
|
9706
|
-
|
9707
|
-
|
9708
|
-
|
9523
|
+
async def fetch(self, rev: DeployRev) -> None:
|
9524
|
+
await self.init()
|
9525
|
+
await self._call('git', 'fetch', '--depth=1', 'origin', rev)
|
9709
9526
|
|
9710
9527
|
#
|
9711
9528
|
|
9712
|
-
|
9713
|
-
|
9714
|
-
|
9715
|
-
|
9716
|
-
|
9717
|
-
|
9718
|
-
|
9719
|
-
|
9720
|
-
|
9529
|
+
async def checkout(self, spec: DeployGitSpec, dst_dir: str) -> None:
|
9530
|
+
check.state(not os.path.exists(dst_dir))
|
9531
|
+
with self._git._atomics.begin_atomic_path_swap( # noqa
|
9532
|
+
'dir',
|
9533
|
+
dst_dir,
|
9534
|
+
auto_commit=True,
|
9535
|
+
make_dirs=True,
|
9536
|
+
) as dst_swap:
|
9537
|
+
await self.fetch(spec.rev)
|
9721
9538
|
|
9722
|
-
|
9539
|
+
dst_call = functools.partial(asyncio_subprocesses.check_call, cwd=dst_swap.tmp_path)
|
9540
|
+
await dst_call('git', 'init')
|
9723
9541
|
|
9724
|
-
|
9725
|
-
|
9726
|
-
|
9727
|
-
app_deploy_link,
|
9728
|
-
target_is_directory=True,
|
9729
|
-
make_dirs=True,
|
9730
|
-
)
|
9542
|
+
await dst_call('git', 'remote', 'add', 'local', self._dir)
|
9543
|
+
await dst_call('git', 'fetch', '--depth=1', 'local', spec.rev)
|
9544
|
+
await dst_call('git', 'checkout', spec.rev, *(spec.subtrees or []))
|
9731
9545
|
|
9732
|
-
|
9546
|
+
def get_repo_dir(self, repo: DeployGitRepo) -> RepoDir:
|
9547
|
+
try:
|
9548
|
+
return self._repo_dirs[repo]
|
9549
|
+
except KeyError:
|
9550
|
+
repo_dir = self._repo_dirs[repo] = DeployGitManager.RepoDir(self, repo)
|
9551
|
+
return repo_dir
|
9733
9552
|
|
9734
|
-
|
9735
|
-
|
9553
|
+
async def checkout(
|
9554
|
+
self,
|
9555
|
+
spec: DeployGitSpec,
|
9556
|
+
dst_dir: str,
|
9557
|
+
) -> None:
|
9558
|
+
await self.get_repo_dir(spec.repo).checkout(spec, dst_dir)
|
9736
9559
|
|
9737
|
-
#
|
9738
9560
|
|
9739
|
-
|
9740
|
-
|
9741
|
-
# check.state(os.path.islink(lp))
|
9742
|
-
# shutil.copy2(
|
9743
|
-
# lp,
|
9744
|
-
# os.path.join(dst, os.path.relpath(lp, src)),
|
9745
|
-
# follow_symlinks=False,
|
9746
|
-
# )
|
9747
|
-
#
|
9748
|
-
# for dp, dns, fns in os.walk(src, followlinks=False):
|
9749
|
-
# for fn in fns:
|
9750
|
-
# mirror_link(os.path.join(dp, fn))
|
9751
|
-
#
|
9752
|
-
# for dn in dns:
|
9753
|
-
# dp2 = os.path.join(dp, dn)
|
9754
|
-
# if os.path.islink(dp2):
|
9755
|
-
# mirror_link(dp2)
|
9756
|
-
# else:
|
9757
|
-
# os.makedirs(os.path.join(dst, os.path.relpath(dp2, src)))
|
9561
|
+
########################################
|
9562
|
+
# ../deploy/paths/manager.py
|
9758
9563
|
|
9759
|
-
current_link = os.path.join(deploy_home, 'deploys/current')
|
9760
9564
|
|
9761
|
-
|
9762
|
-
|
9763
|
-
|
9764
|
-
|
9765
|
-
|
9766
|
-
|
9767
|
-
|
9768
|
-
|
9769
|
-
# )
|
9565
|
+
class DeployPathsManager:
|
9566
|
+
def __init__(
|
9567
|
+
self,
|
9568
|
+
*,
|
9569
|
+
deploy_home: ta.Optional[DeployHome],
|
9570
|
+
deploy_path_owners: DeployPathOwners,
|
9571
|
+
) -> None:
|
9572
|
+
super().__init__()
|
9770
9573
|
|
9771
|
-
|
9574
|
+
self._deploy_home = deploy_home
|
9575
|
+
self._deploy_path_owners = deploy_path_owners
|
9772
9576
|
|
9773
|
-
|
9774
|
-
|
9775
|
-
|
9776
|
-
|
9777
|
-
|
9778
|
-
|
9779
|
-
|
9780
|
-
|
9781
|
-
|
9782
|
-
app_venv_dir = os.path.join(app_dir, 'venv')
|
9783
|
-
await self._venvs.setup_venv(
|
9784
|
-
spec.venv,
|
9785
|
-
app_git_dir,
|
9786
|
-
app_venv_dir,
|
9787
|
-
)
|
9788
|
-
|
9789
|
-
#
|
9790
|
-
|
9791
|
-
if spec.conf is not None:
|
9792
|
-
app_conf_dir = os.path.join(app_dir, 'conf')
|
9793
|
-
await self._conf.write_app_conf(
|
9794
|
-
spec.conf,
|
9795
|
-
tags,
|
9796
|
-
app_conf_dir,
|
9797
|
-
deploy_conf_dir,
|
9798
|
-
)
|
9799
|
-
|
9800
|
-
#
|
9577
|
+
@cached_nullary
|
9578
|
+
def owners_by_path(self) -> ta.Mapping[DeployPath, DeployPathOwner]:
|
9579
|
+
dct: ta.Dict[DeployPath, DeployPathOwner] = {}
|
9580
|
+
for o in self._deploy_path_owners:
|
9581
|
+
for p in o.get_owned_deploy_paths():
|
9582
|
+
if p in dct:
|
9583
|
+
raise DeployPathError(f'Duplicate deploy path owner: {p}')
|
9584
|
+
dct[p] = o
|
9585
|
+
return dct
|
9801
9586
|
|
9802
|
-
|
9587
|
+
def validate_deploy_paths(self) -> None:
|
9588
|
+
self.owners_by_path()
|
9803
9589
|
|
9804
9590
|
|
9805
9591
|
########################################
|
9806
|
-
# ../deploy/
|
9592
|
+
# ../deploy/tmp.py
|
9807
9593
|
|
9808
9594
|
|
9809
|
-
|
9810
|
-
|
9811
|
-
|
9812
|
-
|
9595
|
+
class DeployTmpManager(
|
9596
|
+
SingleDirDeployPathOwner,
|
9597
|
+
AtomicPathSwapping,
|
9598
|
+
):
|
9599
|
+
def __init__(
|
9600
|
+
self,
|
9601
|
+
*,
|
9602
|
+
deploy_home: ta.Optional[DeployHome] = None,
|
9603
|
+
) -> None:
|
9604
|
+
super().__init__(
|
9605
|
+
owned_dir='tmp',
|
9606
|
+
deploy_home=deploy_home,
|
9607
|
+
)
|
9813
9608
|
|
9814
|
-
|
9815
|
-
|
9609
|
+
@cached_nullary
|
9610
|
+
def _swapping(self) -> AtomicPathSwapping:
|
9611
|
+
return TempDirAtomicPathSwapping(
|
9612
|
+
temp_dir=self._make_dir(),
|
9613
|
+
root_dir=check.non_empty_str(self._deploy_home),
|
9614
|
+
)
|
9816
9615
|
|
9817
|
-
|
9616
|
+
def begin_atomic_path_swap(
|
9617
|
+
self,
|
9618
|
+
kind: AtomicPathSwapKind,
|
9619
|
+
dst_path: str,
|
9620
|
+
**kwargs: ta.Any,
|
9621
|
+
) -> AtomicPathSwap:
|
9622
|
+
return self._swapping().begin_atomic_path_swap(
|
9623
|
+
kind,
|
9624
|
+
dst_path,
|
9625
|
+
**kwargs,
|
9626
|
+
)
|
9818
9627
|
|
9819
9628
|
|
9820
9629
|
########################################
|
@@ -10532,90 +10341,358 @@ class SystemInterpProvider(InterpProvider):
|
|
10532
10341
|
|
10533
10342
|
|
10534
10343
|
########################################
|
10535
|
-
# ../deploy/
|
10344
|
+
# ../deploy/paths/inject.py
|
10536
10345
|
|
10537
10346
|
|
10538
|
-
|
10347
|
+
def bind_deploy_paths() -> InjectorBindings:
|
10348
|
+
lst: ta.List[InjectorBindingOrBindings] = [
|
10349
|
+
inj.bind_array(DeployPathOwner),
|
10350
|
+
inj.bind_array_type(DeployPathOwner, DeployPathOwners),
|
10539
10351
|
|
10352
|
+
inj.bind(DeployPathsManager, singleton=True),
|
10353
|
+
]
|
10540
10354
|
|
10541
|
-
|
10355
|
+
return inj.as_bindings(*lst)
|
10542
10356
|
|
10543
10357
|
|
10544
|
-
|
10545
|
-
|
10546
|
-
self,
|
10547
|
-
*,
|
10548
|
-
apps: DeployAppManager,
|
10549
|
-
paths: DeployPathsManager,
|
10358
|
+
########################################
|
10359
|
+
# ../remote/inject.py
|
10550
10360
|
|
10551
|
-
utc_clock: ta.Optional[DeployManagerUtcClock] = None,
|
10552
|
-
):
|
10553
|
-
super().__init__()
|
10554
10361
|
|
10555
|
-
|
10556
|
-
|
10362
|
+
def bind_remote(
|
10363
|
+
*,
|
10364
|
+
remote_config: RemoteConfig,
|
10365
|
+
) -> InjectorBindings:
|
10366
|
+
lst: ta.List[InjectorBindingOrBindings] = [
|
10367
|
+
inj.bind(remote_config),
|
10557
10368
|
|
10558
|
-
|
10369
|
+
inj.bind(SubprocessRemoteSpawning, singleton=True),
|
10370
|
+
inj.bind(RemoteSpawning, to_key=SubprocessRemoteSpawning),
|
10559
10371
|
|
10560
|
-
|
10561
|
-
|
10562
|
-
|
10372
|
+
inj.bind(PyremoteRemoteExecutionConnector, singleton=True),
|
10373
|
+
inj.bind(InProcessRemoteExecutionConnector, singleton=True),
|
10374
|
+
]
|
10375
|
+
|
10376
|
+
#
|
10377
|
+
|
10378
|
+
if (pf := remote_config.payload_file) is not None:
|
10379
|
+
lst.append(inj.bind(pf, key=RemoteExecutionPayloadFile))
|
10380
|
+
|
10381
|
+
#
|
10382
|
+
|
10383
|
+
return inj.as_bindings(*lst)
|
10384
|
+
|
10385
|
+
|
10386
|
+
########################################
|
10387
|
+
# ../system/inject.py
|
10388
|
+
|
10389
|
+
|
10390
|
+
def bind_system(
|
10391
|
+
*,
|
10392
|
+
system_config: SystemConfig,
|
10393
|
+
) -> InjectorBindings:
|
10394
|
+
lst: ta.List[InjectorBindingOrBindings] = [
|
10395
|
+
inj.bind(system_config),
|
10396
|
+
]
|
10397
|
+
|
10398
|
+
#
|
10399
|
+
|
10400
|
+
platform = system_config.platform or detect_system_platform()
|
10401
|
+
lst.append(inj.bind(platform, key=Platform))
|
10402
|
+
|
10403
|
+
#
|
10404
|
+
|
10405
|
+
if isinstance(platform, AmazonLinuxPlatform):
|
10406
|
+
lst.extend([
|
10407
|
+
inj.bind(YumSystemPackageManager, singleton=True),
|
10408
|
+
inj.bind(SystemPackageManager, to_key=YumSystemPackageManager),
|
10409
|
+
])
|
10410
|
+
|
10411
|
+
elif isinstance(platform, LinuxPlatform):
|
10412
|
+
lst.extend([
|
10413
|
+
inj.bind(AptSystemPackageManager, singleton=True),
|
10414
|
+
inj.bind(SystemPackageManager, to_key=AptSystemPackageManager),
|
10415
|
+
])
|
10416
|
+
|
10417
|
+
elif isinstance(platform, DarwinPlatform):
|
10418
|
+
lst.extend([
|
10419
|
+
inj.bind(BrewSystemPackageManager, singleton=True),
|
10420
|
+
inj.bind(SystemPackageManager, to_key=BrewSystemPackageManager),
|
10421
|
+
])
|
10422
|
+
|
10423
|
+
#
|
10424
|
+
|
10425
|
+
lst.extend([
|
10426
|
+
bind_command(CheckSystemPackageCommand, CheckSystemPackageCommandExecutor),
|
10427
|
+
])
|
10428
|
+
|
10429
|
+
#
|
10430
|
+
|
10431
|
+
return inj.as_bindings(*lst)
|
10432
|
+
|
10433
|
+
|
10434
|
+
########################################
|
10435
|
+
# ../targets/connection.py
|
10436
|
+
|
10437
|
+
|
10438
|
+
##
|
10439
|
+
|
10440
|
+
|
10441
|
+
class ManageTargetConnector(abc.ABC):
|
10442
|
+
@abc.abstractmethod
|
10443
|
+
def connect(self, tgt: ManageTarget) -> ta.AsyncContextManager[CommandExecutor]:
|
10444
|
+
raise NotImplementedError
|
10445
|
+
|
10446
|
+
def _default_python(self, python: ta.Optional[ta.Sequence[str]]) -> ta.Sequence[str]:
|
10447
|
+
check.not_isinstance(python, str)
|
10448
|
+
if python is not None:
|
10449
|
+
return python
|
10563
10450
|
else:
|
10564
|
-
return
|
10451
|
+
return ['sh', '-c', get_best_python_sh(), '--']
|
10565
10452
|
|
10566
|
-
def _make_deploy_time(self) -> DeployTime:
|
10567
|
-
return DeployTime(self._utc_now().strftime(DEPLOY_TAG_DATETIME_FMT))
|
10568
10453
|
|
10569
|
-
|
10454
|
+
##
|
10455
|
+
|
10456
|
+
|
10457
|
+
ManageTargetConnectorMap = ta.NewType('ManageTargetConnectorMap', ta.Mapping[ta.Type[ManageTarget], ManageTargetConnector]) # noqa
|
10458
|
+
|
10459
|
+
|
10460
|
+
@dc.dataclass(frozen=True)
|
10461
|
+
class TypeSwitchedManageTargetConnector(ManageTargetConnector):
|
10462
|
+
connectors: ManageTargetConnectorMap
|
10463
|
+
|
10464
|
+
def get_connector(self, ty: ta.Type[ManageTarget]) -> ManageTargetConnector:
|
10465
|
+
for k, v in self.connectors.items():
|
10466
|
+
if issubclass(ty, k):
|
10467
|
+
return v
|
10468
|
+
raise KeyError(ty)
|
10469
|
+
|
10470
|
+
def connect(self, tgt: ManageTarget) -> ta.AsyncContextManager[CommandExecutor]:
|
10471
|
+
return self.get_connector(type(tgt)).connect(tgt)
|
10472
|
+
|
10473
|
+
|
10474
|
+
##
|
10475
|
+
|
10476
|
+
|
10477
|
+
@dc.dataclass(frozen=True)
|
10478
|
+
class LocalManageTargetConnector(ManageTargetConnector):
|
10479
|
+
_local_executor: LocalCommandExecutor
|
10480
|
+
_in_process_connector: InProcessRemoteExecutionConnector
|
10481
|
+
_pyremote_connector: PyremoteRemoteExecutionConnector
|
10482
|
+
_bootstrap: MainBootstrap
|
10483
|
+
|
10484
|
+
@contextlib.asynccontextmanager
|
10485
|
+
async def connect(self, tgt: ManageTarget) -> ta.AsyncGenerator[CommandExecutor, None]:
|
10486
|
+
lmt = check.isinstance(tgt, LocalManageTarget)
|
10487
|
+
|
10488
|
+
if isinstance(lmt, InProcessManageTarget):
|
10489
|
+
imt = check.isinstance(lmt, InProcessManageTarget)
|
10490
|
+
|
10491
|
+
if imt.mode == InProcessManageTarget.Mode.DIRECT:
|
10492
|
+
yield self._local_executor
|
10493
|
+
|
10494
|
+
elif imt.mode == InProcessManageTarget.Mode.FAKE_REMOTE:
|
10495
|
+
async with self._in_process_connector.connect() as rce:
|
10496
|
+
yield rce
|
10497
|
+
|
10498
|
+
else:
|
10499
|
+
raise TypeError(imt.mode)
|
10500
|
+
|
10501
|
+
elif isinstance(lmt, SubprocessManageTarget):
|
10502
|
+
async with self._pyremote_connector.connect(
|
10503
|
+
RemoteSpawning.Target(
|
10504
|
+
python=self._default_python(lmt.python),
|
10505
|
+
),
|
10506
|
+
self._bootstrap,
|
10507
|
+
) as rce:
|
10508
|
+
yield rce
|
10509
|
+
|
10510
|
+
else:
|
10511
|
+
raise TypeError(lmt)
|
10512
|
+
|
10513
|
+
|
10514
|
+
##
|
10515
|
+
|
10516
|
+
|
10517
|
+
@dc.dataclass(frozen=True)
|
10518
|
+
class DockerManageTargetConnector(ManageTargetConnector):
|
10519
|
+
_pyremote_connector: PyremoteRemoteExecutionConnector
|
10520
|
+
_bootstrap: MainBootstrap
|
10521
|
+
|
10522
|
+
@contextlib.asynccontextmanager
|
10523
|
+
async def connect(self, tgt: ManageTarget) -> ta.AsyncGenerator[CommandExecutor, None]:
|
10524
|
+
dmt = check.isinstance(tgt, DockerManageTarget)
|
10525
|
+
|
10526
|
+
sh_parts: ta.List[str] = ['docker']
|
10527
|
+
if dmt.image is not None:
|
10528
|
+
sh_parts.extend(['run', '-i', dmt.image])
|
10529
|
+
elif dmt.container_id is not None:
|
10530
|
+
sh_parts.extend(['exec', '-i', dmt.container_id])
|
10531
|
+
else:
|
10532
|
+
raise ValueError(dmt)
|
10533
|
+
|
10534
|
+
async with self._pyremote_connector.connect(
|
10535
|
+
RemoteSpawning.Target(
|
10536
|
+
shell=' '.join(sh_parts),
|
10537
|
+
python=self._default_python(dmt.python),
|
10538
|
+
),
|
10539
|
+
self._bootstrap,
|
10540
|
+
) as rce:
|
10541
|
+
yield rce
|
10542
|
+
|
10543
|
+
|
10544
|
+
##
|
10545
|
+
|
10546
|
+
|
10547
|
+
@dc.dataclass(frozen=True)
|
10548
|
+
class SshManageTargetConnector(ManageTargetConnector):
|
10549
|
+
_pyremote_connector: PyremoteRemoteExecutionConnector
|
10550
|
+
_bootstrap: MainBootstrap
|
10551
|
+
|
10552
|
+
@contextlib.asynccontextmanager
|
10553
|
+
async def connect(self, tgt: ManageTarget) -> ta.AsyncGenerator[CommandExecutor, None]:
|
10554
|
+
smt = check.isinstance(tgt, SshManageTarget)
|
10555
|
+
|
10556
|
+
sh_parts: ta.List[str] = ['ssh']
|
10557
|
+
if smt.key_file is not None:
|
10558
|
+
sh_parts.extend(['-i', smt.key_file])
|
10559
|
+
addr = check.not_none(smt.host)
|
10560
|
+
if smt.username is not None:
|
10561
|
+
addr = f'{smt.username}@{addr}'
|
10562
|
+
sh_parts.append(addr)
|
10563
|
+
|
10564
|
+
async with self._pyremote_connector.connect(
|
10565
|
+
RemoteSpawning.Target(
|
10566
|
+
shell=' '.join(sh_parts),
|
10567
|
+
shell_quote=True,
|
10568
|
+
python=self._default_python(smt.python),
|
10569
|
+
),
|
10570
|
+
self._bootstrap,
|
10571
|
+
) as rce:
|
10572
|
+
yield rce
|
10573
|
+
|
10574
|
+
|
10575
|
+
########################################
|
10576
|
+
# ../../../omdev/interp/resolvers.py
|
10577
|
+
|
10578
|
+
|
10579
|
+
INTERP_PROVIDER_TYPES_BY_NAME: ta.Mapping[str, ta.Type[InterpProvider]] = {
|
10580
|
+
cls.name: cls for cls in deep_subclasses(InterpProvider) if abc.ABC not in cls.__bases__ # type: ignore
|
10581
|
+
}
|
10582
|
+
|
10583
|
+
|
10584
|
+
class InterpResolver:
|
10585
|
+
def __init__(
|
10570
10586
|
self,
|
10571
|
-
|
10587
|
+
providers: ta.Sequence[ta.Tuple[str, InterpProvider]],
|
10572
10588
|
) -> None:
|
10573
|
-
|
10589
|
+
super().__init__()
|
10574
10590
|
|
10575
|
-
|
10591
|
+
self._providers: ta.Mapping[str, InterpProvider] = collections.OrderedDict(providers)
|
10576
10592
|
|
10577
|
-
|
10578
|
-
|
10579
|
-
|
10593
|
+
async def _resolve_installed(self, spec: InterpSpecifier) -> ta.Optional[ta.Tuple[InterpProvider, InterpVersion]]:
|
10594
|
+
lst = [
|
10595
|
+
(i, si)
|
10596
|
+
for i, p in enumerate(self._providers.values())
|
10597
|
+
for si in await p.get_installed_versions(spec)
|
10598
|
+
if spec.contains(si)
|
10599
|
+
]
|
10600
|
+
|
10601
|
+
slst = sorted(lst, key=lambda t: (-t[0], t[1].version))
|
10602
|
+
if not slst:
|
10603
|
+
return None
|
10604
|
+
|
10605
|
+
bi, bv = slst[-1]
|
10606
|
+
bp = list(self._providers.values())[bi]
|
10607
|
+
return (bp, bv)
|
10608
|
+
|
10609
|
+
async def resolve(
|
10610
|
+
self,
|
10611
|
+
spec: InterpSpecifier,
|
10612
|
+
*,
|
10613
|
+
install: bool = False,
|
10614
|
+
) -> ta.Optional[Interp]:
|
10615
|
+
tup = await self._resolve_installed(spec)
|
10616
|
+
if tup is not None:
|
10617
|
+
bp, bv = tup
|
10618
|
+
return await bp.get_installed_version(bv)
|
10619
|
+
|
10620
|
+
if not install:
|
10621
|
+
return None
|
10622
|
+
|
10623
|
+
tp = list(self._providers.values())[0] # noqa
|
10624
|
+
|
10625
|
+
sv = sorted(
|
10626
|
+
[s for s in await tp.get_installable_versions(spec) if s in spec],
|
10627
|
+
key=lambda s: s.version,
|
10580
10628
|
)
|
10629
|
+
if not sv:
|
10630
|
+
return None
|
10581
10631
|
|
10582
|
-
|
10632
|
+
bv = sv[-1]
|
10633
|
+
return await tp.install_version(bv)
|
10583
10634
|
|
10584
|
-
|
10585
|
-
|
10586
|
-
|
10587
|
-
|
10588
|
-
|
10589
|
-
|
10635
|
+
async def list(self, spec: InterpSpecifier) -> None:
|
10636
|
+
print('installed:')
|
10637
|
+
for n, p in self._providers.items():
|
10638
|
+
lst = [
|
10639
|
+
si
|
10640
|
+
for si in await p.get_installed_versions(spec)
|
10641
|
+
if spec.contains(si)
|
10642
|
+
]
|
10643
|
+
if lst:
|
10644
|
+
print(f' {n}')
|
10645
|
+
for si in lst:
|
10646
|
+
print(f' {si}')
|
10590
10647
|
|
10591
|
-
|
10592
|
-
|
10593
|
-
|
10594
|
-
|
10648
|
+
print()
|
10649
|
+
|
10650
|
+
print('installable:')
|
10651
|
+
for n, p in self._providers.items():
|
10652
|
+
lst = [
|
10653
|
+
si
|
10654
|
+
for si in await p.get_installable_versions(spec)
|
10655
|
+
if spec.contains(si)
|
10656
|
+
]
|
10657
|
+
if lst:
|
10658
|
+
print(f' {n}')
|
10659
|
+
for si in lst:
|
10660
|
+
print(f' {si}')
|
10661
|
+
|
10662
|
+
|
10663
|
+
DEFAULT_INTERP_RESOLVER = InterpResolver([(p.name, p) for p in [
|
10664
|
+
# pyenv is preferred to system interpreters as it tends to have more support for things like tkinter
|
10665
|
+
PyenvInterpProvider(try_update=True),
|
10666
|
+
|
10667
|
+
RunningInterpProvider(),
|
10668
|
+
|
10669
|
+
SystemInterpProvider(),
|
10670
|
+
]])
|
10595
10671
|
|
10596
10672
|
|
10597
10673
|
########################################
|
10598
|
-
# ../
|
10674
|
+
# ../targets/inject.py
|
10599
10675
|
|
10600
10676
|
|
10601
|
-
def
|
10602
|
-
*,
|
10603
|
-
remote_config: RemoteConfig,
|
10604
|
-
) -> InjectorBindings:
|
10677
|
+
def bind_targets() -> InjectorBindings:
|
10605
10678
|
lst: ta.List[InjectorBindingOrBindings] = [
|
10606
|
-
inj.bind(
|
10607
|
-
|
10608
|
-
inj.bind(
|
10609
|
-
inj.bind(RemoteSpawning, to_key=SubprocessRemoteSpawning),
|
10679
|
+
inj.bind(LocalManageTargetConnector, singleton=True),
|
10680
|
+
inj.bind(DockerManageTargetConnector, singleton=True),
|
10681
|
+
inj.bind(SshManageTargetConnector, singleton=True),
|
10610
10682
|
|
10611
|
-
inj.bind(
|
10612
|
-
inj.bind(
|
10683
|
+
inj.bind(TypeSwitchedManageTargetConnector, singleton=True),
|
10684
|
+
inj.bind(ManageTargetConnector, to_key=TypeSwitchedManageTargetConnector),
|
10613
10685
|
]
|
10614
10686
|
|
10615
10687
|
#
|
10616
10688
|
|
10617
|
-
|
10618
|
-
|
10689
|
+
def provide_manage_target_connector_map(injector: Injector) -> ManageTargetConnectorMap:
|
10690
|
+
return ManageTargetConnectorMap({
|
10691
|
+
LocalManageTarget: injector[LocalManageTargetConnector],
|
10692
|
+
DockerManageTarget: injector[DockerManageTargetConnector],
|
10693
|
+
SshManageTarget: injector[SshManageTargetConnector],
|
10694
|
+
})
|
10695
|
+
lst.append(inj.bind(provide_manage_target_connector_map, singleton=True))
|
10619
10696
|
|
10620
10697
|
#
|
10621
10698
|
|
@@ -10623,290 +10700,318 @@ def bind_remote(
|
|
10623
10700
|
|
10624
10701
|
|
10625
10702
|
########################################
|
10626
|
-
# ../
|
10703
|
+
# ../deploy/interp.py
|
10627
10704
|
|
10628
10705
|
|
10629
|
-
|
10630
|
-
*,
|
10631
|
-
system_config: SystemConfig,
|
10632
|
-
) -> InjectorBindings:
|
10633
|
-
lst: ta.List[InjectorBindingOrBindings] = [
|
10634
|
-
inj.bind(system_config),
|
10635
|
-
]
|
10706
|
+
##
|
10636
10707
|
|
10637
|
-
#
|
10638
10708
|
|
10639
|
-
|
10640
|
-
|
10709
|
+
@dc.dataclass(frozen=True)
|
10710
|
+
class InterpCommand(Command['InterpCommand.Output']):
|
10711
|
+
spec: str
|
10712
|
+
install: bool = False
|
10641
10713
|
|
10642
|
-
|
10714
|
+
@dc.dataclass(frozen=True)
|
10715
|
+
class Output(Command.Output):
|
10716
|
+
exe: str
|
10717
|
+
version: str
|
10718
|
+
opts: InterpOpts
|
10643
10719
|
|
10644
|
-
if isinstance(platform, AmazonLinuxPlatform):
|
10645
|
-
lst.extend([
|
10646
|
-
inj.bind(YumSystemPackageManager, singleton=True),
|
10647
|
-
inj.bind(SystemPackageManager, to_key=YumSystemPackageManager),
|
10648
|
-
])
|
10649
10720
|
|
10650
|
-
|
10651
|
-
|
10652
|
-
|
10653
|
-
|
10654
|
-
|
10721
|
+
class InterpCommandExecutor(CommandExecutor[InterpCommand, InterpCommand.Output]):
|
10722
|
+
async def execute(self, cmd: InterpCommand) -> InterpCommand.Output:
|
10723
|
+
i = InterpSpecifier.parse(check.not_none(cmd.spec))
|
10724
|
+
o = check.not_none(await DEFAULT_INTERP_RESOLVER.resolve(i, install=cmd.install))
|
10725
|
+
return InterpCommand.Output(
|
10726
|
+
exe=o.exe,
|
10727
|
+
version=str(o.version.version),
|
10728
|
+
opts=o.version.opts,
|
10729
|
+
)
|
10655
10730
|
|
10656
|
-
elif isinstance(platform, DarwinPlatform):
|
10657
|
-
lst.extend([
|
10658
|
-
inj.bind(BrewSystemPackageManager, singleton=True),
|
10659
|
-
inj.bind(SystemPackageManager, to_key=BrewSystemPackageManager),
|
10660
|
-
])
|
10661
10731
|
|
10662
|
-
|
10732
|
+
########################################
|
10733
|
+
# ../deploy/venvs.py
|
10734
|
+
"""
|
10735
|
+
TODO:
|
10736
|
+
- interp
|
10737
|
+
- share more code with pyproject?
|
10738
|
+
"""
|
10663
10739
|
|
10664
|
-
lst.extend([
|
10665
|
-
bind_command(CheckSystemPackageCommand, CheckSystemPackageCommandExecutor),
|
10666
|
-
])
|
10667
10740
|
|
10668
|
-
|
10741
|
+
class DeployVenvManager:
|
10742
|
+
def __init__(
|
10743
|
+
self,
|
10744
|
+
*,
|
10745
|
+
atomics: AtomicPathSwapping,
|
10746
|
+
) -> None:
|
10747
|
+
super().__init__()
|
10669
10748
|
|
10670
|
-
|
10749
|
+
self._atomics = atomics
|
10671
10750
|
|
10751
|
+
async def setup_venv(
|
10752
|
+
self,
|
10753
|
+
spec: DeployVenvSpec,
|
10754
|
+
git_dir: str,
|
10755
|
+
venv_dir: str,
|
10756
|
+
) -> None:
|
10757
|
+
if spec.interp is not None:
|
10758
|
+
i = InterpSpecifier.parse(check.not_none(spec.interp))
|
10759
|
+
o = check.not_none(await DEFAULT_INTERP_RESOLVER.resolve(i))
|
10760
|
+
sys_exe = o.exe
|
10761
|
+
else:
|
10762
|
+
sys_exe = 'python3'
|
10672
10763
|
|
10673
|
-
|
10674
|
-
# ../targets/connection.py
|
10764
|
+
#
|
10675
10765
|
|
10766
|
+
# !! NOTE: (most) venvs cannot be relocated, so an atomic swap can't be used. it's up to the path manager to
|
10767
|
+
# garbage collect orphaned dirs.
|
10768
|
+
await asyncio_subprocesses.check_call(sys_exe, '-m', 'venv', venv_dir)
|
10676
10769
|
|
10677
|
-
|
10770
|
+
#
|
10678
10771
|
|
10772
|
+
venv_exe = os.path.join(venv_dir, 'bin', 'python3')
|
10679
10773
|
|
10680
|
-
|
10681
|
-
@abc.abstractmethod
|
10682
|
-
def connect(self, tgt: ManageTarget) -> ta.AsyncContextManager[CommandExecutor]:
|
10683
|
-
raise NotImplementedError
|
10774
|
+
#
|
10684
10775
|
|
10685
|
-
|
10686
|
-
check.not_isinstance(python, str)
|
10687
|
-
if python is not None:
|
10688
|
-
return python
|
10689
|
-
else:
|
10690
|
-
return ['sh', '-c', get_best_python_sh(), '--']
|
10776
|
+
reqs_txt = os.path.join(git_dir, 'requirements.txt')
|
10691
10777
|
|
10778
|
+
if os.path.isfile(reqs_txt):
|
10779
|
+
if spec.use_uv:
|
10780
|
+
await asyncio_subprocesses.check_call(venv_exe, '-m', 'pip', 'install', 'uv')
|
10781
|
+
pip_cmd = ['-m', 'uv', 'pip']
|
10782
|
+
else:
|
10783
|
+
pip_cmd = ['-m', 'pip']
|
10692
10784
|
|
10693
|
-
|
10785
|
+
await asyncio_subprocesses.check_call(venv_exe, *pip_cmd,'install', '-r', reqs_txt)
|
10694
10786
|
|
10695
10787
|
|
10696
|
-
|
10788
|
+
########################################
|
10789
|
+
# ../deploy/apps.py
|
10697
10790
|
|
10698
10791
|
|
10699
|
-
|
10700
|
-
|
10701
|
-
|
10792
|
+
class DeployAppManager(DeployPathOwner):
|
10793
|
+
def __init__(
|
10794
|
+
self,
|
10795
|
+
*,
|
10796
|
+
deploy_home: ta.Optional[DeployHome] = None,
|
10702
10797
|
|
10703
|
-
|
10704
|
-
|
10705
|
-
|
10706
|
-
|
10707
|
-
|
10798
|
+
conf: DeployConfManager,
|
10799
|
+
git: DeployGitManager,
|
10800
|
+
venvs: DeployVenvManager,
|
10801
|
+
) -> None:
|
10802
|
+
super().__init__()
|
10708
10803
|
|
10709
|
-
|
10710
|
-
return self.get_connector(type(tgt)).connect(tgt)
|
10804
|
+
self._deploy_home = deploy_home
|
10711
10805
|
|
10806
|
+
self._conf = conf
|
10807
|
+
self._git = git
|
10808
|
+
self._venvs = venvs
|
10712
10809
|
|
10713
|
-
|
10810
|
+
#
|
10714
10811
|
|
10812
|
+
_APP_DIR_STR = 'apps/@app/@time--@app-rev--@app-key/'
|
10813
|
+
_APP_DIR = DeployPath.parse(_APP_DIR_STR)
|
10715
10814
|
|
10716
|
-
|
10717
|
-
|
10718
|
-
_local_executor: LocalCommandExecutor
|
10719
|
-
_in_process_connector: InProcessRemoteExecutionConnector
|
10720
|
-
_pyremote_connector: PyremoteRemoteExecutionConnector
|
10721
|
-
_bootstrap: MainBootstrap
|
10815
|
+
_DEPLOY_DIR_STR = 'deploys/@time--@deploy-key/'
|
10816
|
+
_DEPLOY_DIR = DeployPath.parse(_DEPLOY_DIR_STR)
|
10722
10817
|
|
10723
|
-
|
10724
|
-
|
10725
|
-
lmt = check.isinstance(tgt, LocalManageTarget)
|
10818
|
+
_APP_DEPLOY_LINK = DeployPath.parse(f'{_DEPLOY_DIR_STR}apps/@app')
|
10819
|
+
_CONF_DEPLOY_DIR = DeployPath.parse(f'{_DEPLOY_DIR_STR}conf/@conf/')
|
10726
10820
|
|
10727
|
-
|
10728
|
-
|
10821
|
+
@cached_nullary
|
10822
|
+
def get_owned_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
|
10823
|
+
return {
|
10824
|
+
self._APP_DIR,
|
10729
10825
|
|
10730
|
-
|
10731
|
-
yield self._local_executor
|
10826
|
+
self._DEPLOY_DIR,
|
10732
10827
|
|
10733
|
-
|
10734
|
-
|
10735
|
-
yield rce
|
10828
|
+
self._APP_DEPLOY_LINK,
|
10829
|
+
self._CONF_DEPLOY_DIR,
|
10736
10830
|
|
10737
|
-
|
10738
|
-
|
10831
|
+
*[
|
10832
|
+
DeployPath.parse(f'{self._APP_DIR_STR}{sfx}/')
|
10833
|
+
for sfx in [
|
10834
|
+
'conf',
|
10835
|
+
'git',
|
10836
|
+
'venv',
|
10837
|
+
]
|
10838
|
+
],
|
10839
|
+
}
|
10739
10840
|
|
10740
|
-
|
10741
|
-
async with self._pyremote_connector.connect(
|
10742
|
-
RemoteSpawning.Target(
|
10743
|
-
python=self._default_python(lmt.python),
|
10744
|
-
),
|
10745
|
-
self._bootstrap,
|
10746
|
-
) as rce:
|
10747
|
-
yield rce
|
10841
|
+
#
|
10748
10842
|
|
10749
|
-
|
10750
|
-
|
10843
|
+
async def prepare_app(
|
10844
|
+
self,
|
10845
|
+
spec: DeployAppSpec,
|
10846
|
+
tags: DeployTagMap,
|
10847
|
+
) -> None:
|
10848
|
+
deploy_home = check.non_empty_str(self._deploy_home)
|
10751
10849
|
|
10850
|
+
def build_path(pth: DeployPath) -> str:
|
10851
|
+
return os.path.join(deploy_home, pth.render(tags))
|
10752
10852
|
|
10753
|
-
|
10853
|
+
app_dir = build_path(self._APP_DIR)
|
10854
|
+
deploy_dir = build_path(self._DEPLOY_DIR)
|
10855
|
+
app_deploy_link = build_path(self._APP_DEPLOY_LINK)
|
10754
10856
|
|
10857
|
+
#
|
10755
10858
|
|
10756
|
-
|
10757
|
-
class DockerManageTargetConnector(ManageTargetConnector):
|
10758
|
-
_pyremote_connector: PyremoteRemoteExecutionConnector
|
10759
|
-
_bootstrap: MainBootstrap
|
10859
|
+
os.makedirs(deploy_dir, exist_ok=True)
|
10760
10860
|
|
10761
|
-
|
10762
|
-
|
10763
|
-
|
10861
|
+
deploying_link = os.path.join(deploy_home, 'deploys/deploying')
|
10862
|
+
if os.path.exists(deploying_link):
|
10863
|
+
os.unlink(deploying_link)
|
10864
|
+
relative_symlink(
|
10865
|
+
deploy_dir,
|
10866
|
+
deploying_link,
|
10867
|
+
target_is_directory=True,
|
10868
|
+
make_dirs=True,
|
10869
|
+
)
|
10764
10870
|
|
10765
|
-
|
10766
|
-
if dmt.image is not None:
|
10767
|
-
sh_parts.extend(['run', '-i', dmt.image])
|
10768
|
-
elif dmt.container_id is not None:
|
10769
|
-
sh_parts.extend(['exec', '-i', dmt.container_id])
|
10770
|
-
else:
|
10771
|
-
raise ValueError(dmt)
|
10871
|
+
#
|
10772
10872
|
|
10773
|
-
|
10774
|
-
|
10775
|
-
|
10776
|
-
|
10777
|
-
|
10778
|
-
|
10779
|
-
)
|
10780
|
-
|
10873
|
+
os.makedirs(app_dir)
|
10874
|
+
relative_symlink(
|
10875
|
+
app_dir,
|
10876
|
+
app_deploy_link,
|
10877
|
+
target_is_directory=True,
|
10878
|
+
make_dirs=True,
|
10879
|
+
)
|
10880
|
+
|
10881
|
+
#
|
10882
|
+
|
10883
|
+
deploy_conf_dir = os.path.join(deploy_dir, 'conf')
|
10884
|
+
os.makedirs(deploy_conf_dir, exist_ok=True)
|
10885
|
+
|
10886
|
+
#
|
10887
|
+
|
10888
|
+
# def mirror_symlinks(src: str, dst: str) -> None:
|
10889
|
+
# def mirror_link(lp: str) -> None:
|
10890
|
+
# check.state(os.path.islink(lp))
|
10891
|
+
# shutil.copy2(
|
10892
|
+
# lp,
|
10893
|
+
# os.path.join(dst, os.path.relpath(lp, src)),
|
10894
|
+
# follow_symlinks=False,
|
10895
|
+
# )
|
10896
|
+
#
|
10897
|
+
# for dp, dns, fns in os.walk(src, followlinks=False):
|
10898
|
+
# for fn in fns:
|
10899
|
+
# mirror_link(os.path.join(dp, fn))
|
10900
|
+
#
|
10901
|
+
# for dn in dns:
|
10902
|
+
# dp2 = os.path.join(dp, dn)
|
10903
|
+
# if os.path.islink(dp2):
|
10904
|
+
# mirror_link(dp2)
|
10905
|
+
# else:
|
10906
|
+
# os.makedirs(os.path.join(dst, os.path.relpath(dp2, src)))
|
10781
10907
|
|
10908
|
+
current_link = os.path.join(deploy_home, 'deploys/current')
|
10782
10909
|
|
10783
|
-
|
10910
|
+
# if os.path.exists(current_link):
|
10911
|
+
# mirror_symlinks(
|
10912
|
+
# os.path.join(current_link, 'conf'),
|
10913
|
+
# conf_tag_dir,
|
10914
|
+
# )
|
10915
|
+
# mirror_symlinks(
|
10916
|
+
# os.path.join(current_link, 'apps'),
|
10917
|
+
# os.path.join(deploy_dir, 'apps'),
|
10918
|
+
# )
|
10784
10919
|
|
10920
|
+
#
|
10785
10921
|
|
10786
|
-
|
10787
|
-
|
10788
|
-
|
10789
|
-
|
10922
|
+
app_git_dir = os.path.join(app_dir, 'git')
|
10923
|
+
await self._git.checkout(
|
10924
|
+
spec.git,
|
10925
|
+
app_git_dir,
|
10926
|
+
)
|
10790
10927
|
|
10791
|
-
|
10792
|
-
async def connect(self, tgt: ManageTarget) -> ta.AsyncGenerator[CommandExecutor, None]:
|
10793
|
-
smt = check.isinstance(tgt, SshManageTarget)
|
10928
|
+
#
|
10794
10929
|
|
10795
|
-
|
10796
|
-
|
10797
|
-
|
10798
|
-
|
10799
|
-
|
10800
|
-
|
10801
|
-
|
10930
|
+
if spec.venv is not None:
|
10931
|
+
app_venv_dir = os.path.join(app_dir, 'venv')
|
10932
|
+
await self._venvs.setup_venv(
|
10933
|
+
spec.venv,
|
10934
|
+
app_git_dir,
|
10935
|
+
app_venv_dir,
|
10936
|
+
)
|
10802
10937
|
|
10803
|
-
|
10804
|
-
RemoteSpawning.Target(
|
10805
|
-
shell=' '.join(sh_parts),
|
10806
|
-
shell_quote=True,
|
10807
|
-
python=self._default_python(smt.python),
|
10808
|
-
),
|
10809
|
-
self._bootstrap,
|
10810
|
-
) as rce:
|
10811
|
-
yield rce
|
10938
|
+
#
|
10812
10939
|
|
10940
|
+
if spec.conf is not None:
|
10941
|
+
app_conf_dir = os.path.join(app_dir, 'conf')
|
10942
|
+
await self._conf.write_app_conf(
|
10943
|
+
spec.conf,
|
10944
|
+
tags,
|
10945
|
+
app_conf_dir,
|
10946
|
+
deploy_conf_dir,
|
10947
|
+
)
|
10813
10948
|
|
10814
|
-
|
10815
|
-
# ../../../omdev/interp/resolvers.py
|
10949
|
+
#
|
10816
10950
|
|
10951
|
+
os.replace(deploying_link, current_link)
|
10817
10952
|
|
10818
|
-
INTERP_PROVIDER_TYPES_BY_NAME: ta.Mapping[str, ta.Type[InterpProvider]] = {
|
10819
|
-
cls.name: cls for cls in deep_subclasses(InterpProvider) if abc.ABC not in cls.__bases__ # type: ignore
|
10820
|
-
}
|
10821
10953
|
|
10954
|
+
########################################
|
10955
|
+
# ../deploy/deploy.py
|
10822
10956
|
|
10823
|
-
class InterpResolver:
|
10824
|
-
def __init__(
|
10825
|
-
self,
|
10826
|
-
providers: ta.Sequence[ta.Tuple[str, InterpProvider]],
|
10827
|
-
) -> None:
|
10828
|
-
super().__init__()
|
10829
10957
|
|
10830
|
-
|
10958
|
+
DEPLOY_TAG_DATETIME_FMT = '%Y%m%dT%H%M%SZ'
|
10831
10959
|
|
10832
|
-
async def _resolve_installed(self, spec: InterpSpecifier) -> ta.Optional[ta.Tuple[InterpProvider, InterpVersion]]:
|
10833
|
-
lst = [
|
10834
|
-
(i, si)
|
10835
|
-
for i, p in enumerate(self._providers.values())
|
10836
|
-
for si in await p.get_installed_versions(spec)
|
10837
|
-
if spec.contains(si)
|
10838
|
-
]
|
10839
10960
|
|
10840
|
-
|
10841
|
-
if not slst:
|
10842
|
-
return None
|
10961
|
+
DeployManagerUtcClock = ta.NewType('DeployManagerUtcClock', Func0[datetime.datetime])
|
10843
10962
|
|
10844
|
-
bi, bv = slst[-1]
|
10845
|
-
bp = list(self._providers.values())[bi]
|
10846
|
-
return (bp, bv)
|
10847
10963
|
|
10848
|
-
|
10964
|
+
class DeployManager:
|
10965
|
+
def __init__(
|
10849
10966
|
self,
|
10850
|
-
spec: InterpSpecifier,
|
10851
10967
|
*,
|
10852
|
-
|
10853
|
-
|
10854
|
-
tup = await self._resolve_installed(spec)
|
10855
|
-
if tup is not None:
|
10856
|
-
bp, bv = tup
|
10857
|
-
return await bp.get_installed_version(bv)
|
10968
|
+
apps: DeployAppManager,
|
10969
|
+
paths: DeployPathsManager,
|
10858
10970
|
|
10859
|
-
|
10860
|
-
|
10971
|
+
utc_clock: ta.Optional[DeployManagerUtcClock] = None,
|
10972
|
+
):
|
10973
|
+
super().__init__()
|
10861
10974
|
|
10862
|
-
|
10975
|
+
self._apps = apps
|
10976
|
+
self._paths = paths
|
10863
10977
|
|
10864
|
-
|
10865
|
-
[s for s in await tp.get_installable_versions(spec) if s in spec],
|
10866
|
-
key=lambda s: s.version,
|
10867
|
-
)
|
10868
|
-
if not sv:
|
10869
|
-
return None
|
10978
|
+
self._utc_clock = utc_clock
|
10870
10979
|
|
10871
|
-
|
10872
|
-
|
10980
|
+
def _utc_now(self) -> datetime.datetime:
|
10981
|
+
if self._utc_clock is not None:
|
10982
|
+
return self._utc_clock() # noqa
|
10983
|
+
else:
|
10984
|
+
return datetime.datetime.now(tz=datetime.timezone.utc) # noqa
|
10873
10985
|
|
10874
|
-
|
10875
|
-
|
10876
|
-
for n, p in self._providers.items():
|
10877
|
-
lst = [
|
10878
|
-
si
|
10879
|
-
for si in await p.get_installed_versions(spec)
|
10880
|
-
if spec.contains(si)
|
10881
|
-
]
|
10882
|
-
if lst:
|
10883
|
-
print(f' {n}')
|
10884
|
-
for si in lst:
|
10885
|
-
print(f' {si}')
|
10986
|
+
def _make_deploy_time(self) -> DeployTime:
|
10987
|
+
return DeployTime(self._utc_now().strftime(DEPLOY_TAG_DATETIME_FMT))
|
10886
10988
|
|
10887
|
-
|
10989
|
+
async def run_deploy(
|
10990
|
+
self,
|
10991
|
+
spec: DeploySpec,
|
10992
|
+
) -> None:
|
10993
|
+
self._paths.validate_deploy_paths()
|
10888
10994
|
|
10889
|
-
|
10890
|
-
for n, p in self._providers.items():
|
10891
|
-
lst = [
|
10892
|
-
si
|
10893
|
-
for si in await p.get_installable_versions(spec)
|
10894
|
-
if spec.contains(si)
|
10895
|
-
]
|
10896
|
-
if lst:
|
10897
|
-
print(f' {n}')
|
10898
|
-
for si in lst:
|
10899
|
-
print(f' {si}')
|
10995
|
+
#
|
10900
10996
|
|
10997
|
+
deploy_tags = DeployTagMap(
|
10998
|
+
self._make_deploy_time(),
|
10999
|
+
spec.key(),
|
11000
|
+
)
|
10901
11001
|
|
10902
|
-
|
10903
|
-
# pyenv is preferred to system interpreters as it tends to have more support for things like tkinter
|
10904
|
-
PyenvInterpProvider(try_update=True),
|
11002
|
+
#
|
10905
11003
|
|
10906
|
-
|
11004
|
+
for app in spec.apps:
|
11005
|
+
app_tags = deploy_tags.add(
|
11006
|
+
app.app,
|
11007
|
+
app.key(),
|
11008
|
+
DeployAppRev(app.git.rev),
|
11009
|
+
)
|
10907
11010
|
|
10908
|
-
|
10909
|
-
|
11011
|
+
await self._apps.prepare_app(
|
11012
|
+
app,
|
11013
|
+
app_tags,
|
11014
|
+
)
|
10910
11015
|
|
10911
11016
|
|
10912
11017
|
########################################
|
@@ -10937,65 +11042,6 @@ class DeployCommandExecutor(CommandExecutor[DeployCommand, DeployCommand.Output]
|
|
10937
11042
|
return DeployCommand.Output()
|
10938
11043
|
|
10939
11044
|
|
10940
|
-
########################################
|
10941
|
-
# ../targets/inject.py
|
10942
|
-
|
10943
|
-
|
10944
|
-
def bind_targets() -> InjectorBindings:
|
10945
|
-
lst: ta.List[InjectorBindingOrBindings] = [
|
10946
|
-
inj.bind(LocalManageTargetConnector, singleton=True),
|
10947
|
-
inj.bind(DockerManageTargetConnector, singleton=True),
|
10948
|
-
inj.bind(SshManageTargetConnector, singleton=True),
|
10949
|
-
|
10950
|
-
inj.bind(TypeSwitchedManageTargetConnector, singleton=True),
|
10951
|
-
inj.bind(ManageTargetConnector, to_key=TypeSwitchedManageTargetConnector),
|
10952
|
-
]
|
10953
|
-
|
10954
|
-
#
|
10955
|
-
|
10956
|
-
def provide_manage_target_connector_map(injector: Injector) -> ManageTargetConnectorMap:
|
10957
|
-
return ManageTargetConnectorMap({
|
10958
|
-
LocalManageTarget: injector[LocalManageTargetConnector],
|
10959
|
-
DockerManageTarget: injector[DockerManageTargetConnector],
|
10960
|
-
SshManageTarget: injector[SshManageTargetConnector],
|
10961
|
-
})
|
10962
|
-
lst.append(inj.bind(provide_manage_target_connector_map, singleton=True))
|
10963
|
-
|
10964
|
-
#
|
10965
|
-
|
10966
|
-
return inj.as_bindings(*lst)
|
10967
|
-
|
10968
|
-
|
10969
|
-
########################################
|
10970
|
-
# ../deploy/interp.py
|
10971
|
-
|
10972
|
-
|
10973
|
-
##
|
10974
|
-
|
10975
|
-
|
10976
|
-
@dc.dataclass(frozen=True)
|
10977
|
-
class InterpCommand(Command['InterpCommand.Output']):
|
10978
|
-
spec: str
|
10979
|
-
install: bool = False
|
10980
|
-
|
10981
|
-
@dc.dataclass(frozen=True)
|
10982
|
-
class Output(Command.Output):
|
10983
|
-
exe: str
|
10984
|
-
version: str
|
10985
|
-
opts: InterpOpts
|
10986
|
-
|
10987
|
-
|
10988
|
-
class InterpCommandExecutor(CommandExecutor[InterpCommand, InterpCommand.Output]):
|
10989
|
-
async def execute(self, cmd: InterpCommand) -> InterpCommand.Output:
|
10990
|
-
i = InterpSpecifier.parse(check.not_none(cmd.spec))
|
10991
|
-
o = check.not_none(await DEFAULT_INTERP_RESOLVER.resolve(i, install=cmd.install))
|
10992
|
-
return InterpCommand.Output(
|
10993
|
-
exe=o.exe,
|
10994
|
-
version=str(o.version.version),
|
10995
|
-
opts=o.version.opts,
|
10996
|
-
)
|
10997
|
-
|
10998
|
-
|
10999
11045
|
########################################
|
11000
11046
|
# ../deploy/inject.py
|
11001
11047
|
|
@@ -11144,6 +11190,8 @@ def main_bootstrap(bs: MainBootstrap) -> Injector:
|
|
11144
11190
|
|
11145
11191
|
@dc.dataclass(frozen=True)
|
11146
11192
|
class ManageConfig:
|
11193
|
+
deploy_home: ta.Optional[str] = None
|
11194
|
+
|
11147
11195
|
targets: ta.Optional[ta.Mapping[str, ManageTarget]] = None
|
11148
11196
|
|
11149
11197
|
|
@@ -11178,7 +11226,8 @@ class MainCli(ArgparseCli):
|
|
11178
11226
|
argparse_arg('--deploy-home'),
|
11179
11227
|
|
11180
11228
|
argparse_arg('target'),
|
11181
|
-
argparse_arg('command',
|
11229
|
+
argparse_arg('-f', '--command-file', action='append'),
|
11230
|
+
argparse_arg('command', nargs='*'),
|
11182
11231
|
)
|
11183
11232
|
async def run(self) -> None:
|
11184
11233
|
bs = MainBootstrap(
|
@@ -11189,7 +11238,7 @@ class MainCli(ArgparseCli):
|
|
11189
11238
|
),
|
11190
11239
|
|
11191
11240
|
deploy_config=DeployConfig(
|
11192
|
-
deploy_home=self.args.deploy_home,
|
11241
|
+
deploy_home=self.args.deploy_home or self.config().deploy_home,
|
11193
11242
|
),
|
11194
11243
|
|
11195
11244
|
remote_config=RemoteConfig(
|
@@ -11224,13 +11273,19 @@ class MainCli(ArgparseCli):
|
|
11224
11273
|
#
|
11225
11274
|
|
11226
11275
|
cmds: ta.List[Command] = []
|
11276
|
+
|
11227
11277
|
cmd: Command
|
11228
|
-
|
11278
|
+
|
11279
|
+
for c in self.args.command or []:
|
11229
11280
|
if not c.startswith('{'):
|
11230
11281
|
c = json.dumps({c: {}})
|
11231
11282
|
cmd = msh.unmarshal_obj(json.loads(c), Command)
|
11232
11283
|
cmds.append(cmd)
|
11233
11284
|
|
11285
|
+
for cf in self.args.command_file or []:
|
11286
|
+
cmd = read_config_file(cf, Command, msh=msh)
|
11287
|
+
cmds.append(cmd)
|
11288
|
+
|
11234
11289
|
#
|
11235
11290
|
|
11236
11291
|
async with injector[ManageTargetConnector].connect(tgt) as ce:
|